#![cfg(unix)]
use std::io::Cursor;
use std::os::unix::fs::PermissionsExt;
use apple_bom::ParsedBom;
use apple_xar::reader::XarReader;
use embala_pkg::{XarEntry, XarEntryKind, write_bom, write_xar};
const HELLO: &[u8] = b"#!/bin/sh\necho hi\n";
const README: &[u8] = b"read me\n";
fn spike_entries() -> [XarEntry<'static>; 2] {
[
XarEntry {
name: "embala-hello",
kind: XarEntryKind::File {
data: HELLO,
mode: 0o755,
store: false,
},
},
XarEntry {
name: "README",
kind: XarEntryKind::File {
data: README,
mode: 0o644,
store: false,
},
},
]
}
#[test]
fn xar_round_trips_through_independent_reader() {
let mut archive = Vec::new();
write_xar(&spike_entries(), &mut archive).unwrap();
let mut reader = XarReader::new(Cursor::new(archive)).unwrap();
assert!(reader.verify_table_of_contents_checksum().unwrap());
let files = reader.files().unwrap();
let listed: Vec<(String, Option<u64>)> = files
.iter()
.map(|(name, file)| (name.clone(), file.data.as_ref().map(|d| d.size)))
.collect();
assert_eq!(
listed,
vec![
("embala-hello".to_string(), Some(HELLO.len() as u64)),
("README".to_string(), Some(README.len() as u64)),
]
);
let hello = reader.get_file_data_from_path("embala-hello").unwrap();
assert_eq!(hello.as_deref(), Some(HELLO));
let readme = reader.get_file_data_from_path("README").unwrap();
assert_eq!(readme.as_deref(), Some(README));
let modes: Vec<Option<String>> = files.iter().map(|(_, f)| f.mode.clone()).collect();
assert_eq!(
modes,
vec![Some("0755".to_string()), Some("0644".to_string())]
);
}
fn build_tree(root: &std::path::Path) {
let bin = root.join("bin");
let doc = root.join("share/doc/hello");
std::fs::create_dir_all(&bin).unwrap();
std::fs::create_dir_all(&doc).unwrap();
let hello = bin.join("embala-hello");
std::fs::write(&hello, HELLO).unwrap();
std::fs::set_permissions(&hello, std::fs::Permissions::from_mode(0o755)).unwrap();
let readme = doc.join("README");
std::fs::write(&readme, README).unwrap();
std::fs::set_permissions(&readme, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn bom_records_paths_modes_ownership_and_checksums() {
let dir = tempfile::tempdir().unwrap();
build_tree(dir.path());
let bom = write_bom(dir.path(), 0, 0).unwrap();
let parsed = ParsedBom::parse(&bom).unwrap();
let mut paths: Vec<(String, u16, u32, u32, usize, Option<u32>)> = parsed
.paths()
.unwrap()
.into_iter()
.map(|p| {
(
p.path().to_string(),
p.file_mode(),
p.user_id(),
p.group_id(),
p.size(),
p.crc32(),
)
})
.collect();
paths.sort();
let hello_crc = cksum(HELLO);
let readme_crc = cksum(README);
assert_eq!(
paths,
vec![
(".".to_string(), 0o40755, 0, 0, 0, None),
("./bin".to_string(), 0o40755, 0, 0, 0, None),
(
"./bin/embala-hello".to_string(),
0o100755,
0,
0,
HELLO.len(),
Some(hello_crc)
),
("./share".to_string(), 0o40755, 0, 0, 0, None),
("./share/doc".to_string(), 0o40755, 0, 0, 0, None),
("./share/doc/hello".to_string(), 0o40755, 0, 0, 0, None),
(
"./share/doc/hello/README".to_string(),
0o100644,
0,
0,
README.len(),
Some(readme_crc)
),
]
);
}
#[test]
fn bom_output_is_deterministic() {
let dir = tempfile::tempdir().unwrap();
build_tree(dir.path());
let first = write_bom(dir.path(), 0, 0).unwrap();
let second = write_bom(dir.path(), 0, 0).unwrap();
assert!(!first.is_empty());
assert_eq!(first, second);
}
fn cksum(data: &[u8]) -> u32 {
fn update(crc: u32, byte: u8) -> u32 {
let mut entry = ((((crc >> 24) as u8) ^ byte) as u32) << 24;
for _ in 0..8 {
entry = if entry & 0x8000_0000 != 0 {
(entry << 1) ^ 0x04c1_1db7
} else {
entry << 1
};
}
(crc << 8) ^ entry
}
let mut crc = 0u32;
for byte in data {
crc = update(crc, *byte);
}
let mut length = data.len();
while length != 0 {
crc = update(crc, (length & 0xff) as u8);
length >>= 8;
}
!crc
}