search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix memory safety issues and improve code quality

- use optionals instead of undefined for turso credentials
- fix use-after-free in extractRows (inline parsing in searchDocuments)
- log errors instead of silent catch {}
- use named constants (SearchCol) instead of magic numbers
- use std.Io.Writer.Allocating for proper json.Stringify compatibility
- remove unused struct definitions and empty close() function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz feec67d9 f71fe7f7

+113 -114
+110 -108
backend/src/db.zig
··· 4 4 const http = std.http; 5 5 const Allocator = mem.Allocator; 6 6 7 - pub var turso_url: []const u8 = undefined; 8 - pub var turso_token: []const u8 = undefined; 9 - pub var mutex: std.Thread.Mutex = .{}; 10 - 11 7 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; 12 8 13 - // Turso API response types 14 - const TursoResponse = struct { 15 - results: []const TursoResult, 16 - }; 17 - 18 - const TursoResult = struct { 19 - type: []const u8, 20 - response: ?TursoResultResponse = null, 21 - }; 22 - 23 - const TursoResultResponse = struct { 24 - type: []const u8, 25 - result: ?TursoQueryResult = null, 26 - }; 27 - 28 - const TursoQueryResult = struct { 29 - rows: []const []const TursoValue, 30 - }; 31 - 32 - const TursoValue = struct { 33 - type: []const u8, 34 - value: ?json.Value = null, 35 - }; 36 - 37 - // Search result type 38 - pub const SearchResult = struct { 39 - uri: []const u8, 40 - did: []const u8, 41 - title: []const u8, 42 - snippet: []const u8, 43 - createdAt: []const u8, 44 - rkey: []const u8, 45 - basePath: []const u8, 46 - }; 9 + // initialized by init(), null until then 10 + var turso_url: ?[]const u8 = null; 11 + var turso_token: ?[]const u8 = null; 12 + var mutex: std.Thread.Mutex = .{}; 47 13 48 14 pub fn init() !void { 49 15 turso_url = std.posix.getenv("TURSO_URL") orelse { ··· 55 21 return error.MissingEnv; 56 22 }; 57 23 58 - std.debug.print("using turso database: {s}\n", .{turso_url}); 24 + std.debug.print("using turso database: {s}\n", .{turso_url.?}); 59 25 try initSchema(); 60 26 } 61 - 62 - pub fn close() void {} 63 27 64 28 fn initSchema() !void { 65 29 _ = try execSql( ··· 93 57 \\) 94 58 , &.{}); 95 59 96 - // migrate: add columns if missing 97 - _ = execSql("ALTER TABLE documents ADD COLUMN publication_uri TEXT", &.{}) catch {}; 98 - _ = execSql("ALTER TABLE publications ADD COLUMN base_path TEXT", &.{}) catch {}; 60 + // migrate: add columns if missing (ignore "duplicate column" errors) 61 + _ = execSql("ALTER TABLE documents ADD COLUMN publication_uri TEXT", &.{}) catch |err| { 62 + std.debug.print("migrate documents: {}\n", .{err}); 63 + }; 64 + _ = execSql("ALTER TABLE publications ADD COLUMN base_path TEXT", &.{}) catch |err| { 65 + std.debug.print("migrate publications: {}\n", .{err}); 66 + }; 99 67 100 68 std.debug.print("turso schema initialized with FTS5\n", .{}); 101 69 } ··· 106 74 &.{ uri, did, rkey, title, content, created_at orelse "", publication_uri orelse "" }, 107 75 ); 108 76 109 - _ = execSql("DELETE FROM documents_fts WHERE uri = ?", &.{uri}) catch {}; 77 + // update FTS index - delete old entry first, then insert new 78 + _ = execSql("DELETE FROM documents_fts WHERE uri = ?", &.{uri}) catch |err| { 79 + std.debug.print("delete FTS error for {s}: {}\n", .{ uri, err }); 80 + }; 110 81 111 82 _ = execSql( 112 83 "INSERT INTO documents_fts (uri, title, content) VALUES (?, ?, ?)", 113 84 &.{ uri, title, content }, 114 85 ) catch |err| { 115 - std.debug.print("insert FTS error: {}\n", .{err}); 86 + std.debug.print("insert FTS error for {s}: {}\n", .{ uri, err }); 116 87 }; 117 88 } 118 89 ··· 124 95 } 125 96 126 97 pub fn deleteDocument(uri: []const u8) void { 127 - _ = execSql("DELETE FROM documents WHERE uri = ?", &.{uri}) catch {}; 128 - _ = execSql("DELETE FROM documents_fts WHERE uri = ?", &.{uri}) catch {}; 98 + _ = execSql("DELETE FROM documents WHERE uri = ?", &.{uri}) catch |err| { 99 + std.debug.print("delete document error for {s}: {}\n", .{ uri, err }); 100 + }; 101 + _ = execSql("DELETE FROM documents_fts WHERE uri = ?", &.{uri}) catch |err| { 102 + std.debug.print("delete document FTS error for {s}: {}\n", .{ uri, err }); 103 + }; 129 104 } 130 105 131 106 pub fn deletePublication(uri: []const u8) void { 132 - _ = execSql("DELETE FROM publications WHERE uri = ?", &.{uri}) catch {}; 107 + _ = execSql("DELETE FROM publications WHERE uri = ?", &.{uri}) catch |err| { 108 + std.debug.print("delete publication error for {s}: {}\n", .{ uri, err }); 109 + }; 133 110 } 134 111 135 - pub fn searchDocuments(alloc: Allocator, query: []const u8) !std.ArrayList(u8) { 136 - var output: std.ArrayList(u8) = .{}; 137 - const writer = output.writer(alloc); 112 + // column indices for search query results 113 + const SearchCol = struct { 114 + const uri = 0; 115 + const did = 1; 116 + const title = 2; 117 + const snippet = 3; 118 + const created_at = 4; 119 + const rkey = 5; 120 + const base_path = 6; 121 + const count = 7; 122 + }; 123 + 124 + pub fn searchDocuments(alloc: Allocator, query: []const u8) ![]const u8 { 125 + var output: std.Io.Writer.Allocating = .init(alloc); 126 + errdefer output.deinit(); 138 127 139 128 const temp_alloc = gpa.allocator(); 140 129 141 130 const result = execSql( 142 - "SELECT f.uri, d.did, d.title, snippet(documents_fts, 2, '<mark>', '</mark>', '...', 32) as snippet, d.created_at, d.rkey, p.base_path FROM documents_fts f JOIN documents d ON f.uri = d.uri LEFT JOIN publications p ON d.publication_uri = p.uri WHERE documents_fts MATCH ? ORDER BY rank LIMIT 50", 143 - &.{query}, 144 - ) catch { 145 - try writer.writeAll("[]"); 146 - return output; 131 + \\SELECT f.uri, d.did, d.title, 132 + \\ snippet(documents_fts, 2, '<mark>', '</mark>', '...', 32) as snippet, 133 + \\ d.created_at, d.rkey, p.base_path 134 + \\FROM documents_fts f 135 + \\JOIN documents d ON f.uri = d.uri 136 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 137 + \\WHERE documents_fts MATCH ? 138 + \\ORDER BY rank LIMIT 50 139 + , &.{query}) catch { 140 + try output.writer.writeAll("[]"); 141 + return try output.toOwnedSlice(); 147 142 }; 148 143 defer temp_alloc.free(result); 149 144 150 - const rows = extractRows(temp_alloc, result) catch { 151 - try writer.writeAll("[]"); 152 - return output; 145 + // parse JSON response - keep parsed alive while iterating rows 146 + const parsed = json.parseFromSlice(json.Value, temp_alloc, result, .{}) catch { 147 + try output.writer.writeAll("[]"); 148 + return try output.toOwnedSlice(); 153 149 }; 150 + defer parsed.deinit(); 154 151 155 - var jw: json.Stringify = .{ .writer = &writer.any() }; 152 + const rows = getRowsFromParsed(parsed.value) orelse { 153 + try output.writer.writeAll("[]"); 154 + return try output.toOwnedSlice(); 155 + }; 156 + 157 + var jw: json.Stringify = .{ .writer = &output.writer }; 156 158 try jw.beginArray(); 157 159 158 - for (rows) |row| { 159 - if (row.len < 7) continue; 160 + for (rows.items) |row| { 161 + if (row != .array or row.array.items.len < SearchCol.count) continue; 162 + const cols = row.array.items; 160 163 161 164 try jw.beginObject(); 162 165 try jw.objectField("uri"); 163 - try jw.write(extractText(row[0])); 166 + try jw.write(extractText(cols[SearchCol.uri])); 164 167 try jw.objectField("did"); 165 - try jw.write(extractText(row[1])); 168 + try jw.write(extractText(cols[SearchCol.did])); 166 169 try jw.objectField("title"); 167 - try jw.write(extractText(row[2])); 170 + try jw.write(extractText(cols[SearchCol.title])); 168 171 try jw.objectField("snippet"); 169 - try jw.write(extractText(row[3])); 172 + try jw.write(extractText(cols[SearchCol.snippet])); 170 173 try jw.objectField("createdAt"); 171 - try jw.write(extractText(row[4])); 174 + try jw.write(extractText(cols[SearchCol.created_at])); 172 175 try jw.objectField("rkey"); 173 - try jw.write(extractText(row[5])); 176 + try jw.write(extractText(cols[SearchCol.rkey])); 174 177 try jw.objectField("basePath"); 175 - try jw.write(extractText(row[6])); 178 + try jw.write(extractText(cols[SearchCol.base_path])); 176 179 try jw.endObject(); 177 180 } 178 181 179 182 try jw.endArray(); 180 - return output; 183 + return try output.toOwnedSlice(); 181 184 } 182 185 183 - fn extractRows(alloc: Allocator, result: []const u8) ![]const []const json.Value { 184 - const parsed = try json.parseFromSlice(json.Value, alloc, result, .{}); 185 - defer parsed.deinit(); 186 - 187 - const results = parsed.value.object.get("results") orelse return &.{}; 188 - if (results != .array or results.array.items.len == 0) return &.{}; 186 + fn getRowsFromParsed(value: json.Value) ?json.Array { 187 + const results = value.object.get("results") orelse return null; 188 + if (results != .array or results.array.items.len == 0) return null; 189 189 190 190 const first = results.array.items[0]; 191 - if (first != .object) return &.{}; 191 + if (first != .object) return null; 192 192 193 - const resp = first.object.get("response") orelse return &.{}; 194 - if (resp != .object) return &.{}; 193 + const resp = first.object.get("response") orelse return null; 194 + if (resp != .object) return null; 195 195 196 - const res = resp.object.get("result") orelse return &.{}; 197 - if (res != .object) return &.{}; 196 + const res = resp.object.get("result") orelse return null; 197 + if (res != .object) return null; 198 198 199 - const rows = res.object.get("rows") orelse return &.{}; 200 - if (rows != .array) return &.{}; 199 + const rows = res.object.get("rows") orelse return null; 200 + if (rows != .array) return null; 201 201 202 - var result_rows = std.ArrayList([]const json.Value).init(alloc); 203 - for (rows.array.items) |row| { 204 - if (row == .array) { 205 - try result_rows.append(row.array.items); 206 - } 207 - } 208 - return result_rows.toOwnedSlice(); 202 + return rows.array; 209 203 } 210 204 211 205 fn extractText(val: json.Value) []const u8 { ··· 222 216 223 217 const alloc = gpa.allocator(); 224 218 225 - // libsql:// -> https:// 219 + const url_value = turso_url orelse return error.NotInitialized; 220 + const token_value = turso_token orelse return error.NotInitialized; 221 + 222 + // strip libsql:// prefix if present, use https:// 223 + const libsql_prefix = "libsql://"; 224 + const host = if (mem.startsWith(u8, url_value, libsql_prefix)) 225 + url_value[libsql_prefix.len..] 226 + else 227 + url_value; 228 + 226 229 var url_buf: [512]u8 = undefined; 227 - const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{ 228 - if (mem.startsWith(u8, turso_url, "libsql://")) 229 - turso_url[9..] 230 - else 231 - turso_url, 232 - }) catch return error.UrlTooLong; 230 + const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{host}) catch return error.UrlTooLong; 233 231 234 232 // build request body 235 - var body: std.ArrayList(u8) = .{}; 236 - defer body.deinit(alloc); 237 - const writer = body.writer(alloc); 233 + var body: std.Io.Writer.Allocating = .init(alloc); 234 + defer body.deinit(); 238 235 239 - var jw: json.Stringify = .{ .writer = &writer.any() }; 236 + var jw: json.Stringify = .{ .writer = &body.writer }; 240 237 try jw.beginObject(); 241 238 try jw.objectField("requests"); 242 239 try jw.beginArray(); ··· 275 272 try jw.endObject(); 276 273 277 274 var auth_buf: [512]u8 = undefined; 278 - const auth_header = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{turso_token}) catch return error.AuthTooLong; 275 + const auth_header = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{token_value}) catch return error.AuthTooLong; 279 276 280 277 var client: http.Client = .{ .allocator = alloc }; 281 278 defer client.deinit(); ··· 290 287 .content_type = .{ .override = "application/json" }, 291 288 .authorization = .{ .override = auth_header }, 292 289 }, 293 - .payload = body.items, 290 + .payload = body.written(), 294 291 .response_writer = &response_body.writer, 295 292 }) catch |err| { 296 293 std.debug.print("turso request failed: {}\n", .{err}); ··· 320 317 321 318 fn parseCount(result: []const u8) i64 { 322 319 const alloc = gpa.allocator(); 323 - const rows = extractRows(alloc, result) catch return 0; 324 - if (rows.len == 0) return 0; 325 - if (rows[0].len == 0) return 0; 320 + const parsed = json.parseFromSlice(json.Value, alloc, result, .{}) catch return 0; 321 + defer parsed.deinit(); 322 + 323 + const rows = getRowsFromParsed(parsed.value) orelse return 0; 324 + if (rows.items.len == 0) return 0; 325 + 326 + const first_row = rows.items[0]; 327 + if (first_row != .array or first_row.array.items.len == 0) return 0; 326 328 327 - const val = rows[0][0]; 329 + const val = first_row.array.items[0]; 328 330 return switch (val) { 329 331 .integer => |i| i, 330 332 .object => |obj| blk: {
+3 -5
backend/src/http.zig
··· 75 75 return; 76 76 } 77 77 78 - // perform FTS search 79 - var results = try db.searchDocuments(alloc, query); 80 - defer results.deinit(alloc); 81 - 82 - try sendJson(request, results.items); 78 + // perform FTS search - arena handles cleanup 79 + const results = try db.searchDocuments(alloc, query); 80 + try sendJson(request, results); 83 81 } 84 82 85 83 fn handleStats(request: *http.Server.Request) !void {
-1
backend/src/main.zig
··· 16 16 17 17 // init turso 18 18 try db.init(); 19 - defer db.close(); 20 19 21 20 // start tap consumer in background 22 21 const tap_thread = try Thread.spawn(.{}, tap.consumer, .{allocator});