use crate::git::ChangeKind;
use crate::path::RelPath;
pub const META_SCHEMA_VER: &[u8] = b"schema_ver";
pub const META_LAST_HEAD: &[u8] = b"last_indexed_head"; pub const META_NEXT_ORD: &[u8] = b"next_commit_ord"; pub const META_NEXT_PATH_ID: &[u8] = b"next_path_id"; pub const META_ROOT_SHA: &[u8] = b"root_sha"; pub const META_COMMIT_COUNT: &[u8] = b"commit_count";
fn write_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) -> Option<()> {
let len = u16::try_from(bytes.len()).ok()?;
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(bytes);
Some(())
}
pub fn u32_key(value: u32) -> [u8; 4] {
value.to_be_bytes()
}
pub fn parse_u32(bytes: &[u8]) -> Option<u32> {
let arr: [u8; 4] = bytes.try_into().ok()?;
Some(u32::from_be_bytes(arr))
}
pub fn path_id_by_path_key(rel: &RelPath) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(2 + rel.as_bytes().len());
write_len_prefixed(&mut out, rel.as_bytes())?;
Some(out)
}
pub fn sha_hex_to_raw(hex40: &str) -> Option<[u8; 20]> {
let mut out = [0u8; 20];
hex::decode_to_slice(hex40, &mut out).ok()?;
Some(out)
}
pub fn sha_raw_to_hex(sha20: &[u8; 20]) -> String {
hex::encode(sha20)
}
pub fn change_kind_byte(kind: ChangeKind) -> u8 {
match kind {
ChangeKind::Added => 0,
ChangeKind::Modified => 1,
ChangeKind::Deleted => 2,
ChangeKind::Renamed => 3,
}
}
pub fn change_kind_from_byte(byte: u8) -> ChangeKind {
match byte {
1 => ChangeKind::Modified,
2 => ChangeKind::Deleted,
3 => ChangeKind::Renamed,
_ => ChangeKind::Added,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn u32_key_round_trips() {
for value in [0u32, 1, 255, 256, 65_535, 1_000_000, u32::MAX] {
assert_eq!(parse_u32(&u32_key(value)), Some(value));
}
assert_eq!(parse_u32(&[1, 2, 3]), None, "wrong length rejected");
}
#[test]
fn sha_hex_raw_round_trips() {
let hex = "0a2cad8d74da1107738833adc23ce104835d96cc";
let raw = sha_hex_to_raw(hex).expect("valid sha");
assert_eq!(sha_raw_to_hex(&raw), hex);
assert_eq!(sha_hex_to_raw("not-hex"), None);
assert_eq!(sha_hex_to_raw("abcd"), None, "wrong length rejected");
}
#[test]
fn change_kind_byte_round_trips_all_variants() {
for kind in [
ChangeKind::Added,
ChangeKind::Modified,
ChangeKind::Deleted,
ChangeKind::Renamed,
] {
assert_eq!(change_kind_from_byte(change_kind_byte(kind)), kind);
}
}
#[test]
fn path_id_key_prefix_isolation() {
let foo = path_id_by_path_key(&RelPath::from("Foo".as_bytes())).unwrap();
let foobar = path_id_by_path_key(&RelPath::from("Foobar".as_bytes())).unwrap();
assert_ne!(foo, foobar);
assert!(
!foobar.starts_with(&foo),
"length-prefix prevents prefix spill"
);
}
#[test]
fn oversized_path_is_rejected() {
let huge = RelPath::from(vec![b'a'; 70_000].as_slice());
assert!(
path_id_by_path_key(&huge).is_none(),
"past u16 ceiling → None"
);
}
}