Files
alkblobs/tests/git_interop.rs
T
glm-5.3-flash fef05b4ec5 feat(keys): the content-address layer — canonical hash derivation and key encoding (ADR-002)
Wave 1 task core-hashing-keys: the crate's declared one-way door,
implemented byte-exact to ADR-002.

- src/hashing.rs: Algorithm (exactly GitBlobSha256 canonical /
  GitBlobSha1 tolerated; BLAKE3 absent), Digest (known-length
  derivation H("blob <len>\0" + content) as primary constructor;
  from_git_oid_hex interop constructor, lowercase-only acceptance),
  and the hashing::registry constants (0x01/0x02, 0x00 reserved; new
  algorithm = new ADR)
- src/keys.rs: typed Key wrapper; as_bytes/from_bytes are the only
  backend-boundary conversions; 1 algorithm byte + digest bytes
  (33/21 total); rejects-never-guesses validation -> Error::KeyInvalid
- tests/git_interop.rs: the load-bearing test — byte-exact derivation
  against the `git hash-object` CLI, both algorithms, small and
  75-KiB content; external-oid round-trip into pool keys; both oid
  formats coexisting in one flat pool
- named design point (oid hex policy): lowercase-only acceptance,
  recorded in the task Notes and on from_git_oid_hex

Verified: cargo test (15 passed), cargo clippy --all-targets -- -D
warnings, cargo fmt --check, cargo test --all-features — all green.
2026-10-11 18:20:12 +00:00

190 lines
6.4 KiB
Rust

//! The load-bearing interop test (ADR-002; POC #1 findings 4/5's
//! standard): byte-exact derivation against the `git hash-object` CLI
//! for both algorithms — validated against git, not just hardcoded
//! vectors — plus the external-oid round-trip into pool keys.
//!
//! Harness detail (POC finding 4, empirically learned): in current
//! git, `--object-format` is not a `hash-object` flag; the object
//! format is set on the repo (`git init --object-format=sha256`) and
//! the hash-object runs inside that repo. Repos are created fresh in
//! a per-call temp directory, with global/system config detached.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use alkblobs::{Algorithm, Digest, Key};
/// Content cases: empty, short text, binary-with-NULs, and a
/// multi-KiB case (75 KiB, deterministic).
fn content_cases() -> Vec<(&'static str, Vec<u8>)> {
vec![
("empty", Vec::new()),
("short", b"hello".to_vec()),
(
"binary-nuls",
vec![0u8, 1, 255, 0, 10, 13, 0, 7, 255, 255, 0, 0],
),
(
"multi-kib",
(0..75 * 1024).map(|i| (i % 251) as u8).collect::<Vec<u8>>(),
),
]
}
fn scratch_dir(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"alkblobs-git-interop-{}-{}-{}",
tag,
std::process::id(),
nanos
));
fs::create_dir_all(&dir).expect("temp dir creation");
dir
}
fn detached_config(command: &mut Command) {
let devnull = if cfg!(windows) { "NUL" } else { "/dev/null" };
command
.env("GIT_CONFIG_GLOBAL", devnull)
.env("GIT_CONFIG_SYSTEM", devnull);
}
/// Initialize a fresh repo of the requested object format and return
/// the oid hex `git hash-object --stdin` prints for `content`.
fn git_hash_object(content: &[u8], sha256: bool) -> (PathBuf, String) {
let dir = scratch_dir("repo");
let mut init = Command::new("git");
detached_config(&mut init);
init.arg("init").arg("-q").current_dir(&dir);
if sha256 {
init.arg("--object-format=sha256");
}
let out = init.output().expect("git init spawn");
assert!(
out.status.success(),
"git init failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut hash = Command::new("git");
detached_config(&mut hash);
hash.arg("-C").arg(&dir).arg("hash-object").arg("--stdin");
use std::io::Write as _;
let mut child = hash
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("git hash-object spawn");
child
.stdin
.as_mut()
.expect("stdin piped")
.write_all(content)
.expect("stdin write");
let out = child.wait_with_output().expect("git hash-object output");
assert!(
out.status.success(),
"git hash-object failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let oid = String::from_utf8(out.stdout)
.expect("git output utf8")
.trim()
.to_owned();
(dir, oid)
}
fn drop_dir(dir: &Path) {
let _ = fs::remove_dir_all(dir);
}
/// The load-bearing test: `Digest`'s derivation matches the git CLI
/// byte-exact, both algorithms, small and multi-KiB content.
#[test]
fn derivation_is_byte_exact_against_git_hash_object() {
for (tag, content) in content_cases() {
let (dir, oid) = git_hash_object(&content, false);
drop_dir(&dir);
assert_eq!(oid.len(), 40, "git printed a sha1 oid");
assert_eq!(
Digest::from_git_oid_hex(&oid),
Ok(Algorithm::GitBlobSha1.derive(&content, content.len() as u64)),
"sha1 {tag}: derived digest != git hash-object",
);
let (dir, oid) = git_hash_object(&content, true);
drop_dir(&dir);
assert_eq!(oid.len(), 64, "git printed a sha256 oid");
assert_eq!(
Digest::from_git_oid_hex(&oid),
Ok(Digest::derive(&content, content.len() as u64)),
"sha256 {tag}: derived digest != git hash-object",
);
}
}
/// The external-oid round-trip (POC finding 4 corollary, key level):
/// an oid produced by external git (`from_git_oid_hex` →
/// `as_bytes` → `from_bytes`) becomes exactly the pool key the store
/// core would derive — no mapping layer.
#[test]
fn external_oid_round_trips_into_pool_keys() {
for sha256 in [false, true] {
let content = b"external producer blob \0 bytes";
let (dir, oid) = git_hash_object(content, sha256);
drop_dir(&dir);
let external: Key = Digest::from_git_oid_hex(&oid)
.expect("external oid decodes")
.into();
let derived: Key = {
let len = content.len() as u64;
Digest::derive(content, len).into()
};
if sha256 {
assert_eq!(external, derived, "sha256: external oid == derived key");
} else {
assert_eq!(
external,
Key::from(Algorithm::GitBlobSha1.derive(content, content.len() as u64)),
"sha1: external oid == derived key"
);
}
let decoded = Key::from_bytes(&external.as_bytes()).expect("physical encoding decodes");
assert_eq!(decoded, external);
assert_eq!(decoded.as_bytes(), external.as_bytes());
}
}
/// Tagged-key coexistence (POC finding 5): git oids of both formats
/// address the same flat key universe from one external producer.
#[test]
fn both_oid_formats_coexist_in_one_pool() {
let content = b"same content, two formats";
let (dir1, oid1) = git_hash_object(content, false);
drop_dir(&dir1);
let (dir2, oid2) = git_hash_object(content, true);
drop_dir(&dir2);
let mut pool = std::collections::BTreeSet::new();
pool.insert(
Key::from_bytes(
&Key::from(Digest::from_git_oid_hex(&oid1).expect("sha1 oid decodes")).as_bytes(),
)
.expect("sha1 external key decodes"),
);
pool.insert(
Key::from_bytes(
&Key::from(Digest::from_git_oid_hex(&oid2).expect("sha256 oid decodes")).as_bytes(),
)
.expect("sha256 external key decodes"),
);
assert_eq!(pool.len(), 2, "both algorithms coexist in one flat pool");
}