use crate::fs::MountTable;
const BLOCK: usize = 512;
fn field_str(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
fn field_octal(bytes: &[u8]) -> u64 {
let mut v = 0u64;
for &b in bytes {
if (b'0'..=b'7').contains(&b) {
v = v * 8 + u64::from(b - b'0');
} else if b == 0 || b == b' ' {
if v != 0 {
break;
}
} else {
break;
}
}
v
}
fn norm_path(name: &str) -> Option<String> {
let n = name.strip_prefix("./").unwrap_or(name);
let n = n.strip_suffix('/').unwrap_or(n);
if n.is_empty() || n == "." {
return None;
}
Some(format!("/{n}"))
}
fn ensure_parents(mounts: &mut MountTable, path: &str) {
let mut acc = String::new();
let comps: Vec<&str> = path.trim_start_matches('/').split('/').collect();
for comp in &comps[..comps.len().saturating_sub(1)] {
if comp.is_empty() {
continue;
}
acc.push('/');
acc.push_str(comp);
if mounts.stat(&acc).is_none() {
let _ = mounts.mkdir(&acc, 0o755);
}
}
}
pub fn extract_into(mounts: &mut MountTable, tar: &[u8]) -> usize {
let mut off = 0usize;
let mut created = 0usize;
let mut long_name: Option<String> = None;
while off + BLOCK <= tar.len() {
let hdr = &tar[off..off + BLOCK];
if hdr.iter().all(|&b| b == 0) {
break;
}
let size = field_octal(&hdr[124..136]);
let mode = (field_octal(&hdr[100..108]) as u32) & 0o7777;
let typeflag = hdr[156];
let data_off = off + BLOCK;
let data_blocks = (size as usize).div_ceil(BLOCK);
let next = data_off + data_blocks * BLOCK;
let data_end = (data_off + size as usize).min(tar.len());
let data = if data_off <= tar.len() {
&tar[data_off..data_end]
} else {
&[][..]
};
let name = if let Some(ln) = long_name.take() {
ln
} else {
let name = field_str(&hdr[0..100]);
let prefix = field_str(&hdr[345..500]);
if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
}
};
match typeflag {
b'L' => {
long_name = Some(field_str(data));
}
b'5' => {
if let Some(p) = norm_path(&name) {
ensure_parents(mounts, &p);
if mounts.stat(&p).is_none() {
let _ = mounts.mkdir(&p, mode);
created += 1;
}
}
}
b'2' => {
if let Some(p) = norm_path(&name) {
let target = field_str(&hdr[157..257]);
ensure_parents(mounts, &p);
let _ = mounts.symlink(&target, &p);
created += 1;
}
}
b'0' | 0 => {
if let Some(p) = norm_path(&name) {
ensure_parents(mounts, &p);
if mounts.create(&p, mode).is_ok() {
let _ = mounts.write_at(&p, 0, data);
created += 1;
}
}
}
_ => {} }
off = next.max(off + BLOCK);
}
created
}
#[cfg(feature = "targz")]
pub fn gunzip(gz: &[u8], max_output: u64) -> Result<Vec<u8>, String> {
compcol::vec::decompress_to_vec_capped::<compcol::gzip::Gzip>(gz, max_output)
.map_err(|e| format!("gzip decompression failed: {e:?}"))
}
#[cfg(feature = "targz")]
pub fn extract_targz_into(
mounts: &mut MountTable,
targz: &[u8],
max_output: u64,
) -> Result<usize, String> {
let tar = gunzip(targz, max_output)?;
Ok(extract_into(mounts, &tar))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::TmpFs;
#[cfg(feature = "targz")]
#[test]
fn gunzip_decompresses_a_gzip_stream() {
let gz: &[u8] = &[
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0x48, 0xcd, 0xc9,
0xc9, 0x57, 0xc8, 0xcb, 0xac, 0x28, 0xcb, 0x55, 0x48, 0x2b, 0xca, 0xcf, 0x55, 0x48,
0xce, 0xcf, 0x2d, 0x48, 0xce, 0xcf, 0x01, 0x00, 0x84, 0x0a, 0xf1, 0xef, 0x18, 0x00,
0x00, 0x00,
];
assert_eq!(gunzip(gz, 1 << 20).unwrap(), b"hello nixvm from compcol");
assert!(gunzip(gz, 4).is_err());
}
fn entry(name: &str, typeflag: u8, link: &str, data: &[u8]) -> Vec<u8> {
let mut h = vec![0u8; BLOCK];
let nb = name.as_bytes();
h[..nb.len().min(100)].copy_from_slice(&nb[..nb.len().min(100)]);
h[100..107].copy_from_slice(b"0000644");
let sz = format!("{:011o}", data.len());
h[124..135].copy_from_slice(sz.as_bytes());
h[156] = typeflag;
let lb = link.as_bytes();
h[157..157 + lb.len().min(100)].copy_from_slice(&lb[..lb.len().min(100)]);
h[257..262].copy_from_slice(b"ustar");
let mut out = h;
out.extend_from_slice(data);
let pad = (BLOCK - data.len() % BLOCK) % BLOCK;
out.extend(std::iter::repeat_n(0u8, pad));
out
}
#[test]
fn extracts_files_dirs_and_symlinks() {
let mut tar = Vec::new();
tar.extend(entry("bin/", b'5', "", &[]));
tar.extend(entry("bin/busybox", b'0', "", b"ELFDATA"));
tar.extend(entry("bin/sh", b'2', "/bin/busybox", &[]));
tar.extend(entry("etc/hostname", b'0', "", b"alpine\n"));
tar.extend(vec![0u8; BLOCK * 2]);
let mut mounts = MountTable::new();
mounts.mount("/", Box::new(TmpFs::new()));
let n = extract_into(&mut mounts, &tar);
assert_eq!(n, 4);
assert!(mounts.stat("/bin").is_some());
let mut buf = vec![0u8; 7];
assert_eq!(mounts.read_at("/bin/busybox", 0, &mut buf).unwrap(), 7);
assert_eq!(&buf, b"ELFDATA");
assert_eq!(mounts.readlink("/bin/sh").unwrap(), "/bin/busybox");
assert!(mounts.stat("/etc").is_some());
}
#[test]
fn auto_creates_missing_parents() {
let mut tar = Vec::new();
tar.extend(entry("a/b/c/file", b'0', "", b"x"));
tar.extend(vec![0u8; BLOCK * 2]);
let mut mounts = MountTable::new();
mounts.mount("/", Box::new(TmpFs::new()));
extract_into(&mut mounts, &tar);
assert!(mounts.stat("/a/b/c").is_some());
assert!(mounts.stat("/a/b/c/file").is_some());
}
}