// Provenance: own design — shared run scripts (spec §5.4) using System.Buffers.Binary; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace Godosa.Core.Runs.Scripts; public sealed record LabelState(string Name, long Start, string File, int Line); public sealed record RunnerState(long Frame, ulong Digest, List Scopes, List Labels); public sealed record CachedCheckpoint(RunnerState State, byte[] Game); public static class CheckpointKeys { public const string RunnerVersion = "run-scripts 1"; public static IReadOnlyDictionary Compute(ScriptProgram p, string contentVersion, ulong seed) { var d = new StateDigest(); d.Add(RunnerVersion); d.Add(contentVersion); d.Add(seed); d.Add((int)p.Mode); foreach (var (k, v) in p.Vars.OrderBy(kv => kv.Key, StringComparer.Ordinal)) d.Add($"{k}={v}"); var keys = new Dictionary(); for (var i = 0; i < p.Code.Count; i++) { var instr = p.Code[i]; d.Add((int)instr.Op); d.Add(instr.Format); d.Add(instr.At.File); d.Add(instr.Text); if (instr.Op == Op.Checkpoint) keys[i] = d.Hex; } return keys; } } public sealed class CheckpointCache(string dir, long capBytes) { private static readonly byte[] Magic = "GRS1"u8.ToArray(); private static readonly JsonSerializerOptions Json = new() { Converters = { new JsonStringEnumConverter() } }; public CachedCheckpoint? Load(string key) { var path = Path.Combine(dir, key + ".snap"); if (!File.Exists(path)) return null; byte[] bytes; try { bytes = File.ReadAllBytes(path); } catch (Exception e) when (e is IOException or UnauthorizedAccessException) { return null; } if (bytes.Length < 8 || !bytes.AsSpan(0, 4).SequenceEqual(Magic)) return null; var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4)); if (length < 0 || 8 + length > bytes.Length) return null; RunnerState? state; try { state = JsonSerializer.Deserialize(Encoding.UTF8.GetString(bytes, 8, length), Json); } catch (JsonException) { return null; // a corrupt entry is a miss } return state == null ? null : new CachedCheckpoint(state, bytes[(8 + length)..]); } public void Save(string key, CachedCheckpoint cp) { Directory.CreateDirectory(dir); var json = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cp.State, Json)); var path = Path.Combine(dir, key + ".snap"); var tmp = path + ".tmp"; using (var f = File.Create(tmp)) { f.Write(Magic); Span len = stackalloc byte[4]; BinaryPrimitives.WriteInt32LittleEndian(len, json.Length); f.Write(len); f.Write(json); f.Write(cp.Game); } File.Move(tmp, path, overwrite: true); Evict(path); } private void Evict(string keep) { var files = new DirectoryInfo(dir).GetFiles("*.snap").OrderBy(f => f.LastWriteTimeUtc).ToList(); var total = files.Sum(f => f.Length); foreach (var f in files) { if (total <= capBytes) break; if (f.FullName == Path.GetFullPath(keep)) continue; total -= f.Length; f.Delete(); } } }