PranitInitial commit439f83c3 months agoHistory
1952 lines · 74.9 KB · Zig
RawDownload
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.