PranitInitial commit439f83c3 months agoHistory
762 lines · 26.0 KB · Zig
RawDownload
const std = @import("std");const builtin = @import("builtin");const io_mod = @import("../shared/io.zig");const session = @import("session.zig");const session_event = @import("session_event.zig");const session_json = @import("session_json.zig");const session_log = @import("session_log.zig");const session_projection = @import("session_projection.zig");const Allocator = std.mem.Allocator; const paths = @import("session_store_paths.zig"); const validateSessionId = paths.validateSessionId; const AuthorityClassification = enum {    schema_v3,    legacy,}; const AuthoritySource = enum {    native_create,    legacy_migration,}; const AuthorityMarker = struct {    session_id: []u8,    authority_id: session_event.Identifier,    source: AuthoritySource,     pub fn deinit(self: *AuthorityMarker, alloc: Allocator) void {        alloc.free(self.session_id);        self.* = undefined;    }}; const LegacyFingerprint = struct {    schema: session_json.LegacySchemaVersion,    primary_bytes: u64,    primary_sha256: session_projection.Digest,}; const AuthorityTransitionKind = enum {    session_create,    legacy_to_v3,}; pub const AuthorityTransition = struct {    session_id: []u8,    kind: AuthorityTransitionKind,    authority_id: session_event.Identifier,    prior: ?LegacyFingerprint,    proposed: session_log.CommitPosition,     pub fn deinit(self: *AuthorityTransition, alloc: Allocator) void {        alloc.free(self.session_id);        self.* = undefined;    }}; /// Shared front half of the two `classifyAuthority*` entry points: rejects a/// pending authority fence, then returns `.schema_v3` when a valid marker is/// present, or `null` when none is (caller decides legacy vs. orphan)./// Returns `error.InvalidSessionFormat` if the marker names a different session.fn classifyMarker(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    session_id: []const u8,) !?AuthorityClassification {    try requireAuthorityFenceAbsent(alloc, session_dir, session_id);    const marker = try loadAuthorityMarkerOptional(alloc, session_dir);    if (marker) |marker_value| {        var owned = marker_value;        defer owned.deinit(alloc);        if (!std.mem.eql(u8, owned.session_id, session_id)) {            return error.InvalidSessionFormat;        }        return .schema_v3;    }    return null;} /// Classifies a session as schema-v3 or legacy. A directory with no marker but/// a complete prepared schema-v3 manifest is a creation orphan and reported as/// `error.SessionNotFound` (it must not be read as legacy).pub fn classifyAuthority(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    session_id: []const u8,) !AuthorityClassification {    if (try classifyMarker(alloc, session_dir, session_id)) |classification| {        return classification;    }    if (try isPreparedCreationOrphan(alloc, session_dir, session_id)) {        return error.SessionNotFound;    }    return .legacy;} /// Like `classifyAuthority` but treats a marker-less directory as legacy without/// the creation-orphan check, so an oversized legacy snapshot can still be/// migrated under an explicit `allow_large` request.pub fn classifyAuthorityAllowingLargeLegacy(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    session_id: []const u8,) !AuthorityClassification {    if (try classifyMarker(alloc, session_dir, session_id)) |classification| {        return classification;    }    return .legacy;} fn isPreparedCreationOrphan(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    session_id: []const u8,) !bool {    var file = openSessionFile(session_dir, "session.json", .read_only) catch |err| switch (err) {        error.FileNotFound => return false,        else => return err,    };    defer file.close(io_mod.getIo());    const stat = try file.stat(io_mod.getIo());    if (stat.size > session_projection.manifest_max_bytes) {        return false;    }     const manifest_bytes = readExactLegacyFile(        alloc,        &file,        stat.size,    ) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return error.InvalidSessionFormat,    };    defer alloc.free(manifest_bytes);    const schema_version = manifestSchemaVersion(alloc, manifest_bytes) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return false,    };    if (schema_version != 3) return false;     var manifest = session_projection.decodeManifest(        alloc,        manifest_bytes,    ) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return error.InvalidSessionFormat,    };    defer manifest.deinit(alloc);    if (!std.mem.eql(u8, manifest.id, session_id)) {        return error.InvalidSessionFormat;    }    return true;} /// Reads and validates `authority.json` if present. Returns null when absent;/// `error.InvalidSessionFormat` on a malformed or wrong-schema marker. Caller/// owns the returned marker.pub fn loadAuthorityMarkerOptional(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,) !?AuthorityMarker {    const bytes = try readOptionalSessionFile(        alloc,        session_dir,        "authority.json",        16 * 1024,    ) orelse return null;    defer alloc.free(bytes);    var parsed = std.json.parseFromSlice(std.json.Value, alloc, bytes, .{        .parse_numbers = false,    }) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return error.InvalidSessionFormat,    };    defer parsed.deinit();    if (parsed.value != .object) return error.InvalidSessionFormat;    const object = parsed.value.object;    if (try jsonU64(object, "schema_version") != 1 or        !std.mem.eql(u8, try objectString(object, "storage_format"), "event_log_v1"))    {        return error.InvalidSessionFormat;    }    const source = std.meta.stringToEnum(        AuthoritySource,        try objectString(object, "source"),    ) orelse return error.InvalidSessionFormat;    const session_id = try alloc.dupe(u8, try objectString(object, "session_id"));    errdefer alloc.free(session_id);    try validateSessionId(session_id);    return .{        .session_id = session_id,        .authority_id = try parseIdentifier(try objectString(object, "authority_id")),        .source = source,    };} /// Fails with `error.SessionAuthorityBoundaryUnavailable` if an in-flight/// `authority.pending.json` transition exists for this session (the "fence"),/// so read paths refuse a directory mid-migration.pub fn requireAuthorityFenceAbsent(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    session_id: []const u8,) !void {    const transition = try loadAuthorityTransitionOptional(        alloc,        session_dir,    ) orelse return;    var owned = transition;    defer owned.deinit(alloc);    if (!std.mem.eql(u8, owned.session_id, session_id)) {        return error.InvalidSessionFormat;    }    return error.SessionAuthorityBoundaryUnavailable;} /// Reads and exact-parses `authority.pending.json` if present, returning null/// when absent. Caller owns the returned transition.pub fn loadAuthorityTransitionOptional(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,) !?AuthorityTransition {    const bytes = try readOptionalSessionFile(        alloc,        session_dir,        "authority.pending.json",        32 * 1024,    ) orelse return null;    defer alloc.free(bytes);    var parsed = std.json.parseFromSlice(std.json.Value, alloc, bytes, .{        .parse_numbers = false,    }) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return error.InvalidSessionFormat,    };    defer parsed.deinit();    const expected_keys = [_][]const u8{        "schema_version",        "session_id",        "operation_id",        "kind",        "authority_id",        "prior",        "proposed",    };    const object = try exactJsonObject(parsed.value, &expected_keys);    if (try jsonU64(object, "schema_version") != 1) {        return error.InvalidSessionFormat;    }    const session_id = try alloc.dupe(u8, try objectString(object, "session_id"));    errdefer alloc.free(session_id);    try validateSessionId(session_id);    _ = try parseIdentifier(try objectString(object, "operation_id"));    const kind = std.meta.stringToEnum(        AuthorityTransitionKind,        try objectString(object, "kind"),    ) orelse return error.InvalidSessionFormat;    const authority_id = try parseIdentifier(        try objectString(object, "authority_id"),    );    const prior_value = object.get("prior") orelse        return error.InvalidSessionFormat;    const prior: ?LegacyFingerprint = switch (kind) {        .session_create => blk: {            if (prior_value != .null) return error.InvalidSessionFormat;            break :blk null;        },        .legacy_to_v3 => try parseLegacyFingerprint(prior_value),    };    return .{        .session_id = session_id,        .kind = kind,        .authority_id = authority_id,        .prior = prior,        .proposed = try parseProposedPosition(            object.get("proposed") orelse return error.InvalidSessionFormat,        ),    };} fn parseLegacyFingerprint(value: std.json.Value) error{InvalidSessionFormat}!LegacyFingerprint {    const expected_keys = [_][]const u8{        "storage_format",        "primary_bytes",        "primary_sha256",    };    const object = try exactJsonObject(value, &expected_keys);    const storage = try objectString(object, "storage_format");    const schema: session_json.LegacySchemaVersion =        if (std.mem.eql(u8, storage, "legacy_snapshot_v1"))            .v1        else if (std.mem.eql(u8, storage, "legacy_snapshot_v2"))            .v2        else            return error.InvalidSessionFormat;    return .{        .schema = schema,        .primary_bytes = try jsonU64(object, "primary_bytes"),        .primary_sha256 = try parseDigest(            try objectString(object, "primary_sha256"),        ),    };} fn parseProposedPosition(value: std.json.Value) error{InvalidSessionFormat}!session_log.CommitPosition {    const expected_keys = [_][]const u8{        "storage_format",        "log_generation",        "through_seq",        "through_event_id",        "through_event_log_bytes",    };    const object = try exactJsonObject(value, &expected_keys);    if (!std.mem.eql(        u8,        try objectString(object, "storage_format"),        "event_log_v1",    )) {        return error.InvalidSessionFormat;    }    return .{        .log_generation = try parseIdentifier(            try objectString(object, "log_generation"),        ),        .through_seq = try jsonU64(object, "through_seq"),        .through_event_id = try parseIdentifier(            try objectString(object, "through_event_id"),        ),        .through_event_log_bytes = try jsonU64(            object,            "through_event_log_bytes",        ),    };} /// Returns the object only if it has exactly `expected_keys` and no others,/// rejecting unknown/missing keys with `error.InvalidSessionFormat`.pub fn exactJsonObject(    value: std.json.Value,    expected_keys: []const []const u8,) !std.json.ObjectMap {    if (value != .object or value.object.count() != expected_keys.len) {        return error.InvalidSessionFormat;    }    var iter = value.object.iterator();    while (iter.next()) |entry| {        var found = false;        for (expected_keys) |key| {            if (std.mem.eql(u8, entry.key_ptr.*, key)) {                found = true;                break;            }        }        if (!found) return error.InvalidSessionFormat;    }    return value.object;} fn parseDigest(raw: []const u8) error{InvalidSessionFormat}!session_projection.Digest {    if (raw.len != @sizeOf(session_projection.Digest) * 2) {        return error.InvalidSessionFormat;    }    var result: session_projection.Digest = undefined;    _ = std.fmt.hexToBytes(&result, raw) catch        return error.InvalidSessionFormat;    const canonical = std.fmt.bytesToHex(result, .lower);    if (!std.mem.eql(u8, &canonical, raw)) {        return error.InvalidSessionFormat;    }    return result;} fn legacyFingerprintMatches(    alloc: Allocator,    bytes: []const u8,    expected: LegacyFingerprint,) !bool {    if (bytes.len != expected.primary_bytes) return false;    const schema = session_json.parseLegacySchemaVersion(        alloc,        bytes,    ) catch return false;    if (schema != expected.schema) return false;    const digest = session_projection.sha256(bytes);    return std.mem.eql(u8, &digest, &expected.primary_sha256);} /// Asserts a parsed transition names `session_id`; otherwise `error.InvalidSessionFormat`.pub fn requireAuthorityTransitionSession(    transition: AuthorityTransition,    session_id: []const u8,) !void {    if (!std.mem.eql(u8, transition.session_id, session_id)) {        return error.InvalidSessionFormat;    }} fn legacyPrimaryBytes(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    name: []const u8,    expected: LegacyFingerprint,) !?[]u8 {    const max_bytes = std.math.cast(        usize,        std.math.add(u64, expected.primary_bytes, 1) catch            return error.LegacySessionMigrationResourceExhausted,    ) orelse return error.LegacySessionMigrationResourceExhausted;    const bytes = try readOptionalSessionFile(        alloc,        session_dir,        name,        max_bytes,    ) orelse return null;    errdefer alloc.free(bytes);    if (!try legacyFingerprintMatches(alloc, bytes, expected)) {        alloc.free(bytes);        return null;    }    return bytes;} fn removeSessionEntryIfPresent(    session_dir: *io_mod.VerifiedDir,    name: []const u8,) !void {    session_dir.dir.deleteFile(io_mod.getIo(), name) catch |err| switch (err) {        error.FileNotFound => return,        else => return err,    };} /// Rolls an interrupted migration back to its legacy snapshot: restores the/// fingerprint-matched primary from the stable copy, removes the v3 marker, and/// clears the intent. Inconsistent on-disk state yields/// `error.LegacySessionMigrationIndeterminate`.pub fn restoreLegacyAuthority(    alloc: Allocator,    writable: *session_log.WritableSessionDir,    transition: AuthorityTransition,) !void {    const prior = transition.prior orelse return error.InvalidSessionFormat;    const current = try legacyPrimaryBytes(        alloc,        &writable.dir,        "session.json",        prior,    );    if (current) |bytes| {        alloc.free(bytes);    } else {        const stable = try legacyPrimaryBytes(            alloc,            &writable.dir,            "session.legacy.json",            prior,        ) orelse return error.LegacySessionMigrationIndeterminate;        defer alloc.free(stable);        io_mod.durableReplaceVerified(            alloc,            &writable.dir,            "session.json",            stable,        ) catch return error.LegacySessionMigrationIndeterminate;    }    removeSessionEntryIfPresent(&writable.dir, "authority.json") catch        return error.LegacySessionMigrationIndeterminate;    io_mod.syncVerifiedDir(writable.dir.dir) catch        return error.LegacySessionMigrationIndeterminate;    const confirmed = try legacyPrimaryBytes(        alloc,        &writable.dir,        "session.json",        prior,    ) orelse return error.LegacySessionMigrationIndeterminate;    alloc.free(confirmed);    if (try entryExistsRelative(&writable.dir, "authority.json")) {        return error.LegacySessionMigrationIndeterminate;    }    deleteSessionEntry(&writable.dir, "authority.pending.json") catch        return error.SessionAuthorityIntentCleanupPending;} /// Reads an in-session file fully, capped at `max_bytes`. Returns null if the/// file is absent; read/parse failures collapse to `error.InvalidSessionFormat`/// (except `OutOfMemory`). Caller owns the returned bytes.pub fn readOptionalSessionFile(    alloc: Allocator,    session_dir: *io_mod.VerifiedDir,    name: []const u8,    max_bytes: usize,) !?[]u8 {    var file = openSessionFile(session_dir, name, .read_only) catch |err| switch (err) {        error.FileNotFound => return null,        else => return err,    };    defer file.close(io_mod.getIo());    return io_mod.readFileToEnd(alloc, &file, max_bytes) catch |err| switch (err) {

Lines 1–500 of 762.