use std::io::{self, Read, Write};
pub const FOOTER_SIZE: usize = 76;
pub const MAGIC: [u8; 8] = *b"ONELF\x00\x01\x00";
pub const END_MAGIC: [u8; 8] = *b"FLENONE\x00";
pub const FORMAT_VERSION: u16 = 2;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flags: u16 {
const HAS_DICT = 1 << 0;
const MEMFD_HINT = 1 << 1;
const STORED = 1 << 3;
const EXTERNAL_UPDATER = 1 << 4;
const NO_HOST_LIB_DIRS = 1 << 5;
const HOST_LIBS_ALWAYS = 1 << 6;
const CACHE_REQUESTED = 1 << 7;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostLibsPolicy {
Never,
Auto,
Always,
}
impl HostLibsPolicy {
pub fn from_flags(flags: Flags) -> Self {
if flags.contains(Flags::NO_HOST_LIB_DIRS) {
HostLibsPolicy::Never
} else if flags.contains(Flags::HOST_LIBS_ALWAYS) {
HostLibsPolicy::Always
} else {
HostLibsPolicy::Auto
}
}
pub fn to_flags(self) -> Flags {
match self {
HostLibsPolicy::Never => Flags::NO_HOST_LIB_DIRS,
HostLibsPolicy::Auto => Flags::empty(),
HostLibsPolicy::Always => Flags::HOST_LIBS_ALWAYS,
}
}
pub fn as_str(self) -> &'static str {
match self {
HostLibsPolicy::Never => "never",
HostLibsPolicy::Auto => "auto",
HostLibsPolicy::Always => "always",
}
}
}
#[derive(Debug, Clone)]
pub struct Footer {
pub format_version: u16,
pub flags: Flags,
pub manifest_offset: u64,
pub manifest_compressed: u64,
pub manifest_original: u64,
pub payload_offset: u64,
pub payload_size: u64,
pub dict_offset: u64,
pub dict_size: u32,
pub manifest_checksum: [u8; 4],
}
impl Footer {
pub fn is_stored(&self) -> bool {
self.flags.contains(Flags::STORED)
}
pub fn structure_hash(&self) -> [u8; 32] {
let mut buf = [0u8; FOOTER_SIZE];
let mut sink: &mut [u8] = &mut buf;
self.write_to(&mut sink).ok();
buf[20..44].fill(0);
buf[52..60].fill(0);
*blake3::hash(&buf).as_bytes()
}
pub fn write_to<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&MAGIC)?; w.write_all(&self.format_version.to_le_bytes())?; w.write_all(&self.flags.bits().to_le_bytes())?; w.write_all(&self.manifest_offset.to_le_bytes())?; w.write_all(&self.manifest_compressed.to_le_bytes())?; w.write_all(&self.manifest_original.to_le_bytes())?; w.write_all(&self.payload_offset.to_le_bytes())?; w.write_all(&self.payload_size.to_le_bytes())?; w.write_all(&self.dict_offset.to_le_bytes())?; w.write_all(&self.dict_size.to_le_bytes())?; w.write_all(&self.manifest_checksum)?; w.write_all(&END_MAGIC)?; Ok(()) }
pub fn read_from<R: Read>(r: &mut R) -> io::Result<Self> {
let mut buf = [0u8; FOOTER_SIZE];
r.read_exact(&mut buf)?;
Self::from_bytes(&buf)
}
pub fn from_bytes(buf: &[u8; FOOTER_SIZE]) -> io::Result<Self> {
if buf[0..8] != MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid onelf magic",
));
}
if buf[68..76] != END_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid onelf end magic",
));
}
let format_version = u16::from_le_bytes(buf[8..10].try_into().unwrap());
if format_version != 1 && format_version != FORMAT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported format version: {}", format_version),
));
}
let flags_raw = u16::from_le_bytes(buf[10..12].try_into().unwrap());
let flags = Flags::from_bits_retain(flags_raw);
Ok(Footer {
format_version,
flags,
manifest_offset: u64::from_le_bytes(buf[12..20].try_into().unwrap()),
manifest_compressed: u64::from_le_bytes(buf[20..28].try_into().unwrap()),
manifest_original: u64::from_le_bytes(buf[28..36].try_into().unwrap()),
payload_offset: u64::from_le_bytes(buf[36..44].try_into().unwrap()),
payload_size: u64::from_le_bytes(buf[44..52].try_into().unwrap()),
dict_offset: u64::from_le_bytes(buf[52..60].try_into().unwrap()),
dict_size: u32::from_le_bytes(buf[60..64].try_into().unwrap()),
manifest_checksum: buf[64..68].try_into().unwrap(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Footer {
Footer {
format_version: FORMAT_VERSION,
flags: Flags::HAS_DICT | Flags::STORED,
manifest_offset: 0x1122,
manifest_compressed: 0x33,
manifest_original: 0x44,
payload_offset: 0x55,
payload_size: 0x66,
dict_offset: 0x77,
dict_size: 0x88,
manifest_checksum: [1, 2, 3, 4],
}
}
#[test]
fn footer_roundtrips() {
let f = sample();
let mut buf = Vec::new();
f.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), FOOTER_SIZE);
let back = Footer::from_bytes(&buf.try_into().unwrap()).unwrap();
assert_eq!(back.format_version, f.format_version);
assert_eq!(back.flags, f.flags);
assert_eq!(back.manifest_offset, f.manifest_offset);
assert_eq!(back.dict_size, f.dict_size);
assert_eq!(back.manifest_checksum, f.manifest_checksum);
}
#[test]
fn malformed_footers_error_without_panic() {
let mut buf = Vec::new();
sample().write_to(&mut buf).unwrap();
let mut bad = buf.clone();
bad[0] ^= 0xff;
assert!(Footer::from_bytes(&bad.clone().try_into().unwrap()).is_err());
let mut bad = buf.clone();
bad[68] ^= 0xff;
assert!(Footer::from_bytes(&bad.clone().try_into().unwrap()).is_err());
for version in [0u16, 3u16, 99u16] {
let mut bad = buf.clone();
bad[8..10].copy_from_slice(&version.to_le_bytes());
assert!(Footer::from_bytes(&bad.try_into().unwrap()).is_err());
}
for version in [1u16, FORMAT_VERSION] {
let mut ok = buf.clone();
ok[8..10].copy_from_slice(&version.to_le_bytes());
assert!(Footer::from_bytes(&ok.try_into().unwrap()).is_ok());
}
}
#[test]
fn structure_hash_pins_everything_but_the_size_fields() {
let f = sample();
let h = f.structure_hash();
let mut grown = f.clone();
grown.manifest_compressed += 7;
grown.manifest_original += 7;
grown.payload_offset += 7;
grown.dict_offset += 7;
assert_eq!(grown.structure_hash(), h);
let mut other = f.clone();
other.payload_size += 1;
assert_ne!(other.structure_hash(), h);
let mut other = f.clone();
other.manifest_checksum = [9, 9, 9, 9];
assert_ne!(other.structure_hash(), h);
let mut other = f.clone();
other.flags = Flags::empty();
assert_ne!(other.structure_hash(), h);
let mut other = f.clone();
other.manifest_offset += 1;
assert_ne!(other.structure_hash(), h);
}
#[test]
fn host_libs_policy_decodes_every_encoding() {
assert_eq!(
HostLibsPolicy::from_flags(Flags::empty()),
HostLibsPolicy::Auto
);
assert_eq!(
HostLibsPolicy::from_flags(Flags::NO_HOST_LIB_DIRS),
HostLibsPolicy::Never
);
assert_eq!(
HostLibsPolicy::from_flags(Flags::HOST_LIBS_ALWAYS),
HostLibsPolicy::Always
);
assert_eq!(
HostLibsPolicy::from_flags(Flags::NO_HOST_LIB_DIRS | Flags::HOST_LIBS_ALWAYS),
HostLibsPolicy::Never
);
for policy in [
HostLibsPolicy::Never,
HostLibsPolicy::Auto,
HostLibsPolicy::Always,
] {
assert_eq!(HostLibsPolicy::from_flags(policy.to_flags()), policy);
}
}
#[test]
fn host_libs_policy_ignores_unknown_bits() {
let unknown = Flags::from_bits_retain(1 << 15);
assert_eq!(HostLibsPolicy::from_flags(unknown), HostLibsPolicy::Auto);
assert_eq!(
HostLibsPolicy::from_flags(unknown | Flags::HOST_LIBS_ALWAYS),
HostLibsPolicy::Always
);
let mut buf = Vec::new();
let mut f = sample();
f.flags = unknown | Flags::CACHE_REQUESTED;
f.write_to(&mut buf).unwrap();
let back = Footer::from_bytes(&buf.try_into().unwrap()).unwrap();
assert!(back.flags.contains(Flags::CACHE_REQUESTED));
assert_eq!(back.flags.bits() & (1 << 15), 1 << 15);
}
}