use std::io::Read;
pub(crate) const MAX_EXECUTABLE: u64 = 256 * 1024 * 1024;
const MAX_SKIPPED: u64 = 16 * 1024 * 1024;
const BLOCK: usize = 512;
pub(crate) fn executable(gz: &[u8]) -> Result<Vec<u8>, String> {
let mut input = flate2::read::GzDecoder::new(gz);
let mut found: Option<Vec<u8>> = None;
let mut skipped = 0u64;
loop {
let mut header = [0u8; BLOCK];
if let Err(e) = input.read_exact(&mut header) {
return Err(format!("the archive ends early: {e}"));
}
if header.iter().all(|&b| b == 0) {
break;
}
let member = Header::parse(&header)?;
if member.path.split('/').any(|part| part == "..") {
return Err(format!("the archive holds a path with ..: {}", member.path));
}
let name = member.path.strip_prefix("./").unwrap_or(&member.path);
let base = name.rsplit('/').next().unwrap_or(name);
let skip = match member.kind {
b'x' | b'g' => true,
b'0' | 0 if base.starts_with("._") => true,
b'5' if member.size == 0 => true,
b'0' | 0 if name == "harness" => {
if found.is_some() {
return Err("the archive holds a second harness".into());
}
if member.size > MAX_EXECUTABLE {
return Err(format!(
"the archive's harness is {} bytes, over 256 MiB",
member.size
));
}
let mut body = Vec::with_capacity(member.size as usize);
read_body(&mut input, member.size, Some(&mut body))?;
found = Some(body);
false
}
b'0' | 0 => {
return Err(format!(
"the archive holds a regular file other than harness: {}",
member.path
));
}
kind => {
return Err(format!(
"the archive holds a member of type {:?} that is not a regular file: {}",
char::from(kind),
member.path
));
}
};
if skip {
skipped += member.size;
if skipped > MAX_SKIPPED {
return Err("the archive's other members are over 16 MiB".into());
}
read_body(&mut input, member.size, None)?;
}
}
found.ok_or_else(|| "the archive holds no harness".into())
}
fn read_body(
input: &mut impl Read,
size: u64,
mut keep: Option<&mut Vec<u8>>,
) -> Result<(), String> {
let padded = size.div_ceil(BLOCK as u64) * BLOCK as u64;
let mut buf = [0u8; 64 * 1024];
let mut done = 0u64;
while done < padded {
let want = ((padded - done) as usize).min(buf.len());
input
.read_exact(&mut buf[..want])
.map_err(|e| format!("the archive ends early: {e}"))?;
if let Some(out) = keep.as_deref_mut() {
let body_left = size.saturating_sub(done) as usize;
out.extend_from_slice(&buf[..want.min(body_left)]);
}
done += want as u64;
}
Ok(())
}
struct Header {
path: String,
kind: u8,
size: u64,
}
impl Header {
fn parse(h: &[u8; BLOCK]) -> Result<Header, String> {
let stored = octal(&h[148..156]).ok_or("the archive has a bad header checksum")?;
let sum: u64 = h
.iter()
.enumerate()
.map(|(i, &b)| {
if (148..156).contains(&i) {
32
} else {
u64::from(b)
}
})
.sum();
if sum != stored {
return Err("the archive has a bad header checksum".into());
}
let size = octal(&h[124..136]).ok_or("the archive has a bad member size")?;
let name = text(&h[0..100]);
let prefix = if &h[257..262] == b"ustar" {
text(&h[345..500])
} else {
String::new()
};
let path = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
Ok(Header {
path,
kind: h[156],
size,
})
}
}
fn octal(field: &[u8]) -> Option<u64> {
let digits: Vec<u8> = field
.iter()
.copied()
.skip_while(|&b| b == b' ')
.take_while(|&b| b != 0 && b != b' ')
.collect();
if digits.is_empty() || digits.len() > 21 {
return if digits.is_empty() { Some(0) } else { None };
}
digits.iter().try_fold(0u64, |n, &d| {
(b'0'..=b'7')
.contains(&d)
.then(|| n * 8 + u64::from(d - b'0'))
})
}
fn text(field: &[u8]) -> String {
let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
String::from_utf8_lossy(&field[..end]).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn header(name: &str, kind: u8, size: u64) -> [u8; BLOCK] {
let mut h = [0u8; BLOCK];
h[..name.len()].copy_from_slice(name.as_bytes());
h[124..135].copy_from_slice(format!("{size:011o}").as_bytes());
h[156] = kind;
h[257..263].copy_from_slice(b"ustar\0");
h[148..156].copy_from_slice(b" ");
let sum: u32 = h.iter().map(|&b| u32::from(b)).sum();
h[148..156].copy_from_slice(format!("{sum:06o}\0 ").as_bytes());
h
}
fn gz(blocks: &[&[u8]]) -> Vec<u8> {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
for b in blocks {
e.write_all(b).unwrap();
}
e.write_all(&[0u8; 1024]).unwrap();
e.finish().unwrap()
}
#[test]
fn an_executable_over_256_mib_is_refused_from_its_header() {
let h = header("harness", b'0', MAX_EXECUTABLE + 1);
let err = executable(&gz(&[&h])).unwrap_err();
assert!(err.contains("over 256 MiB"), "{err}");
}
#[test]
fn a_corrupt_header_is_refused() {
let mut h = header("harness", b'0', 0);
h[0] = b'H';
let err = executable(&gz(&[&h])).unwrap_err();
assert!(err.contains("checksum"), "{err}");
}
#[test]
fn a_folder_with_content_or_a_device_is_refused() {
for (kind, size) in [(b'5', 1), (b'3', 0), (b'4', 0), (b'6', 0)] {
let h = header("harness", kind, size);
let err = executable(&gz(&[&h, &[0u8; BLOCK]])).unwrap_err();
assert!(err.contains("not a regular file"), "{err}");
}
}
#[test]
fn the_body_is_read_exactly() {
let mut body = vec![7u8; 700];
body.resize(1024, 0);
let h = header("harness", b'0', 700);
assert_eq!(executable(&gz(&[&h, &body])).unwrap(), vec![7u8; 700]);
}
}