syntax = "proto3";

// Wire format for `soldr save` / `soldr load` archives. One archive
// captures the state of a build cache directory PLUS the mtimes of
// the source files Cargo will fingerprint on the next build.
//
// Forward-compat: every new field must be optional (proto3 default
// semantics make all scalar fields optional). Readers must accept
// unknown fields and skip them. Bump `version` for breaking changes.

package soldr.save.v1;

// Top-level manifest stored at the well-known path
// `SOLDR_MANIFEST.pb` at the root of the archive.
message Manifest {
  // Schema version. Currently 1. Bump only for breaking format changes.
  uint32 version = 1;

  // Wall-clock time the archive was produced (ms since UNIX epoch).
  // Pure diagnostic; not enforced on load.
  int64 saved_at_ms = 2;

  // Absolute path of the workspace at save time. Diagnostic only;
  // load happily targets a different absolute path.
  string workspace = 3;

  // The cache directory's basename inside the archive (always "cache"
  // in this implementation; reserved as a field in case future
  // versions support multi-cache archives).
  string cache_dir_name = 4;

  // Source file entries. The path is workspace-relative POSIX and
  // sorted ascending for determinism.
  repeated SourceFile files = 5;

  // Counters for diagnostic reporting on load. Optional; readers
  // recompute from `files.len()` if zero.
  uint64 source_file_count = 6;
  uint64 cache_file_count = 7;

  // Cache archive layer kind. COMPLETE is the historical all-files
  // archive. DELTA contains only changed/new cache files plus
  // deleted_cache_paths tombstones against base_manifest_blake3.
  CacheLayerKind cache_layer_kind = 8;

  // Cache file metadata, sorted by path. COMPLETE archives include every
  // cache file. DELTA archives include changed/new cache files plus
  // metadata-only entries whose content matches the base but whose mtime
  // should be replayed. Bytes are present under cache/ only for DELTA
  // entries whose content is new or changed.
  repeated CacheFile cache_files = 9;

  // Base manifest digest for DELTA archives: BLAKE3 over the protobuf
  // bytes of the base manifest file used to compute the delta.
  bytes base_manifest_blake3 = 10;

  // Cache paths present in the base layer but absent from the delta's
  // source tree. Load applies these as tombstones before extracting the
  // delta payload so restore is base + delta with delta winning.
  // Since #1548 this also carries tombstones for symlinks that were
  // present in the base layer's cache_symlinks but have been removed.
  repeated string deleted_cache_paths = 11;

  // Symlinks below the cache dir (#1548). Manifest-only — symlinks are
  // NEVER carried as tar entries, so pre-#1548 readers skip this unknown
  // field and behave exactly as before. Save records only links whose
  // target is relative, lexically stays inside the cache root, AND
  // resolves to something real; load re-validates the same containment
  // property before creating any link (a crafted manifest can never make
  // load create a link that escapes the restore root). DELTA archives
  // carry the FULL current symlink set, not a diff.
  repeated SymlinkEntry cache_symlinks = 12;
}

// A symlink below the cache dir (#1548).
message SymlinkEntry {
  // Cache-dir-relative POSIX path of the link itself.
  string path = 1;

  // Raw link target, forward slashes on every platform. Always RELATIVE:
  // absolute or root-escaping targets are rejected at save time and
  // re-rejected at load time before any link is created.
  string target = 2;

  // Whether the target resolved to a directory at save time. Used on
  // Windows to pick symlink_dir vs symlink_file at restore.
  bool is_dir = 3;
}

message SourceFile {
  // Workspace-relative path, forward slashes on every platform.
  string path = 1;

  // mtime in milliseconds since UNIX epoch, matching what Node's
  // fs.Stats.mtimeMs / Rust's SystemTime exposes after rounding.
  int64 mtime_ms = 2;

  // File size in bytes. Cheap rejection signal on replay — if the
  // size differs, skip the hash check entirely.
  uint64 size = 3;

  // BLAKE3 hash of the file content. We ship the full 32 bytes so
  // future tooling can validate beyond the 128-bit prefix we use
  // for the replay decision. (Hash size adds <0.5 KB per 1000 files
  // after zstd — not worth truncating.)
  bytes blake3 = 4;
}

message CacheFile {
  // Cache-dir-relative path, forward slashes on every platform.
  string path = 1;

  // mtime in nanoseconds since UNIX epoch. Cache artifacts can be more
  // sensitive than source-file mtime replay, so this keeps the full
  // Rust/filetime precision instead of the source mtime_ms field.
  int64 mtime_ns = 2;

  // File size in bytes. Cheap rejection signal for delta comparison.
  uint64 size = 3;

  // BLAKE3 hash of the cache file content.
  bytes blake3 = 4;
}

enum CacheLayerKind {
  COMPLETE = 0;
  BASE = 1;
  DELTA = 2;
}
