const AR_MAGIC: &[u8; 8] = b"!<arch>\n";
const AR_HEADER_LEN: usize = 60;
const DOMAIN: &[u8] = b"kache.native-ar.gnu.member-content.v1\0";
pub fn portable_static_archive_hash(bytes: &[u8]) -> Option<String> {
if bytes.len() < AR_MAGIC.len() || &bytes[..AR_MAGIC.len()] != AR_MAGIC {
return None;
}
let mut hasher = blake3::Hasher::new();
hasher.update(DOMAIN);
let mut pos = AR_MAGIC.len();
let mut member_index: usize = 0;
let mut seen_symtab = false;
let mut seen_longnames = false;
let mut object_members: u64 = 0;
while pos < bytes.len() {
let header = bytes.get(pos..pos.checked_add(AR_HEADER_LEN)?)?;
if &header[58..60] != b"`\n" {
return None;
}
let name = ar_name(&header[0..16]);
let size = parse_ar_decimal(&header[48..58])?;
let data_start = pos + AR_HEADER_LEN;
let data_end = data_start.checked_add(size)?;
let data = bytes.get(data_start..data_end)?;
if name.starts_with("#1/")
|| name == "__.SYMDEF"
|| name == "__.SYMDEF SORTED"
|| name == "__.SYMDEF_64"
{
return None;
}
match classify(&name) {
Member::SymbolTable => {
if seen_symtab || member_index != 0 {
return None;
}
seen_symtab = true;
let tag: &[u8] = if name == "/SYM64/" {
b"symtab64\0"
} else {
b"symtab32\0"
};
hasher.update(tag);
hasher.update(&(data.len() as u64).to_le_bytes());
hasher.update(data);
}
Member::LongNameTable => {
let allowed = usize::from(seen_symtab);
if seen_longnames || member_index != allowed {
return None;
}
seen_longnames = true;
hasher.update(b"longnames\0");
hasher.update(&(data.len() as u64).to_le_bytes());
}
Member::Object => {
hasher.update(b"member\0");
hasher.update(&(data.len() as u64).to_le_bytes());
hasher.update(data);
object_members += 1;
}
}
pos = data_end.checked_add(size & 1)?;
member_index += 1;
}
if pos != bytes.len() {
return None;
}
if object_members == 0 {
return None;
}
Some(format!("gnu-ar-v1:{}", hasher.finalize().to_hex()))
}
enum Member {
SymbolTable,
LongNameTable,
Object,
}
fn classify(name: &str) -> Member {
match name {
"/" | "/SYM64/" => Member::SymbolTable,
"//" => Member::LongNameTable,
_ => Member::Object,
}
}
fn ar_name(field: &[u8]) -> String {
let end = field.iter().rposition(|&b| b != b' ').map_or(0, |i| i + 1);
String::from_utf8_lossy(&field[..end]).into_owned()
}
fn parse_ar_decimal(field: &[u8]) -> Option<usize> {
let s = std::str::from_utf8(field).ok()?.trim_end();
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse::<usize>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn member(name: &str, data: &[u8]) -> Vec<u8> {
assert!(name.len() <= 16);
let mut h = Vec::new();
h.extend_from_slice(format!("{name:<16}").as_bytes()); h.extend_from_slice(b"0 "); h.extend_from_slice(b"0 "); h.extend_from_slice(b"0 "); h.extend_from_slice(b"100644 "); h.extend_from_slice(format!("{:<10}", data.len()).as_bytes()); h.extend_from_slice(b"`\n"); assert_eq!(h.len(), AR_HEADER_LEN);
h.extend_from_slice(data);
if data.len() % 2 == 1 {
h.push(b'\n'); }
h
}
fn archive(members: &[(&str, &[u8])]) -> Vec<u8> {
let mut a = AR_MAGIC.to_vec();
for (n, d) in members {
a.extend_from_slice(&member(n, d));
}
a
}
const SYMTAB: &[u8] = b"\x00\x00\x00\x01\x00\x00\x00\x68foo\x00";
#[test]
fn identical_content_different_member_names_hash_equal() {
let obj1 = b"\x7fELF-object-one-contents";
let obj2 = b"\x7fELF-object-two-contents!"; let clone_a = archive(&[
("/", SYMTAB),
("//", b"cafca65b3467684e-a.o/\ncafca65b3467684e-b.o/\n"),
("/0", obj1),
("/22", obj2),
]);
let clone_b = archive(&[
("/", SYMTAB),
("//", b"4af22b2a007cb61a-a.o/\n4af22b2a007cb61a-b.o/\n"),
("/0", obj1),
("/22", obj2),
]);
let ha = portable_static_archive_hash(&clone_a).expect("clone-a parses");
let hb = portable_static_archive_hash(&clone_b).expect("clone-b parses");
assert_eq!(ha, hb, "path-derived member names must not affect the hash");
}
#[test]
fn changed_object_content_changes_hash() {
let a = archive(&[("/", SYMTAB), ("//", b"x.o/\n"), ("/0", b"object-vONE")]);
let b = archive(&[("/", SYMTAB), ("//", b"x.o/\n"), ("/0", b"object-vTWO")]);
assert_ne!(
portable_static_archive_hash(&a).unwrap(),
portable_static_archive_hash(&b).unwrap(),
);
}
#[test]
fn changed_symbol_table_changes_hash() {
let a = archive(&[("/", SYMTAB), ("/0", b"obj-data")]);
let b = archive(&[
("/", b"\x00\x00\x00\x01\x00\x00\x00\x99bar\x00"),
("/0", b"obj-data"),
]);
assert_ne!(
portable_static_archive_hash(&a).unwrap(),
portable_static_archive_hash(&b).unwrap(),
);
}
#[test]
fn member_order_changes_hash() {
let a = archive(&[("/0", b"aaaa"), ("/4", b"bbbb")]);
let b = archive(&[("/0", b"bbbb"), ("/4", b"aaaa")]);
assert_ne!(
portable_static_archive_hash(&a).unwrap(),
portable_static_archive_hash(&b).unwrap(),
);
}
#[test]
fn longname_table_content_is_ignored() {
let a = archive(&[("//", b"aaaaaaaa/\n"), ("/0", b"obj")]);
let b = archive(&[("//", b"bbbbbbbb/\n"), ("/0", b"obj")]);
assert_eq!(
portable_static_archive_hash(&a).unwrap(),
portable_static_archive_hash(&b).unwrap(),
);
}
#[test]
fn longname_table_length_change_changes_hash() {
let a = archive(&[("/", SYMTAB), ("//", b"aaaaaaaa/\n"), ("/0", b"obj")]);
let b = archive(&[
("/", SYMTAB),
("//", b"aaaaaaaaaaaaaaaa/\n"),
("/0", b"obj"),
]);
assert_ne!(
portable_static_archive_hash(&a).unwrap(),
portable_static_archive_hash(&b).unwrap(),
);
}
#[test]
fn output_is_domain_tagged() {
let a = archive(&[("/0", b"obj")]);
assert!(
portable_static_archive_hash(&a)
.unwrap()
.starts_with("gnu-ar-v1:"),
"portable digest must be textually distinct from the whole-file fallback"
);
}
#[test]
fn symbol_table_must_be_first() {
let a = archive(&[("/0", b"obj"), ("/", SYMTAB)]);
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn second_symbol_table_falls_back_coff_like() {
let a = archive(&[("/", SYMTAB), ("/", SYMTAB), ("/0", b"obj")]);
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn misplaced_longname_table_falls_back() {
let a = archive(&[("/0", b"obj"), ("//", b"x.o/\n")]);
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn darwin64_symdef_falls_back() {
let a = archive(&[("__.SYMDEF_64", b"symdef64data"), ("/0", b"obj")]);
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn sym64_and_sym32_with_identical_bytes_hash_differently() {
let map = b"\x00\x00\x00\x00\x00\x00\x00\x58foo\x00";
let a32 = archive(&[("/", map), ("foo.o/", b"OBJ\n")]);
let a64 = archive(&[("/SYM64/", map), ("foo.o/", b"OBJ\n")]);
assert_ne!(
portable_static_archive_hash(&a32).unwrap(),
portable_static_archive_hash(&a64).unwrap(),
);
}
#[test]
fn bsd_archive_falls_back() {
let mut a = AR_MAGIC.to_vec();
a.extend_from_slice(&member("#1/20", b"name________________objbytes"));
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn bsd_symdef_falls_back() {
let a = archive(&[("__.SYMDEF", b"symdefdata"), ("/0", b"obj")]);
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn thin_archive_falls_back() {
let mut a = b"!<thin>\n".to_vec();
a.extend_from_slice(&member("/0", b"obj"));
assert!(portable_static_archive_hash(&a).is_none());
}
#[test]
fn non_archive_falls_back() {
assert!(portable_static_archive_hash(b"not an archive at all").is_none());
assert!(portable_static_archive_hash(b"").is_none());
assert!(portable_static_archive_hash(b"!<arch>\n").is_none()); }
#[test]
fn malformed_falls_back() {
let mut bad = AR_MAGIC.to_vec();
let mut m = member("/0", b"obj");
m[58] = b'X';
bad.extend_from_slice(&m);
assert!(portable_static_archive_hash(&bad).is_none());
let mut trunc = AR_MAGIC.to_vec();
trunc.extend_from_slice(b"/0 0 0 0 100644 9999 `\n");
trunc.extend_from_slice(b"short");
assert!(portable_static_archive_hash(&trunc).is_none());
let mut nondec = AR_MAGIC.to_vec();
nondec.extend_from_slice(b"/0 0 0 0 100644 12x4 `\n");
nondec.extend_from_slice(b"data");
assert!(portable_static_archive_hash(&nondec).is_none());
}
}