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. Load more
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) {