use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
use onelf_format::{EntryKind, FOOTER_SIZE, Footer, Manifest};
pub fn info(path: &Path) -> io::Result<()> {
let (footer, manifest) = read_footer_and_manifest(path)?;
println!("onelf binary: {}", path.display());
println!();
println!("Format version: {}", footer.format_version);
println!("Flags: {:?}", footer.flags);
println!();
println!("Manifest:");
println!(" Offset: {}", footer.manifest_offset);
println!(" Compressed: {} bytes", footer.manifest_compressed);
println!(" Original: {} bytes", footer.manifest_original);
println!(
" Checksum: {:02x}{:02x}{:02x}{:02x}",
footer.manifest_checksum[0],
footer.manifest_checksum[1],
footer.manifest_checksum[2],
footer.manifest_checksum[3]
);
println!();
println!("Payload:");
println!(" Offset: {}", footer.payload_offset);
println!(" Size: {} bytes", footer.payload_size);
println!();
if footer.dict_size > 0 {
println!("Dictionary:");
println!(" Offset: {}", footer.dict_offset);
println!(" Size: {} bytes", footer.dict_size);
println!();
}
let file_count = manifest
.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.count();
let dir_count = manifest
.entries
.iter()
.filter(|e| e.kind == EntryKind::Dir)
.count();
let symlink_count = manifest
.entries
.iter()
.filter(|e| e.kind == EntryKind::Symlink)
.count();
println!("Package ID: {}", hex(&manifest.header.package_id));
println!(
"Entries: {} ({} dirs, {} files, {} symlinks)",
manifest.header.entry_count, dir_count, file_count, symlink_count
);
println!();
if let Some(info) = read_package_info(path, &manifest)? {
println!("Metadata:");
for line in info.lines() {
println!(" {line}");
}
println!();
}
println!("Entrypoints:");
for (i, ep) in manifest.entrypoints.iter().enumerate() {
let name = manifest.get_string(ep.name);
let target_path = manifest.entry_path(ep.target_entry as usize);
let args = manifest.get_string(ep.args);
let default_marker = if i == manifest.header.default_entrypoint as usize {
" (default)"
} else {
""
};
let memfd = if ep.is_memfd_eligible() {
" [memfd]"
} else {
""
};
print!(" {}{}{}: {}", name, default_marker, memfd, target_path);
if !args.is_empty() {
print!(" args={}", args.replace('\x1f', " "));
}
println!();
}
let total_original: u64 = manifest
.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.map(|e| e.blocks.iter().map(|b| b.original_size).sum::<u64>())
.sum();
println!();
println!(
"Total original size: {} bytes ({:.1} MB)",
total_original,
total_original as f64 / 1_048_576.0
);
println!(
"Total compressed size: {} bytes ({:.1} MB)",
footer.payload_size,
footer.payload_size as f64 / 1_048_576.0
);
if total_original > 0 {
let ratio = footer.payload_size as f64 / total_original as f64 * 100.0;
println!("Compression ratio: {:.1}%", ratio);
}
Ok(())
}
pub fn read_footer_and_manifest(path: &Path) -> io::Result<(Footer, Manifest)> {
let mut file = File::open(path)?;
let file_size = file.metadata()?.len();
if file_size < FOOTER_SIZE as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"file too small for onelf footer",
));
}
file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
let mut footer_buf = [0u8; FOOTER_SIZE];
file.read_exact(&mut footer_buf)?;
let footer = Footer::from_bytes(&footer_buf)?;
file.seek(SeekFrom::Start(footer.manifest_offset))?;
let mut manifest_compressed = vec![0u8; footer.manifest_compressed as usize];
file.read_exact(&mut manifest_compressed)?;
let manifest_bytes =
zstd::bulk::decompress(&manifest_compressed, footer.manifest_original as usize).map_err(
|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("manifest decompression failed: {e}"),
)
},
)?;
let checksum = xxhash_rust::xxh32::xxh32(&manifest_bytes, 0).to_le_bytes();
if checksum != footer.manifest_checksum {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"manifest checksum mismatch",
));
}
let manifest = Manifest::deserialize(&manifest_bytes)?;
Ok((footer, manifest))
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn read_package_info(path: &Path, manifest: &Manifest) -> io::Result<Option<String>> {
use crate::extract::decompress_entry;
let idx = (0..manifest.entries.len()).find(|&i| {
manifest.entries[i].kind == EntryKind::File
&& manifest.entry_path(i) == ".onelf/package-info.toml"
});
let Some(idx) = idx else { return Ok(None) };
let mut file = File::open(path)?;
file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
let mut footer_buf = [0u8; FOOTER_SIZE];
file.read_exact(&mut footer_buf)?;
let footer = Footer::from_bytes(&footer_buf)?;
let dict = if footer.dict_size > 0 {
file.seek(SeekFrom::Start(footer.dict_offset))?;
let mut buf = vec![0u8; footer.dict_size as usize];
file.read_exact(&mut buf)?;
Some(buf)
} else {
None
};
let data = decompress_entry(&mut file, &footer, &manifest.entries[idx], dict.as_deref())?;
let text = String::from_utf8(data).unwrap_or_default();
Ok(Some(text))
}