use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqry_core::graph::unified::file::id::FileId;
use sqry_core::persistence::{PathSafetyError, atomic_write_bytes, validate_path_in_workspace};
pub const DERIVED_MAGIC: [u8; 16] = *b"SQRY_DERIVED_V02";
pub const DERIVED_FORMAT_VERSION: u16 = 2;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct QueryDeps {
pub file_deps: Vec<(FileId, u64)>,
pub edge_revision: Option<u64>,
pub metadata_revision: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DerivedHeader {
pub magic: [u8; 16],
pub format_version: u16,
pub snapshot_sha256: [u8; 32],
pub edge_revision: u64,
pub metadata_revision: u64,
pub file_revisions: Vec<(FileId, u64)>,
pub entry_count: u64,
pub saved_at: u64,
}
impl DerivedHeader {
#[must_use]
pub fn new(
snapshot_sha256: [u8; 32],
edge_revision: u64,
metadata_revision: u64,
file_revisions: Vec<(FileId, u64)>,
entry_count: u64,
) -> Self {
let saved_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
magic: DERIVED_MAGIC,
format_version: DERIVED_FORMAT_VERSION,
snapshot_sha256,
edge_revision,
metadata_revision,
file_revisions,
entry_count,
saved_at,
}
}
#[must_use]
pub fn is_valid_v02(&self) -> bool {
self.magic == DERIVED_MAGIC && self.format_version == DERIVED_FORMAT_VERSION
}
#[must_use]
pub fn matches_snapshot(&self, snapshot_sha256: &[u8; 32]) -> bool {
self.snapshot_sha256 == *snapshot_sha256
}
}
pub type DerivedManifest = DerivedHeader;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedEntry {
pub query_type_id: u32,
pub raw_key_bytes: Vec<u8>,
pub raw_result_bytes: Vec<u8>,
pub deps: QueryDeps,
}
pub fn serialize_derived_stream<I>(
header: &DerivedHeader,
entries: I,
) -> Result<Vec<u8>, postcard::Error>
where
I: IntoIterator<Item = PersistedEntry>,
{
let mut buf = postcard::to_allocvec(header)?;
for entry in entries {
let entry_bytes = postcard::to_allocvec(&entry)?;
buf.extend_from_slice(&entry_bytes);
}
Ok(buf)
}
pub fn deserialize_derived_header(bytes: &[u8]) -> Result<(DerivedHeader, &[u8]), postcard::Error> {
postcard::take_from_bytes(bytes)
}
pub fn deserialize_next_entry(bytes: &[u8]) -> Result<(PersistedEntry, &[u8]), postcard::Error> {
postcard::take_from_bytes(bytes)
}
pub fn compute_file_sha256(path: &Path) -> std::io::Result<[u8; 32]> {
let data = std::fs::read(path)?;
let mut hasher = Sha256::new();
hasher.update(&data);
let result = hasher.finalize();
let mut hash = [0u8; 32];
hash.copy_from_slice(&result);
Ok(hash)
}
#[must_use]
pub fn derived_path_for_snapshot(snapshot_path: &Path, filename: &str) -> PathBuf {
snapshot_path
.parent()
.unwrap_or(Path::new("."))
.join(filename)
}
pub fn save_manifest(path: &Path, manifest: &DerivedHeader) -> anyhow::Result<()> {
let bytes = postcard::to_allocvec(manifest)?;
std::fs::write(path, bytes)?;
Ok(())
}
#[must_use]
pub fn load_manifest(path: &Path) -> Option<DerivedHeader> {
let bytes = std::fs::read(path).ok()?;
postcard::from_bytes(&bytes).ok()
}
pub fn save_derived(
db: &crate::QueryDb,
snapshot_sha256: [u8; 32],
path: &Path,
workspace_root: &Path,
) -> anyhow::Result<()> {
let canonical_path = validate_path_in_workspace(path, workspace_root)?;
let persistent: Vec<PersistedEntry> = db
.iter_persistent_cache_entries()
.map(|e| PersistedEntry {
query_type_id: e.query_type_id,
raw_key_bytes: e.raw_key_bytes.to_vec(),
raw_result_bytes: e.raw_result_bytes.to_vec(),
deps: e.deps,
})
.collect();
let header = DerivedHeader::new(
snapshot_sha256,
db.edge_revision(),
db.metadata_revision(),
db.inputs().all_revisions(),
persistent.len() as u64,
);
let bytes = serialize_derived_stream(&header, persistent)?;
atomic_write_bytes(&canonical_path, &bytes)?;
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("derived-cache file not found: {path}")]
NotFound {
path: PathBuf,
},
#[error("derived-cache snapshot SHA mismatch — file discarded")]
StaleSnapshot,
#[error("derived-cache file is corrupt: {detail}")]
Corrupt {
detail: String,
},
#[error("derived-cache path validation failed: {0}")]
PathSafety(#[from] PathSafetyError),
#[error("derived-cache IO error: {0}")]
Io(#[from] std::io::Error),
#[error("derived-cache load already applied to this DB; subsequent calls are no-ops")]
AlreadyLoaded,
}
#[derive(Debug, Clone)]
pub enum LoadOutcome {
Applied {
entries: usize,
},
Skipped(SkipReason),
}
#[derive(Debug, Clone)]
pub enum SkipReason {
}
pub struct StagedEntry {
pub query_type_id: u32,
pub raw_key_bytes: Vec<u8>,
pub raw_result_bytes: Vec<u8>,
pub deps: QueryDeps,
}
#[inline]
fn is_known_builtin(id: u32) -> bool {
use crate::queries::type_ids;
matches!(
id,
type_ids::CALLERS
| type_ids::CALLEES
| type_ids::IMPORTS
| type_ids::EXPORTS
| type_ids::REFERENCES
| type_ids::IMPLEMENTS
| type_ids::CYCLES
| type_ids::IS_IN_CYCLE
| type_ids::UNUSED
| type_ids::IS_NODE_UNUSED
| type_ids::REACHABILITY
| type_ids::ENTRY_POINTS
| type_ids::REACHABLE_FROM_ENTRY_POINTS
| type_ids::SCC
| type_ids::CONDENSATION
)
}
pub fn load_derived(
db: &mut crate::QueryDb,
snapshot_sha256: [u8; 32],
path: &Path,
workspace_root: &Path,
) -> Result<LoadOutcome, LoadError> {
let canonical_path = validate_path_in_workspace(path, workspace_root)?;
if !db.cold_load_allowed.load(Ordering::Acquire) {
return Err(LoadError::AlreadyLoaded);
}
let bytes = match std::fs::read(&canonical_path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(LoadError::NotFound {
path: canonical_path.clone(),
});
}
Err(e) => return Err(LoadError::Io(e)),
};
let (header, mut tail) =
deserialize_derived_header(&bytes).map_err(|e| LoadError::Corrupt {
detail: format!("header decode: {e}"),
})?;
if header.magic != DERIVED_MAGIC {
return Err(LoadError::Corrupt {
detail: "magic mismatch".to_owned(),
});
}
if header.format_version != DERIVED_FORMAT_VERSION {
return Err(LoadError::Corrupt {
detail: format!(
"version mismatch: expected {DERIVED_FORMAT_VERSION}, got {}",
header.format_version
),
});
}
if header.snapshot_sha256 != snapshot_sha256 {
return Err(LoadError::StaleSnapshot);
}
let mut staged: Vec<StagedEntry> = Vec::new();
while !tail.is_empty() {
let (entry, rest) = deserialize_next_entry(tail).map_err(|e| LoadError::Corrupt {
detail: format!("entry decode: {e}"),
})?;
tail = rest;
if !is_known_builtin(entry.query_type_id) {
continue;
}
staged.push(StagedEntry {
query_type_id: entry.query_type_id,
raw_key_bytes: entry.raw_key_bytes,
raw_result_bytes: entry.raw_result_bytes,
deps: entry.deps,
});
}
let entries_applied = staged.len();
db.commit_staged_load(header, staged);
db.cold_load_allowed.store(false, Ordering::Release);
Ok(LoadOutcome::Applied {
entries: entries_applied,
})
}
#[cfg(test)]
mod load_path_tests {
use std::sync::Arc;
use sqry_core::graph::unified::concurrent::CodeGraph;
use tempfile::TempDir;
use super::*;
use crate::queries::type_ids;
use crate::{QueryDb, QueryDbConfig};
fn empty_db() -> QueryDb {
let snapshot = Arc::new(CodeGraph::new().snapshot());
QueryDb::new(snapshot, QueryDbConfig::default())
}
fn make_valid_stream(sha: [u8; 32], n_entries: usize) -> Vec<u8> {
let entries: Vec<PersistedEntry> = (0..n_entries)
.map(|i| PersistedEntry {
query_type_id: type_ids::CALLERS,
raw_key_bytes: vec![i as u8],
raw_result_bytes: vec![0xAA, i as u8],
deps: QueryDeps::default(),
})
.collect();
let header = DerivedHeader::new(sha, 5, 3, vec![], entries.len() as u64);
serialize_derived_stream(&header, entries).unwrap()
}
#[test]
fn happy_path_roundtrip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x42; 32];
let bytes = make_valid_stream(sha, 3);
std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
let outcome = load_derived(&mut db, sha, &path, workspace_root).unwrap();
match outcome {
LoadOutcome::Applied { entries } => {
assert_eq!(entries, 3, "expected 3 entries applied");
}
LoadOutcome::Skipped(_) => panic!("unexpected Skipped outcome"),
}
}
#[test]
fn missing_file_returns_not_found() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nonexistent.sqry");
let workspace_root = dir.path();
let mut db = empty_db();
let err = load_derived(&mut db, [0u8; 32], &path, workspace_root)
.expect_err("missing file must return Err");
assert!(
matches!(err, LoadError::NotFound { .. }),
"expected NotFound, got: {err}"
);
}
#[test]
fn sha_mismatch_returns_stale_snapshot() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let saved_sha: [u8; 32] = [0x11; 32];
let caller_sha: [u8; 32] = [0x22; 32];
let bytes = make_valid_stream(saved_sha, 0);
std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
let err = load_derived(&mut db, caller_sha, &path, workspace_root)
.expect_err("SHA mismatch must return Err");
assert!(
matches!(err, LoadError::StaleSnapshot),
"expected StaleSnapshot, got: {err}"
);
}
#[test]
fn magic_mismatch_returns_corrupt() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x33; 32];
let mut bytes = make_valid_stream(sha, 0);
bytes[0] ^= 0xFF; std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
let err = load_derived(&mut db, sha, &path, workspace_root)
.expect_err("magic mismatch must return Err");
assert!(
matches!(err, LoadError::Corrupt { .. }),
"expected Corrupt, got: {err}"
);
}
#[test]
fn truncated_file_returns_corrupt_and_db_unchanged() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x44; 32];
let full_bytes = make_valid_stream(sha, 2);
let (_header, tail) = deserialize_derived_header(&full_bytes).unwrap();
let header_len = full_bytes.len() - tail.len();
let partial_entry_start = header_len;
let truncated_len = partial_entry_start + 3;
let truncated_bytes = &full_bytes[..truncated_len];
std::fs::write(&path, truncated_bytes).unwrap();
let mut db = empty_db();
let initial_edge_rev = db.edge_revision();
let err = load_derived(&mut db, sha, &path, workspace_root)
.expect_err("truncated file must return Err");
assert!(
matches!(err, LoadError::Corrupt { .. }),
"expected Corrupt, got: {err}"
);
assert_eq!(
db.edge_revision(),
initial_edge_rev,
"DB edge_revision must be unchanged after failed load"
);
assert!(
db.cold_load_allowed(),
"cold_load_allowed must remain true after failed load"
);
}
#[test]
fn unknown_query_type_id_skipped_silently() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x55; 32];
const UNKNOWN_ID: u32 = 0xBEEF;
let entries = vec![
PersistedEntry {
query_type_id: type_ids::CALLERS,
raw_key_bytes: vec![1],
raw_result_bytes: vec![0xA1],
deps: QueryDeps::default(),
},
PersistedEntry {
query_type_id: type_ids::CALLERS,
raw_key_bytes: vec![2],
raw_result_bytes: vec![0xA2],
deps: QueryDeps::default(),
},
PersistedEntry {
query_type_id: UNKNOWN_ID,
raw_key_bytes: vec![3],
raw_result_bytes: vec![0xA3],
deps: QueryDeps::default(),
},
PersistedEntry {
query_type_id: type_ids::CALLEES,
raw_key_bytes: vec![4],
raw_result_bytes: vec![0xA4],
deps: QueryDeps::default(),
},
PersistedEntry {
query_type_id: type_ids::CALLEES,
raw_key_bytes: vec![5],
raw_result_bytes: vec![0xA5],
deps: QueryDeps::default(),
},
];
let header = DerivedHeader::new(sha, 0, 0, vec![], entries.len() as u64);
let bytes = serialize_derived_stream(&header, entries).unwrap();
std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
let outcome = load_derived(&mut db, sha, &path, workspace_root).unwrap();
match outcome {
LoadOutcome::Applied { entries } => {
assert_eq!(
entries, 4,
"unknown entry must be silently skipped; expected 4 applied"
);
}
LoadOutcome::Skipped(_) => panic!("unexpected Skipped outcome"),
}
}
#[test]
fn second_load_returns_already_loaded() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x66; 32];
let bytes = make_valid_stream(sha, 1);
std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
load_derived(&mut db, sha, &path, workspace_root).unwrap();
std::fs::remove_file(&path).unwrap();
let err = load_derived(&mut db, sha, &path, workspace_root)
.expect_err("second load must return Err");
assert!(
matches!(err, LoadError::AlreadyLoaded),
"expected AlreadyLoaded, got: {err}"
);
}
#[test]
fn header_restoration_restores_three_tiers() {
use sqry_core::graph::unified::file::id::FileId;
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let sha: [u8; 32] = [0x77; 32];
let file_revisions = vec![(FileId::new(1), 7u64), (FileId::new(2), 99u64)];
let header = DerivedHeader::new(
sha,
42,
17,
file_revisions.clone(),
0,
);
let bytes = serialize_derived_stream(&header, std::iter::empty()).unwrap();
std::fs::write(&path, &bytes).unwrap();
let mut db = empty_db();
let outcome = load_derived(&mut db, sha, &path, workspace_root).unwrap();
assert!(
matches!(outcome, LoadOutcome::Applied { entries: 0 }),
"expected Applied(0), got: {outcome:?}"
);
assert_eq!(db.edge_revision(), 42, "edge_revision must be restored");
assert_eq!(
db.metadata_revision(),
17,
"metadata_revision must be restored"
);
assert_eq!(
db.inputs().revision(FileId::new(1)),
Some(7),
"file 1 revision must be restored"
);
assert_eq!(
db.inputs().revision(FileId::new(2)),
Some(99),
"file 2 revision must be restored"
);
}
}
#[cfg(test)]
mod save_path_tests {
use std::sync::Arc;
use sqry_core::graph::unified::concurrent::CodeGraph;
use tempfile::TempDir;
use super::*;
use crate::{QueryDb, QueryDbConfig};
fn empty_db() -> QueryDb {
let snapshot = Arc::new(CodeGraph::new().snapshot());
QueryDb::new(snapshot, QueryDbConfig::default())
}
#[test]
fn save_then_read_back_header_fields_match() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let db = empty_db();
let snapshot_sha: [u8; 32] = [0xAB; 32];
save_derived(&db, snapshot_sha, &path, workspace_root).unwrap();
let bytes = std::fs::read(&path).unwrap();
let (header, tail) = deserialize_derived_header(&bytes).unwrap();
assert_eq!(
header.snapshot_sha256, snapshot_sha,
"snapshot SHA mismatch"
);
assert_eq!(
header.edge_revision,
db.edge_revision(),
"edge_revision mismatch"
);
assert_eq!(
header.metadata_revision,
db.metadata_revision(),
"metadata_revision mismatch"
);
assert_eq!(header.entry_count, 0, "expected 0 entries for empty db");
assert!(header.is_valid_v02(), "header must pass v02 validation");
assert!(tail.is_empty(), "no entry bytes expected after header");
}
#[test]
#[cfg(unix)]
fn save_rejects_symlinked_target_path() {
use std::os::unix::fs::symlink;
let dir = TempDir::new().unwrap();
let real_file = dir.path().join("real.sqry");
std::fs::write(&real_file, b"placeholder").unwrap();
let symlink_path = dir.path().join("link.sqry");
symlink(&real_file, &symlink_path).unwrap();
let db = empty_db();
let workspace_root = dir.path();
let snapshot_sha: [u8; 32] = [0u8; 32];
let err = save_derived(&db, snapshot_sha, &symlink_path, workspace_root)
.expect_err("save must reject symlinked target");
let is_symlink_error = err
.chain()
.any(|e| e.to_string().contains("symlink") || e.to_string().contains("SymlinkTarget"));
assert!(
is_symlink_error,
"expected SymlinkTarget error; got: {err:#}"
);
}
#[test]
fn save_empty_cache_writes_header_only() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let db = empty_db();
let snapshot_sha: [u8; 32] = [0xCC; 32];
save_derived(&db, snapshot_sha, &path, workspace_root).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert!(
!bytes.is_empty(),
"output must be non-empty even for 0 entries"
);
let (header, tail) = deserialize_derived_header(&bytes).unwrap();
assert!(header.is_valid_v02());
assert_eq!(header.entry_count, 0);
assert!(
tail.is_empty(),
"empty cache must produce no entry bytes after the header"
);
}
#[test]
fn save_is_idempotent_header_fields_stable_across_repeat_calls() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("derived.sqry");
let workspace_root = dir.path();
let db = empty_db();
let snapshot_sha: [u8; 32] = [0x55; 32];
save_derived(&db, snapshot_sha, &path, workspace_root).unwrap();
let first_bytes = std::fs::read(&path).unwrap();
std::fs::remove_file(&path).unwrap();
save_derived(&db, snapshot_sha, &path, workspace_root).unwrap();
let second_bytes = std::fs::read(&path).unwrap();
let (h1, tail1) = deserialize_derived_header(&first_bytes).unwrap();
let (h2, tail2) = deserialize_derived_header(&second_bytes).unwrap();
assert_eq!(h1.snapshot_sha256, h2.snapshot_sha256);
assert_eq!(h1.edge_revision, h2.edge_revision);
assert_eq!(h1.metadata_revision, h2.metadata_revision);
assert_eq!(h1.entry_count, h2.entry_count);
assert_eq!(h1.file_revisions, h2.file_revisions);
assert!(tail1.is_empty());
assert!(tail2.is_empty());
let _ = (first_bytes, second_bytes); }
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn magic_is_16_bytes_exactly() {
assert_eq!(DERIVED_MAGIC.len(), 16);
assert_eq!(&DERIVED_MAGIC, b"SQRY_DERIVED_V02");
}
#[test]
fn format_version_is_two() {
assert_eq!(DERIVED_FORMAT_VERSION, 2);
}
#[test]
fn header_round_trip() {
let h = DerivedHeader {
magic: DERIVED_MAGIC,
format_version: DERIVED_FORMAT_VERSION,
snapshot_sha256: [0xAB; 32],
edge_revision: 7,
metadata_revision: 3,
file_revisions: vec![(FileId::new(1), 42), (FileId::new(2), 99)],
entry_count: 42,
saved_at: 1_700_000_000,
};
let bytes = postcard::to_allocvec(&h).unwrap();
let decoded: DerivedHeader = postcard::from_bytes(&bytes).unwrap();
assert_eq!(decoded, h);
}
#[test]
fn header_is_valid_v02() {
let h = DerivedHeader::new([0u8; 32], 1, 2, vec![], 0);
assert!(h.is_valid_v02());
}
#[test]
fn header_with_wrong_magic_is_not_valid_v02() {
let mut h = DerivedHeader::new([0u8; 32], 1, 2, vec![], 0);
h.magic[0] = b'X'; assert!(!h.is_valid_v02());
}
#[test]
fn header_with_wrong_format_version_is_not_valid_v02() {
let mut h = DerivedHeader::new([0u8; 32], 1, 2, vec![], 0);
h.format_version = 1;
assert!(!h.is_valid_v02());
}
#[test]
fn stream_round_trip() {
let header = DerivedHeader {
magic: DERIVED_MAGIC,
format_version: DERIVED_FORMAT_VERSION,
snapshot_sha256: [0x55; 32],
edge_revision: 10,
metadata_revision: 5,
file_revisions: vec![(FileId::new(3), 7)],
entry_count: 2,
saved_at: 1_700_000_001,
};
let entries = vec![
PersistedEntry {
query_type_id: 0x0001,
raw_key_bytes: vec![1, 2, 3],
raw_result_bytes: vec![4, 5, 6],
deps: QueryDeps {
file_deps: vec![(FileId::new(1), 1)],
edge_revision: Some(10),
metadata_revision: None,
},
},
PersistedEntry {
query_type_id: 0x0002,
raw_key_bytes: vec![7],
raw_result_bytes: vec![8],
deps: QueryDeps {
file_deps: vec![],
edge_revision: None,
metadata_revision: Some(5),
},
},
];
let bytes = serialize_derived_stream(&header, entries.clone()).unwrap();
let (decoded_header, mut tail) = deserialize_derived_header(&bytes).unwrap();
assert_eq!(decoded_header, header);
let mut decoded_entries = Vec::new();
while !tail.is_empty() {
let (entry, rest) = deserialize_next_entry(tail).unwrap();
decoded_entries.push(entry);
tail = rest;
}
assert_eq!(decoded_entries.len(), 2);
assert_eq!(decoded_entries[0].query_type_id, entries[0].query_type_id);
assert_eq!(decoded_entries[0].raw_key_bytes, entries[0].raw_key_bytes);
assert_eq!(
decoded_entries[0].raw_result_bytes,
entries[0].raw_result_bytes
);
assert_eq!(decoded_entries[0].deps, entries[0].deps);
assert_eq!(decoded_entries[1].query_type_id, entries[1].query_type_id);
assert_eq!(decoded_entries[1].deps, entries[1].deps);
}
#[test]
fn stream_with_zero_entries() {
let header = DerivedHeader::new([0xCC; 32], 0, 0, vec![], 0);
let bytes = serialize_derived_stream(&header, std::iter::empty()).unwrap();
let (decoded_header, tail) = deserialize_derived_header(&bytes).unwrap();
assert_eq!(decoded_header, header);
assert!(tail.is_empty(), "no entries means empty tail");
}
#[test]
fn legacy_v01_magic_is_not_v02_magic() {
let hypothetical_v01_first_16 = [0u8; 16]; assert_ne!(
&DERIVED_MAGIC[..],
&hypothetical_v01_first_16[..],
"DERIVED_MAGIC must not equal any plausible v01 SHA-256 prefix"
);
let sha_like_prefix: [u8; 16] = [
0x6b, 0x86, 0xb2, 0x73, 0xff, 0x34, 0xfc, 0xe1, 0x9d, 0x6b, 0x80, 0x4e, 0xff, 0x5a,
0x3f, 0x57,
];
assert_ne!(&DERIVED_MAGIC[..], &sha_like_prefix[..]);
let magic_as_ascii = std::str::from_utf8(&DERIVED_MAGIC).expect("DERIVED_MAGIC is ASCII");
assert_eq!(magic_as_ascii, "SQRY_DERIVED_V02");
}
#[test]
fn legacy_v01_bytes_decode_as_invalid_header() {
#[derive(Serialize)]
struct OldManifest {
snapshot_sha256: [u8; 32],
entry_count: usize,
saved_at: u64,
}
let old = OldManifest {
snapshot_sha256: [0xDE; 32],
entry_count: 5,
saved_at: 1_700_000_000,
};
let v01_bytes = postcard::to_allocvec(&old).unwrap();
match postcard::from_bytes::<DerivedHeader>(&v01_bytes) {
Ok(decoded) => {
assert!(
!decoded.is_valid_v02(),
"v01 bytes accidentally decoded as valid v02 header — \
LOAD_PATH rejection would be bypassed"
);
}
Err(_) => {
}
}
}
#[test]
fn manifest_round_trip() {
let hash = [42u8; 32];
let header = DerivedHeader::new(hash, 0, 0, vec![], 100);
assert!(header.matches_snapshot(&hash));
assert!(!header.matches_snapshot(&[0u8; 32]));
assert!(header.is_valid_v02());
let temp = NamedTempFile::new().unwrap();
save_manifest(temp.path(), &header).unwrap();
let loaded = load_manifest(temp.path()).unwrap();
assert_eq!(loaded.snapshot_sha256, hash);
assert_eq!(loaded.entry_count, 100);
assert!(loaded.matches_snapshot(&hash));
assert!(loaded.is_valid_v02());
}
#[test]
fn derived_path_computation() {
let snapshot = Path::new("/home/user/.sqry/graph/snapshot.sqry");
let derived = derived_path_for_snapshot(snapshot, "derived.sqry");
assert_eq!(
derived,
PathBuf::from("/home/user/.sqry/graph/derived.sqry")
);
}
#[test]
fn load_manifest_missing_file() {
let result = load_manifest(Path::new("/nonexistent/path/derived.sqry"));
assert!(result.is_none());
}
#[test]
fn file_sha256() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), b"hello world").unwrap();
let hash = compute_file_sha256(temp.path()).unwrap();
assert_eq!(hash.len(), 32);
assert_ne!(hash, [0u8; 32]); }
#[test]
fn query_deps_default_is_empty() {
let deps = QueryDeps::default();
assert!(deps.file_deps.is_empty());
assert!(deps.edge_revision.is_none());
assert!(deps.metadata_revision.is_none());
}
#[test]
fn query_deps_round_trip() {
let deps = QueryDeps {
file_deps: vec![(FileId::new(1), 7), (FileId::new(2), 3)],
edge_revision: Some(99),
metadata_revision: Some(4),
};
let bytes = postcard::to_allocvec(&deps).unwrap();
let decoded: QueryDeps = postcard::from_bytes(&bytes).unwrap();
assert_eq!(decoded, deps);
}
}