use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use indicatif::{ProgressBar, ProgressStyle};
use onelf_format::{EntryKind, symlink_target_within_root};
use crate::info::read_footer_and_manifest;
fn mode_bits(mode: u32, preserve_mode: bool) -> u32 {
if preserve_mode { mode } else { mode & 0o777 }
}
pub fn extract(
binary: &Path,
output: Option<&Path>,
files: &[String],
preserve_mode: bool,
) -> io::Result<()> {
if files.is_empty() {
let output_dir = output.unwrap_or(Path::new("onelf_extracted"));
return extract_all(binary, output_dir, preserve_mode);
}
extract_selective(binary, output, files, preserve_mode)
}
pub(crate) fn decompress_entry(
file: &mut File,
footer: &onelf_format::Footer,
entry: &onelf_format::Entry,
dict: Option<&[u8]>,
) -> io::Result<Vec<u8>> {
let mut result = Vec::new();
for block in &entry.blocks {
file.seek(SeekFrom::Start(
footer.payload_offset + block.payload_offset,
))?;
let mut compressed = vec![0u8; block.compressed_size as usize];
file.read_exact(&mut compressed)?;
if footer.is_stored() {
result.extend_from_slice(&compressed);
continue;
}
let cap = block.original_size as usize;
let decompressed = if let Some(d) = dict {
let mut dec = zstd::bulk::Decompressor::with_dictionary(d)?;
dec.decompress(&compressed, cap).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("decompression failed: {e}"),
)
})?
} else {
zstd::bulk::decompress(&compressed, cap).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("decompression failed: {e}"),
)
})?
};
if decompressed.len() != cap {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"decompressed block size {} != expected {cap}",
decompressed.len()
),
));
}
result.extend_from_slice(&decompressed);
}
Ok(result)
}
fn decompress_verified(
file: &mut File,
footer: &onelf_format::Footer,
entry: &onelf_format::Entry,
dict: Option<&[u8]>,
) -> io::Result<Vec<u8>> {
let data = decompress_entry(file, footer, entry, dict)?;
if blake3::hash(&data).as_bytes() != &entry.content_hash {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"content hash mismatch (tampered or corrupt package)",
));
}
Ok(data)
}
fn extract_selective(
binary: &Path,
output: Option<&Path>,
files: &[String],
preserve_mode: bool,
) -> io::Result<()> {
let (footer, manifest) = read_footer_and_manifest(binary)?;
let mut file = File::open(binary)?;
let dict = crate::info::read_dict(&mut file, &footer)?;
let matched: Vec<(usize, String)> = manifest
.entries
.iter()
.enumerate()
.filter(|(_, e)| e.kind == EntryKind::File)
.filter_map(|(i, _)| {
let path = manifest.entry_path(i);
if files.iter().any(|f| f == &path) {
Some((i, path))
} else {
None
}
})
.collect();
if matched.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("no files matched: {}", files.join(", ")),
));
}
let to_stdout = output.map_or(false, |p| p.as_os_str() == "-");
if to_stdout {
if matched.len() > 1 || files.len() > 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"stdout output (-o -) only supports a single --file",
));
}
let (idx, _) = &matched[0];
let entry = &manifest.entries[*idx];
let data = decompress_verified(&mut file, &footer, entry, dict.as_deref())?;
io::stdout().write_all(&data)?;
return Ok(());
}
if matched.len() == 1 && files.len() == 1 {
let (idx, _) = &matched[0];
let entry = &manifest.entries[*idx];
let data = decompress_verified(&mut file, &footer, entry, dict.as_deref())?;
if let Some(out) = output {
if out.is_dir() {
let rel_path = manifest.validated_entry_path(*idx)?;
let target = out.join(&rel_path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&target, &data)?;
fs::set_permissions(
&target,
fs::Permissions::from_mode(mode_bits(entry.mode, preserve_mode)),
)?;
} else {
if let Some(parent) = out.parent() {
fs::create_dir_all(parent)?;
}
fs::write(out, &data)?;
fs::set_permissions(
out,
fs::Permissions::from_mode(mode_bits(entry.mode, preserve_mode)),
)?;
}
} else {
let target = manifest.validated_entry_path(*idx)?;
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&target, &data)?;
fs::set_permissions(
&target,
fs::Permissions::from_mode(mode_bits(entry.mode, preserve_mode)),
)?;
}
return Ok(());
}
let output_dir = output.unwrap_or(Path::new("onelf_extracted"));
fs::create_dir_all(output_dir)?;
for (idx, _) in &matched {
let entry = &manifest.entries[*idx];
let data = decompress_verified(&mut file, &footer, entry, dict.as_deref())?;
let rel_path = manifest.validated_entry_path(*idx)?;
let target = output_dir.join(&rel_path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&target, &data)?;
fs::set_permissions(
&target,
fs::Permissions::from_mode(mode_bits(entry.mode, preserve_mode)),
)?;
}
Ok(())
}
fn extract_all(binary: &Path, output_dir: &Path, preserve_mode: bool) -> io::Result<()> {
let (footer, manifest) = read_footer_and_manifest(binary)?;
let mut file = File::open(binary)?;
let dict = crate::info::read_dict(&mut file, &footer)?;
let file_count = manifest
.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.count();
let pb = ProgressBar::new(file_count as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("=> "),
);
pb.set_message("Extracting...");
fs::create_dir_all(output_dir)?;
let mut dir_modes: Vec<(PathBuf, u32)> = Vec::new();
for (i, entry) in manifest.entries.iter().enumerate() {
let rel_path = manifest.validated_entry_path(i)?;
if rel_path.as_os_str().is_empty() {
continue;
}
let target = output_dir.join(&rel_path);
match entry.kind {
EntryKind::Dir => {
fs::create_dir_all(&target)?;
let mode = fs::metadata(&target)?.permissions().mode();
if mode & 0o700 != 0o700 {
fs::set_permissions(&target, fs::Permissions::from_mode(mode | 0o700))?;
}
dir_modes.push((target, mode_bits(entry.mode, preserve_mode)));
}
EntryKind::File => {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let data = decompress_verified(&mut file, &footer, entry, dict.as_deref())?;
fs::write(&target, &data)?;
fs::set_permissions(
&target,
fs::Permissions::from_mode(mode_bits(entry.mode, preserve_mode)),
)?;
pb.inc(1);
}
EntryKind::Symlink => {}
}
}
for (i, entry) in manifest.entries.iter().enumerate() {
if entry.kind != EntryKind::Symlink {
continue;
}
let rel_path = manifest.validated_entry_path(i)?;
if rel_path.as_os_str().is_empty() {
continue;
}
let link_target = manifest.get_string(entry.symlink_target);
if !symlink_target_within_root(&rel_path, link_target) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("symlink target escapes output dir: {}", rel_path.display()),
));
}
let target = output_dir.join(&rel_path);
if target.symlink_metadata().is_ok() {
fs::remove_file(&target)?;
}
std::os::unix::fs::symlink(link_target, &target)?;
}
dir_modes.sort_by_key(|(p, _)| std::cmp::Reverse(p.components().count()));
for (dir, mode) in &dir_modes {
fs::set_permissions(dir, fs::Permissions::from_mode(*mode))?;
}
pb.finish_with_message("Extraction complete");
Ok(())
}