const std = @import("std");const io_mod = @import("../shared/io.zig");const session = @import("session.zig");const Allocator = std.mem.Allocator; const paths = @import("session_store_paths.zig");const types = @import("session_store_types.zig");const sort_utils = @import("../shared/sort_utils.zig"); const validateSessionId = paths.validateSessionId;const SessionSummary = types.SessionSummary;const ResumableSessionContinuation = types.ResumableSessionContinuation;const ResumableSessionPage = types.ResumableSessionPage;const StateSummary = types.StateSummary; const WorkspaceLatest = struct { workspace_root: []u8, session_id: []u8, fn deinit(self: *WorkspaceLatest, alloc: Allocator) void { alloc.free(self.workspace_root); alloc.free(self.session_id); self.* = undefined; }}; const SmallCacheRead = union(enum) { missing, oversized, bytes: []const u8,}; const JsonStringToken = struct { text: []const u8, owned: ?[]u8 = null, fn deinit(self: JsonStringToken, alloc: Allocator) void { if (self.owned) |value| alloc.free(value); }}; pub const session_index_file = "index.json";const session_index_marker_file = "index.pending";const session_index_marker_contents = "pending\n";const relationship_migration_snapshot_file = "relationship-migration-index.json";pub const max_session_index_bytes: usize = 16 * 1024 * 1024;const session_index_schema_version: i64 = 3;pub const relationship_migration_candidate_limit: usize = 16;const relationship_migration_read_bytes: usize = 64 * 1024;const relationship_migration_overlap_bytes: u64 = 512;const private_file_permissions = std.Io.File.Permissions.fromMode(0o600); pub const RelationshipMigrationCursor = struct { inode: u64 = 0, size: u64 = 0, mtime_ns: i128 = 0, offset: u64 = 0,}; pub const RelationshipMigrationCandidatePage = struct { cursor: RelationshipMigrationCursor, ids: std.ArrayList([]u8) = .empty, has_more: bool = false, pub fn deinit(self: *RelationshipMigrationCandidatePage, alloc: Allocator) void { for (self.ids.items) |id| alloc.free(id); self.ids.deinit(alloc); self.* = undefined; }}; fn readOptionalCacheFile(alloc: Allocator, path: []const u8, max_bytes: usize) !?[]u8 { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(io_mod.getIo()); return try io_mod.readFileToEnd(alloc, &file, max_bytes);} /// Reads the aggregate summary cache at `path`, trying a stack-buffer fast/// read then the fast parser, falling back to a heap read + scanner parse./// `error.SessionIndexNotFound` if absent. Caller owns the result.pub fn readSessionStateSummary(alloc: Allocator, path: []const u8) !StateSummary { var prefix_buf: [4096]u8 = undefined; if (try readCachePrefix(path, &prefix_buf)) |prefix| { if (try parseSessionStateSummaryFast(alloc, prefix)) |summary| { return summary; } } else { return error.SessionIndexNotFound; } var stack_buf: [64 * 1024]u8 = undefined; switch (try readSmallCacheFile(path, &stack_buf)) { .missing => return error.SessionIndexNotFound, .oversized => {}, .bytes => |bytes| { if (try parseSessionStateSummaryFast(alloc, bytes)) |summary| return summary; return parseSessionStateSummary(alloc, bytes); }, } const bytes = try readOptionalCacheFile(alloc, path, 256 * 1024) orelse return error.SessionIndexNotFound; defer alloc.free(bytes); if (try parseSessionStateSummaryFast(alloc, bytes)) |summary| return summary; return parseSessionStateSummary(alloc, bytes);} fn readCachePrefix(path: []const u8, buffer: []u8) !?[]const u8 { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(io_mod.getIo()); const count = try file.readPositionalAll(io_mod.getIo(), buffer, 0); return buffer[0..count];} /// Reads the latest session id for `target_workspace` from the summary cache,/// fast path then scanner fallback. Returns null if the workspace has no entry.fn readLatestWorkspaceSessionId(alloc: Allocator, path: []const u8, target_workspace: []const u8) !?[]u8 { var stack_buf: [64 * 1024]u8 = undefined; switch (try readSmallCacheFile(path, &stack_buf)) { .missing => return error.SessionIndexNotFound, .oversized => {}, .bytes => |bytes| { if (try parseLatestWorkspaceSessionIdFast(alloc, bytes, target_workspace)) |id| return id; return parseLatestWorkspaceSessionId(alloc, bytes, target_workspace); }, } const bytes = try readOptionalCacheFile(alloc, path, 256 * 1024) orelse return error.SessionIndexNotFound; defer alloc.free(bytes); if (try parseLatestWorkspaceSessionIdFast(alloc, bytes, target_workspace)) |id| return id; return parseLatestWorkspaceSessionId(alloc, bytes, target_workspace);} fn readSmallCacheFile(path: []const u8, buf: []u8) !SmallCacheRead { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return .missing, else => return err, }; defer file.close(io_mod.getIo()); const zio = io_mod.getIo(); var total: usize = 0; while (total < buf.len) { const n = file.readStreaming(zio, &.{buf[total..]}) catch |err| switch (err) { error.EndOfStream => return .{ .bytes = buf[0..total] }, else => return err, }; if (n == 0) return .{ .bytes = buf[0..total] }; total += n; } var extra: [1]u8 = undefined; const n = file.readStreaming(zio, &.{extra[0..]}) catch |err| switch (err) { error.EndOfStream => return .{ .bytes = buf[0..total] }, else => return err, }; return if (n == 0) .{ .bytes = buf[0..total] } else .oversized;} /// Best-effort byte scan for one workspace entry. Returns null to defer to the/// scanner path; only allocation can fail.fn parseLatestWorkspaceSessionIdFast(alloc: Allocator, bytes: []const u8, target_workspace: []const u8) Allocator.Error!?[]u8 { if (!isSimpleJsonStringContent(target_workspace)) return null; const array_key = "\"latest_by_workspace\":["; const array_start = std.mem.find(u8, bytes, array_key) orelse return null; const array_tail = bytes[array_start + array_key.len ..]; const array_end = std.mem.findScalar(u8, array_tail, ']') orelse return null; const array = array_tail[0..array_end]; var needle_buf: [std.fs.max_path_bytes + 32]u8 = undefined; const needle = std.fmt.bufPrint(&needle_buf, "\"workspace_root\":\"{s}\"", .{target_workspace}) catch return null; const match_start = std.mem.find(u8, array, needle) orelse return null; const entry_tail = array[match_start + needle.len ..]; const entry_end = std.mem.findScalar(u8, entry_tail, '}') orelse return null; const entry = entry_tail[0..entry_end]; const id_key = "\"id\":\""; const id_start = std.mem.find(u8, entry, id_key) orelse return null; const id_tail = entry[id_start + id_key.len ..]; const id_end = std.mem.findScalar(u8, id_tail, '"') orelse return null; const id = id_tail[0..id_end]; validateSessionId(id) catch return null; return try alloc.dupe(u8, id);} /// True if `text` contains no characters needing JSON escaping, so it is safe/// to match literally in the fast scan.fn isSimpleJsonStringContent(text: []const u8) bool { for (text) |byte| { if (byte < 0x20 or byte == '"' or byte == '\\') return false; } return true;} /// Best-effort byte scan of the summary cache. Returns the parsed summary, or/// null to signal "fall back to the scanner" (it never reports a hard parse/// error); only allocation can fail.fn parseSessionStateSummaryFast(alloc: Allocator, bytes: []const u8) Allocator.Error!?StateSummary { const schema_key = "\"schema_version\":1"; if (std.mem.find(u8, bytes, schema_key) == null) return null; const count_key = "\"count\":"; const count_start = std.mem.find(u8, bytes, count_key) orelse return null; const count_tail = bytes[count_start + count_key.len ..]; const count_end = countNumberEnd(count_tail); if (count_end == 0) return null; const count = std.fmt.parseUnsigned(usize, count_tail[0..count_end], 10) catch return null; const latest_key = "\"latest_id\":"; const latest_start = std.mem.find(u8, bytes, latest_key) orelse return null; const latest_tail = bytes[latest_start + latest_key.len ..]; if (std.mem.startsWith(u8, latest_tail, "null")) { return .{ .count = count, .latest_id = null }; } if (latest_tail.len == 0 or latest_tail[0] != '"') return null; const id_tail = latest_tail[1..]; const id_end = std.mem.findScalar(u8, id_tail, '"') orelse return null; const id = id_tail[0..id_end]; if (!isSimpleJsonStringContent(id)) return null; validateSessionId(id) catch return null; return .{ .count = count, .latest_id = try alloc.dupe(u8, id), };} fn countNumberEnd(bytes: []const u8) usize { var end: usize = 0; while (end < bytes.len and std.ascii.isDigit(bytes[end])) { end += 1; } return end;} /// Authoritative `std.json.Scanner` parse of the summary cache. Strict:/// malformed input is `error.InvalidSessionIndex`.fn parseSessionStateSummary(alloc: Allocator, bytes: []const u8) !StateSummary { var scanner = std.json.Scanner.initCompleteInput(alloc, bytes); defer scanner.deinit(); try expectJsonToken(try scanner.next(), .object_begin); var schema_ok = false; var count: ?usize = null; var latest_id: ?[]u8 = null; errdefer if (latest_id) |id| alloc.free(id); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "schema_version")) { schema_ok = try readSummarySchemaVersion(&scanner); } else if (std.mem.eql(u8, key.text, "count")) { count = try readJsonUsize(&scanner); } else if (std.mem.eql(u8, key.text, "latest_id")) { latest_id = try readOptionalJsonStringDup(&scanner, alloc); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } try expectJsonToken(try scanner.next(), .end_of_document); if (!schema_ok) return error.InvalidSessionIndex; if (latest_id) |id| validateSessionId(id) catch return error.InvalidSessionIndex; return .{ .count = count orelse return error.InvalidSessionIndex, .latest_id = latest_id };} /// Authoritative scanner parse returning the matched workspace id, or null.fn parseLatestWorkspaceSessionId(alloc: Allocator, bytes: []const u8, target_workspace: []const u8) !?[]u8 { var scanner = std.json.Scanner.initCompleteInput(alloc, bytes); defer scanner.deinit(); try expectJsonToken(try scanner.next(), .object_begin); var schema_ok = false; var matched_id: ?[]u8 = null; errdefer if (matched_id) |id| alloc.free(id); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "schema_version")) { schema_ok = try readSummarySchemaVersion(&scanner); } else if (std.mem.eql(u8, key.text, "latest_by_workspace")) { matched_id = try readLatestWorkspaceArray(&scanner, alloc, target_workspace); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } try expectJsonToken(try scanner.next(), .end_of_document); if (!schema_ok) return error.InvalidSessionIndex; return matched_id;} fn readLatestWorkspaceArray(scanner: *std.json.Scanner, alloc: Allocator, target_workspace: []const u8) !?[]u8 { try expectJsonToken(try scanner.next(), .array_begin); var matched_id: ?[]u8 = null; errdefer if (matched_id) |id| alloc.free(id); while (true) { const token = try scanner.next(); switch (token) { .array_end => return matched_id, .object_begin => { const candidate = try readLatestWorkspaceEntry(scanner, alloc, target_workspace); if (candidate) |id| { if (matched_id == null) { matched_id = id; } else { alloc.free(id); } } }, else => return error.InvalidSessionIndex, } }} fn readLatestWorkspaceEntry(scanner: *std.json.Scanner, alloc: Allocator, target_workspace: []const u8) !?[]u8 { var workspace_matches = false; var id: ?[]u8 = null; errdefer if (id) |value| alloc.free(value); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "workspace_root")) { const value = try readJsonString(scanner, alloc); defer value.deinit(alloc); workspace_matches = std.mem.eql(u8, value.text, target_workspace); } else if (std.mem.eql(u8, key.text, "id")) { if (id) |old| alloc.free(old); id = try readJsonStringDup(scanner, alloc); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } if (workspace_matches) { const matched_id = id orelse return error.InvalidSessionIndex; validateSessionId(matched_id) catch return error.InvalidSessionIndex; return matched_id; } if (id) |value| alloc.free(value); return null;} fn readSummarySchemaVersion(scanner: *std.json.Scanner) !bool { const token = try scanner.next(); switch (token) { .number => |raw| return std.mem.eql(u8, raw, "1"), else => return error.InvalidSessionIndex, }} fn readJsonUsize(scanner: *std.json.Scanner) !usize { const token = try scanner.next(); switch (token) { .number => |raw| return std.fmt.parseUnsigned(usize, raw, 10) catch error.InvalidSessionIndex, else => return error.InvalidSessionIndex, }} fn readOptionalJsonStringDup(scanner: *std.json.Scanner, alloc: Allocator) !?[]u8 { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .null => return null, .string, .allocated_string => { const value = try jsonStringFromToken(token); defer value.deinit(alloc); return try alloc.dupe(u8, value.text); }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, }} fn readJsonStringDup(scanner: *std.json.Scanner, alloc: Allocator) ![]u8 { const value = try readJsonString(scanner, alloc); defer value.deinit(alloc); return try alloc.dupe(u8, value.text);} fn readJsonString(scanner: *std.json.Scanner, alloc: Allocator) !JsonStringToken { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .string, .allocated_string => return jsonStringFromToken(token), else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, }} fn jsonStringFromToken(token: std.json.Token) !JsonStringToken { return switch (token) { .string => |text| .{ .text = text }, .allocated_string => |text| .{ .text = text, .owned = text }, else => error.InvalidSessionIndex, };} fn freeJsonToken(alloc: Allocator, token: std.json.Token) void { switch (token) { .allocated_string => |text| alloc.free(text), .allocated_number => |text| alloc.free(text), else => {}, }} fn expectJsonToken(token: std.json.Token, comptime expected: std.json.TokenType) !void { const actual: std.json.TokenType = switch (token) { .object_begin => .object_begin, .object_end => .object_end, .array_begin => .array_begin, .array_end => .array_end, .true => .true, .false => .false, .null => .null, .number, .partial_number, .allocated_number => .number, .string, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4, .allocated_string => .string, .end_of_document => .end_of_document, }; if (actual != expected) return error.InvalidSessionIndex;} fn appendLatestWorkspaceSummary(alloc: Allocator, latest: *std.ArrayList(WorkspaceLatest), workspace_root: []const u8, session_id: []const u8) !void { for (latest.items) |*entry| { if (!std.mem.eql(u8, entry.workspace_root, workspace_root)) continue; if (std.mem.order(u8, session_id, entry.session_id) != .gt) return; const replacement_id = try alloc.dupe(u8, session_id); alloc.free(entry.session_id); entry.session_id = replacement_id; return; } const owned_workspace_root = try alloc.dupe(u8, workspace_root); errdefer alloc.free(owned_workspace_root); const owned_session_id = try alloc.dupe(u8, session_id); errdefer alloc.free(owned_session_id); try latest.append(alloc, .{ .workspace_root = owned_workspace_root, .session_id = owned_session_id, });} /// Parses the session index document into newest-first summaries, validating/// the schema version and each id. Caller owns and frees the list.fn parseSessionIndex(alloc: Allocator, json_text: []const u8) !std.ArrayList(SessionSummary) { var parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.InvalidSessionIndex; defer parsed.deinit(); if (parsed.value != .object) return error.InvalidSessionIndex; const schema_value = parsed.value.object.get("schema_version") orelse return error.InvalidSessionIndex; if (schema_value != .integer or schema_value.integer != session_index_schema_version) return error.InvalidSessionIndex; const sessions_value = parsed.value.object.get("sessions") orelse return error.InvalidSessionIndex;Lines 1–500 of 1952. Load more
const std = @import("std"); const io_mod = @import("../shared/io.zig"); const session = @import("session.zig"); const Allocator = std.mem.Allocator; const paths = @import("session_store_paths.zig"); const types = @import("session_store_types.zig"); const sort_utils = @import("../shared/sort_utils.zig"); const validateSessionId = paths.validateSessionId; const SessionSummary = types.SessionSummary; const ResumableSessionContinuation = types.ResumableSessionContinuation; const ResumableSessionPage = types.ResumableSessionPage; const StateSummary = types.StateSummary; const WorkspaceLatest = struct { workspace_root: []u8, session_id: []u8, fn deinit(self: *WorkspaceLatest, alloc: Allocator) void { alloc.free(self.workspace_root); alloc.free(self.session_id); self.* = undefined; } }; const SmallCacheRead = union(enum) { missing, oversized, bytes: []const u8, }; const JsonStringToken = struct { text: []const u8, owned: ?[]u8 = null, fn deinit(self: JsonStringToken, alloc: Allocator) void { if (self.owned) |value| alloc.free(value); } }; pub const session_index_file = "index.json"; const session_index_marker_file = "index.pending"; const session_index_marker_contents = "pending\n"; const relationship_migration_snapshot_file = "relationship-migration-index.json"; pub const max_session_index_bytes: usize = 16 * 1024 * 1024; const session_index_schema_version: i64 = 3; pub const relationship_migration_candidate_limit: usize = 16; const relationship_migration_read_bytes: usize = 64 * 1024; const relationship_migration_overlap_bytes: u64 = 512; const private_file_permissions = std.Io.File.Permissions.fromMode(0o600); pub const RelationshipMigrationCursor = struct { inode: u64 = 0, size: u64 = 0, mtime_ns: i128 = 0, offset: u64 = 0, }; pub const RelationshipMigrationCandidatePage = struct { cursor: RelationshipMigrationCursor, ids: std.ArrayList([]u8) = .empty, has_more: bool = false, pub fn deinit(self: *RelationshipMigrationCandidatePage, alloc: Allocator) void { for (self.ids.items) |id| alloc.free(id); self.ids.deinit(alloc); self.* = undefined; } }; fn readOptionalCacheFile(alloc: Allocator, path: []const u8, max_bytes: usize) !?[]u8 { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(io_mod.getIo()); return try io_mod.readFileToEnd(alloc, &file, max_bytes); } /// Reads the aggregate summary cache at `path`, trying a stack-buffer fast /// read then the fast parser, falling back to a heap read + scanner parse. /// `error.SessionIndexNotFound` if absent. Caller owns the result. pub fn readSessionStateSummary(alloc: Allocator, path: []const u8) !StateSummary { var prefix_buf: [4096]u8 = undefined; if (try readCachePrefix(path, &prefix_buf)) |prefix| { if (try parseSessionStateSummaryFast(alloc, prefix)) |summary| { return summary; } } else { return error.SessionIndexNotFound; } var stack_buf: [64 * 1024]u8 = undefined; switch (try readSmallCacheFile(path, &stack_buf)) { .missing => return error.SessionIndexNotFound, .oversized => {}, .bytes => |bytes| { if (try parseSessionStateSummaryFast(alloc, bytes)) |summary| return summary; return parseSessionStateSummary(alloc, bytes); }, } const bytes = try readOptionalCacheFile(alloc, path, 256 * 1024) orelse return error.SessionIndexNotFound; defer alloc.free(bytes); if (try parseSessionStateSummaryFast(alloc, bytes)) |summary| return summary; return parseSessionStateSummary(alloc, bytes); } fn readCachePrefix(path: []const u8, buffer: []u8) !?[]const u8 { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(io_mod.getIo()); const count = try file.readPositionalAll(io_mod.getIo(), buffer, 0); return buffer[0..count]; } /// Reads the latest session id for `target_workspace` from the summary cache, /// fast path then scanner fallback. Returns null if the workspace has no entry. fn readLatestWorkspaceSessionId(alloc: Allocator, path: []const u8, target_workspace: []const u8) !?[]u8 { var stack_buf: [64 * 1024]u8 = undefined; switch (try readSmallCacheFile(path, &stack_buf)) { .missing => return error.SessionIndexNotFound, .oversized => {}, .bytes => |bytes| { if (try parseLatestWorkspaceSessionIdFast(alloc, bytes, target_workspace)) |id| return id; return parseLatestWorkspaceSessionId(alloc, bytes, target_workspace); }, } const bytes = try readOptionalCacheFile(alloc, path, 256 * 1024) orelse return error.SessionIndexNotFound; defer alloc.free(bytes); if (try parseLatestWorkspaceSessionIdFast(alloc, bytes, target_workspace)) |id| return id; return parseLatestWorkspaceSessionId(alloc, bytes, target_workspace); } fn readSmallCacheFile(path: []const u8, buf: []u8) !SmallCacheRead { var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return .missing, else => return err, }; defer file.close(io_mod.getIo()); const zio = io_mod.getIo(); var total: usize = 0; while (total < buf.len) { const n = file.readStreaming(zio, &.{buf[total..]}) catch |err| switch (err) { error.EndOfStream => return .{ .bytes = buf[0..total] }, else => return err, }; if (n == 0) return .{ .bytes = buf[0..total] }; total += n; } var extra: [1]u8 = undefined; const n = file.readStreaming(zio, &.{extra[0..]}) catch |err| switch (err) { error.EndOfStream => return .{ .bytes = buf[0..total] }, else => return err, }; return if (n == 0) .{ .bytes = buf[0..total] } else .oversized; } /// Best-effort byte scan for one workspace entry. Returns null to defer to the /// scanner path; only allocation can fail. fn parseLatestWorkspaceSessionIdFast(alloc: Allocator, bytes: []const u8, target_workspace: []const u8) Allocator.Error!?[]u8 { if (!isSimpleJsonStringContent(target_workspace)) return null; const array_key = "\"latest_by_workspace\":["; const array_start = std.mem.find(u8, bytes, array_key) orelse return null; const array_tail = bytes[array_start + array_key.len ..]; const array_end = std.mem.findScalar(u8, array_tail, ']') orelse return null; const array = array_tail[0..array_end]; var needle_buf: [std.fs.max_path_bytes + 32]u8 = undefined; const needle = std.fmt.bufPrint(&needle_buf, "\"workspace_root\":\"{s}\"", .{target_workspace}) catch return null; const match_start = std.mem.find(u8, array, needle) orelse return null; const entry_tail = array[match_start + needle.len ..]; const entry_end = std.mem.findScalar(u8, entry_tail, '}') orelse return null; const entry = entry_tail[0..entry_end]; const id_key = "\"id\":\""; const id_start = std.mem.find(u8, entry, id_key) orelse return null; const id_tail = entry[id_start + id_key.len ..]; const id_end = std.mem.findScalar(u8, id_tail, '"') orelse return null; const id = id_tail[0..id_end]; validateSessionId(id) catch return null; return try alloc.dupe(u8, id); } /// True if `text` contains no characters needing JSON escaping, so it is safe /// to match literally in the fast scan. fn isSimpleJsonStringContent(text: []const u8) bool { for (text) |byte| { if (byte < 0x20 or byte == '"' or byte == '\\') return false; } return true; } /// Best-effort byte scan of the summary cache. Returns the parsed summary, or /// null to signal "fall back to the scanner" (it never reports a hard parse /// error); only allocation can fail. fn parseSessionStateSummaryFast(alloc: Allocator, bytes: []const u8) Allocator.Error!?StateSummary { const schema_key = "\"schema_version\":1"; if (std.mem.find(u8, bytes, schema_key) == null) return null; const count_key = "\"count\":"; const count_start = std.mem.find(u8, bytes, count_key) orelse return null; const count_tail = bytes[count_start + count_key.len ..]; const count_end = countNumberEnd(count_tail); if (count_end == 0) return null; const count = std.fmt.parseUnsigned(usize, count_tail[0..count_end], 10) catch return null; const latest_key = "\"latest_id\":"; const latest_start = std.mem.find(u8, bytes, latest_key) orelse return null; const latest_tail = bytes[latest_start + latest_key.len ..]; if (std.mem.startsWith(u8, latest_tail, "null")) { return .{ .count = count, .latest_id = null }; } if (latest_tail.len == 0 or latest_tail[0] != '"') return null; const id_tail = latest_tail[1..]; const id_end = std.mem.findScalar(u8, id_tail, '"') orelse return null; const id = id_tail[0..id_end]; if (!isSimpleJsonStringContent(id)) return null; validateSessionId(id) catch return null; return .{ .count = count, .latest_id = try alloc.dupe(u8, id), }; } fn countNumberEnd(bytes: []const u8) usize { var end: usize = 0; while (end < bytes.len and std.ascii.isDigit(bytes[end])) { end += 1; } return end; } /// Authoritative `std.json.Scanner` parse of the summary cache. Strict: /// malformed input is `error.InvalidSessionIndex`. fn parseSessionStateSummary(alloc: Allocator, bytes: []const u8) !StateSummary { var scanner = std.json.Scanner.initCompleteInput(alloc, bytes); defer scanner.deinit(); try expectJsonToken(try scanner.next(), .object_begin); var schema_ok = false; var count: ?usize = null; var latest_id: ?[]u8 = null; errdefer if (latest_id) |id| alloc.free(id); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "schema_version")) { schema_ok = try readSummarySchemaVersion(&scanner); } else if (std.mem.eql(u8, key.text, "count")) { count = try readJsonUsize(&scanner); } else if (std.mem.eql(u8, key.text, "latest_id")) { latest_id = try readOptionalJsonStringDup(&scanner, alloc); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } try expectJsonToken(try scanner.next(), .end_of_document); if (!schema_ok) return error.InvalidSessionIndex; if (latest_id) |id| validateSessionId(id) catch return error.InvalidSessionIndex; return .{ .count = count orelse return error.InvalidSessionIndex, .latest_id = latest_id }; } /// Authoritative scanner parse returning the matched workspace id, or null. fn parseLatestWorkspaceSessionId(alloc: Allocator, bytes: []const u8, target_workspace: []const u8) !?[]u8 { var scanner = std.json.Scanner.initCompleteInput(alloc, bytes); defer scanner.deinit(); try expectJsonToken(try scanner.next(), .object_begin); var schema_ok = false; var matched_id: ?[]u8 = null; errdefer if (matched_id) |id| alloc.free(id); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "schema_version")) { schema_ok = try readSummarySchemaVersion(&scanner); } else if (std.mem.eql(u8, key.text, "latest_by_workspace")) { matched_id = try readLatestWorkspaceArray(&scanner, alloc, target_workspace); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } try expectJsonToken(try scanner.next(), .end_of_document); if (!schema_ok) return error.InvalidSessionIndex; return matched_id; } fn readLatestWorkspaceArray(scanner: *std.json.Scanner, alloc: Allocator, target_workspace: []const u8) !?[]u8 { try expectJsonToken(try scanner.next(), .array_begin); var matched_id: ?[]u8 = null; errdefer if (matched_id) |id| alloc.free(id); while (true) { const token = try scanner.next(); switch (token) { .array_end => return matched_id, .object_begin => { const candidate = try readLatestWorkspaceEntry(scanner, alloc, target_workspace); if (candidate) |id| { if (matched_id == null) { matched_id = id; } else { alloc.free(id); } } }, else => return error.InvalidSessionIndex, } } } fn readLatestWorkspaceEntry(scanner: *std.json.Scanner, alloc: Allocator, target_workspace: []const u8) !?[]u8 { var workspace_matches = false; var id: ?[]u8 = null; errdefer if (id) |value| alloc.free(value); while (true) { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .object_end => break, .string, .allocated_string => { const key = try jsonStringFromToken(token); defer key.deinit(alloc); if (std.mem.eql(u8, key.text, "workspace_root")) { const value = try readJsonString(scanner, alloc); defer value.deinit(alloc); workspace_matches = std.mem.eql(u8, value.text, target_workspace); } else if (std.mem.eql(u8, key.text, "id")) { if (id) |old| alloc.free(old); id = try readJsonStringDup(scanner, alloc); } else { try scanner.skipValue(); } }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } if (workspace_matches) { const matched_id = id orelse return error.InvalidSessionIndex; validateSessionId(matched_id) catch return error.InvalidSessionIndex; return matched_id; } if (id) |value| alloc.free(value); return null; } fn readSummarySchemaVersion(scanner: *std.json.Scanner) !bool { const token = try scanner.next(); switch (token) { .number => |raw| return std.mem.eql(u8, raw, "1"), else => return error.InvalidSessionIndex, } } fn readJsonUsize(scanner: *std.json.Scanner) !usize { const token = try scanner.next(); switch (token) { .number => |raw| return std.fmt.parseUnsigned(usize, raw, 10) catch error.InvalidSessionIndex, else => return error.InvalidSessionIndex, } } fn readOptionalJsonStringDup(scanner: *std.json.Scanner, alloc: Allocator) !?[]u8 { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .null => return null, .string, .allocated_string => { const value = try jsonStringFromToken(token); defer value.deinit(alloc); return try alloc.dupe(u8, value.text); }, else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } fn readJsonStringDup(scanner: *std.json.Scanner, alloc: Allocator) ![]u8 { const value = try readJsonString(scanner, alloc); defer value.deinit(alloc); return try alloc.dupe(u8, value.text); } fn readJsonString(scanner: *std.json.Scanner, alloc: Allocator) !JsonStringToken { const token = try scanner.nextAlloc(alloc, .alloc_if_needed); switch (token) { .string, .allocated_string => return jsonStringFromToken(token), else => { freeJsonToken(alloc, token); return error.InvalidSessionIndex; }, } } fn jsonStringFromToken(token: std.json.Token) !JsonStringToken { return switch (token) { .string => |text| .{ .text = text }, .allocated_string => |text| .{ .text = text, .owned = text }, else => error.InvalidSessionIndex, }; } fn freeJsonToken(alloc: Allocator, token: std.json.Token) void { switch (token) { .allocated_string => |text| alloc.free(text), .allocated_number => |text| alloc.free(text), else => {}, } } fn expectJsonToken(token: std.json.Token, comptime expected: std.json.TokenType) !void { const actual: std.json.TokenType = switch (token) { .object_begin => .object_begin, .object_end => .object_end, .array_begin => .array_begin, .array_end => .array_end, .true => .true, .false => .false, .null => .null, .number, .partial_number, .allocated_number => .number, .string, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4, .allocated_string => .string, .end_of_document => .end_of_document, }; if (actual != expected) return error.InvalidSessionIndex; } fn appendLatestWorkspaceSummary(alloc: Allocator, latest: *std.ArrayList(WorkspaceLatest), workspace_root: []const u8, session_id: []const u8) !void { for (latest.items) |*entry| { if (!std.mem.eql(u8, entry.workspace_root, workspace_root)) continue; if (std.mem.order(u8, session_id, entry.session_id) != .gt) return; const replacement_id = try alloc.dupe(u8, session_id); alloc.free(entry.session_id); entry.session_id = replacement_id; return; } const owned_workspace_root = try alloc.dupe(u8, workspace_root); errdefer alloc.free(owned_workspace_root); const owned_session_id = try alloc.dupe(u8, session_id); errdefer alloc.free(owned_session_id); try latest.append(alloc, .{ .workspace_root = owned_workspace_root, .session_id = owned_session_id, }); } /// Parses the session index document into newest-first summaries, validating /// the schema version and each id. Caller owns and frees the list. fn parseSessionIndex(alloc: Allocator, json_text: []const u8) !std.ArrayList(SessionSummary) { var parsed = std.json.parseFromSlice(std.json.Value, alloc, json_text, .{}) catch return error.InvalidSessionIndex; defer parsed.deinit(); if (parsed.value != .object) return error.InvalidSessionIndex; const schema_value = parsed.value.object.get("schema_version") orelse return error.InvalidSessionIndex; if (schema_value != .integer or schema_value.integer != session_index_schema_version) return error.InvalidSessionIndex; const sessions_value = parsed.value.object.get("sessions") orelse return error.InvalidSessionIndex;