use crate::bundle::format_size;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{self, BufWriter, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use indicatif::{ProgressBar, ProgressStyle};
use jwalk::WalkDir;
use rayon::prelude::*;
use onelf_format::{
Entry, EntryKind, EntryPoint, EntryPointFlags, Flags, Footer, Manifest, ManifestHeader,
StringTableBuilder, WorkingDir,
};
use crate::compress;
pub struct PackOptions {
pub directory: PathBuf,
pub output: PathBuf,
pub command: String,
pub name: Option<String>,
pub entrypoints: Vec<(String, String, Vec<String>)>,
pub default_entrypoint: Option<String>,
pub lib_dirs: Vec<String>,
pub level: i32,
pub use_dict: bool,
pub no_compress: bool,
pub memfd: Option<bool>,
pub working_dir: WorkingDir,
pub update_url: Option<String>,
pub update_key: Option<Vec<u8>>,
pub exclude: Vec<String>,
pub package_info: Option<String>,
pub mtime: Option<u64>,
pub env: Vec<(String, String)>,
pub preload: Vec<String>,
}
struct CollectedFile {
rel_path: PathBuf,
content: Vec<u8>,
mode: u32,
mtime_secs: u64,
mtime_nsec: u32,
}
struct CompressedFile {
rel_path: PathBuf,
blocks: Vec<compress::CompressedBlock>,
content_hash: [u8; 32],
mode: u32,
mtime_secs: u64,
mtime_nsec: u32,
}
struct CollectedDir {
rel_path: PathBuf,
mode: u32,
mtime_secs: u64,
mtime_nsec: u32,
}
struct CollectedSymlink {
rel_path: PathBuf,
target: PathBuf,
mode: u32,
mtime_secs: u64,
mtime_nsec: u32,
}
fn auto_detect_lib_dirs(directory: &Path) -> Vec<String> {
let mut lib_dirs = Vec::new();
let Ok(canonical) = directory.canonicalize() else {
return lib_dirs;
};
for entry in WalkDir::new(&canonical).skip_hidden(false).sort(true) {
let Ok(entry) = entry else { continue };
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.contains(".so") {
continue;
}
let rel = path
.parent()
.and_then(|p| p.strip_prefix(&canonical).ok())
.map(|p| p.to_string_lossy().to_string());
if let Some(dir) = rel {
if dir.is_empty() {
continue;
}
if dir.starts_with(".onelf")
|| dir.starts_with("share/")
|| dir.starts_with("bin/")
|| dir.starts_with("etc/")
{
continue;
}
if !lib_dirs.contains(&dir) {
lib_dirs.push(dir);
}
}
}
lib_dirs
}
fn check_libgl_conflicts(directory: &Path, lib_dirs: &mut Vec<String>) {
let mut glvnd_dirs = Vec::new();
let mut legacy_dirs = Vec::new();
for dir in lib_dirs.iter() {
let full = directory.join(dir);
let gl_path = full.join("libGL.so.1");
if !gl_path.exists() {
continue;
}
let resolved = std::fs::canonicalize(&gl_path).unwrap_or(gl_path.clone());
if let Ok(data) = std::fs::read(&resolved) {
let has_gldispatch = data.windows(14).any(|w| w == b"libGLdispatch\0")
|| data.windows(7).any(|w| w == b"libGLX\0");
if has_gldispatch {
glvnd_dirs.push(dir.clone());
} else {
legacy_dirs.push(dir.clone());
}
}
}
if !glvnd_dirs.is_empty() && !legacy_dirs.is_empty() {
eprintln!(" warning: conflicting libGL.so detected");
eprintln!(" glvnd (modern): {}", glvnd_dirs.join(", "));
eprintln!(" legacy Mesa: {}", legacy_dirs.join(", "));
eprintln!(" Removing legacy dirs to avoid GL initialization failures");
lib_dirs.retain(|d| !legacy_dirs.contains(d));
}
}
fn utf8_file_name(p: &Path) -> io::Result<&str> {
p.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("non-UTF-8 file name: {}", p.display()),
)
})
}
fn utf8_str(p: &Path) -> io::Result<&str> {
p.to_str().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("non-UTF-8 path: {}", p.display()),
)
})
}
fn inject_onelf_file(
dirs: &mut Vec<CollectedDir>,
files: &mut Vec<CollectedFile>,
rel_path: &str,
content: Vec<u8>,
mtime: u64,
) {
if !dirs.iter().any(|d| d.rel_path == Path::new(".onelf")) {
dirs.push(CollectedDir {
rel_path: PathBuf::from(".onelf"),
mode: 0o755,
mtime_secs: mtime,
mtime_nsec: 0,
});
}
files.push(CollectedFile {
rel_path: PathBuf::from(rel_path),
content,
mode: 0o644,
mtime_secs: mtime,
mtime_nsec: 0,
});
}
pub fn pack(opts: &PackOptions, runtime_binary: &[u8]) -> io::Result<()> {
let dir = opts.directory.canonicalize()?;
let mut lib_dirs = if opts.lib_dirs.iter().any(|d| d == "auto") {
let mut dirs: Vec<String> = opts
.lib_dirs
.iter()
.filter(|d| *d != "auto")
.cloned()
.collect();
let auto = auto_detect_lib_dirs(&dir);
for d in auto {
if !dirs.contains(&d) {
dirs.push(d);
}
}
if !dirs.is_empty() {
eprintln!(" Auto-detected lib dirs: {}", dirs.join(", "));
}
dirs
} else {
opts.lib_dirs.clone()
};
check_libgl_conflicts(&dir, &mut lib_dirs);
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
pb.set_message("Scanning directory...");
let mut dirs: Vec<CollectedDir> = Vec::new();
let mut files: Vec<CollectedFile> = Vec::new();
let mut symlinks: Vec<CollectedSymlink> = Vec::new();
for entry in WalkDir::new(&dir).skip_hidden(false).sort(true) {
let entry = entry.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let abs_path = entry.path();
let rel_path = abs_path.strip_prefix(&dir).unwrap().to_path_buf();
if rel_path.as_os_str().is_empty() {
continue;
}
if !opts.exclude.is_empty() {
let excluded = rel_path.components().any(|c| {
let name = c.as_os_str().to_string_lossy();
opts.exclude.iter().any(|pat| {
if let Some(ext) = pat.strip_prefix("*.") {
name.ends_with(&format!(".{ext}"))
} else {
name == pat.as_str()
}
})
});
if excluded {
continue;
}
}
let symlink_meta = fs::symlink_metadata(&abs_path)?;
let (mtime_secs, mtime_nsec) = get_mtime(&symlink_meta, opts.mtime);
let mode = symlink_meta.permissions().mode();
if symlink_meta.is_symlink() {
let target = fs::read_link(&abs_path)?;
symlinks.push(CollectedSymlink {
rel_path,
target,
mode,
mtime_secs,
mtime_nsec,
});
} else if symlink_meta.is_dir() {
dirs.push(CollectedDir {
rel_path,
mode,
mtime_secs,
mtime_nsec,
});
} else if symlink_meta.is_file() {
let content = fs::read(&abs_path)?;
files.push(CollectedFile {
rel_path,
content,
mode,
mtime_secs,
mtime_nsec,
});
}
}
let inject_mtime = opts.mtime.unwrap_or(0);
let reserved = Path::new(".onelf");
let collision = files
.iter()
.map(|f| &f.rel_path)
.chain(dirs.iter().map(|d| &d.rel_path))
.chain(symlinks.iter().map(|s| &s.rel_path))
.find(|p| {
p.starts_with(reserved)
&& p.as_path() != reserved
&& !p.starts_with(".onelf/icons")
&& !p.starts_with(".onelf/desktop")
});
if let Some(p) = collision {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"source contains reserved path {}; the .onelf/ namespace is injected by onelf",
p.display()
),
));
}
if let Some(ref url) = opts.update_url {
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/update-url",
url.as_bytes().to_vec(),
inject_mtime,
);
}
if let Some(ref key) = opts.update_key {
if key.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"update key must be a 32-byte Ed25519 public key, got {} bytes",
key.len()
),
));
}
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/update-key",
key.clone(),
inject_mtime,
);
}
if let Some(ref info) = opts.package_info {
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/package-info.toml",
info.as_bytes().to_vec(),
inject_mtime,
);
}
let package_name = opts
.name
.as_deref()
.unwrap_or_else(|| opts.command.split('/').last().unwrap_or("app"));
{
let original_interp = files.iter().find_map(|f| elf_interp(&f.content));
let bundled_relpath = original_interp.as_ref().and_then(|interp| {
let interp_name = Path::new(interp).file_name()?.to_str()?;
let match_name = |p: &Path| p.file_name().and_then(|n| n.to_str()) == Some(interp_name);
files
.iter()
.find(|f| match_name(&f.rel_path))
.map(|f| f.rel_path.to_string_lossy().into_owned())
.or_else(|| {
symlinks
.iter()
.find(|s| match_name(&s.rel_path))
.map(|s| s.rel_path.to_string_lossy().into_owned())
})
});
if let Some(bundled_rel) = bundled_relpath {
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/interp",
bundled_rel.into_bytes(),
inject_mtime,
);
}
}
{
let user_sets_path = opts.env.iter().any(|(k, _)| k == "PATH");
let mut env_lines: Vec<String> = Vec::new();
if !user_sets_path {
env_lines.push("PATH=${ONELF_DIR}/bin:${PATH:-/usr/bin:/bin}".to_string());
}
for (k, v) in &opts.env {
env_lines.push(format!("{k}={v}"));
}
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/env",
env_lines.join("\n").into_bytes(),
inject_mtime,
);
}
if !opts.preload.is_empty() {
inject_onelf_file(
&mut dirs,
&mut files,
".onelf/preload",
opts.preload.join("\n").into_bytes(),
inject_mtime,
);
}
pb.finish_with_message(format!(
"Found {} dirs, {} files, {} symlinks",
dirs.len(),
files.len(),
symlinks.len()
));
let total_content_size: usize = files.iter().map(|f| f.content.len()).sum();
let dict = if opts.no_compress {
if opts.use_dict {
eprintln!("note: store mode overrides dictionary; payload stored raw");
}
None
} else if opts.use_dict && files.len() > 1 && total_content_size > 4096 {
let pb = ProgressBar::new_spinner();
pb.set_message("Building dictionary...");
let samples: Vec<Vec<u8>> = files.iter().map(|f| f.content.clone()).collect();
let dict_size = 1_048_576.min(total_content_size / 2);
match compress::build_dictionary(&samples, dict_size) {
Ok(dict) => {
pb.finish_with_message("Dictionary built");
Some(dict)
}
Err(e) => {
pb.finish_with_message(format!("Dictionary skipped: {e}"));
None
}
}
} else {
None
};
let pb = ProgressBar::new(files.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("=> "),
);
pb.set_message(if opts.no_compress {
"Storing files..."
} else {
"Compressing files..."
});
let compressed_files: Vec<CompressedFile> = files
.par_iter()
.map(|f| -> io::Result<CompressedFile> {
let content_hash: [u8; 32] = *blake3::hash(&f.content).as_bytes();
let blocks = if opts.no_compress {
compress::store_in_blocks(&f.content)
} else {
compress::compress_in_blocks(&f.content, opts.level, dict.as_deref()).map_err(
|e| io::Error::other(format!("compress {}: {e}", f.rel_path.display())),
)?
};
pb.inc(1);
Ok(CompressedFile {
rel_path: f.rel_path.clone(),
blocks,
content_hash,
mode: f.mode,
mtime_secs: f.mtime_secs,
mtime_nsec: f.mtime_nsec,
})
})
.collect::<io::Result<Vec<_>>>()?;
pb.finish_with_message(if opts.no_compress {
"Files stored (uncompressed)"
} else {
"Compression complete"
});
let mut strings = StringTableBuilder::new();
let mut path_to_index: HashMap<PathBuf, u32> = HashMap::new();
let mut entries: Vec<Entry> = Vec::new();
let root_name = strings.add("");
let name_offset = u16::try_from(strings.add(package_name)).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"package name offset exceeds u16 (string table too large)",
)
})?;
entries.push(Entry {
kind: EntryKind::Dir,
parent: u32::MAX,
name: root_name,
mode: 0o755,
mtime_secs: 0,
mtime_nsec: 0,
content_hash: [0; 32],
blocks: Vec::new(),
symlink_target: 0,
});
path_to_index.insert(PathBuf::new(), 0);
let mut sorted_dirs = dirs;
sorted_dirs.sort_by_key(|d| d.rel_path.components().count());
for d in &sorted_dirs {
let name_str = utf8_file_name(&d.rel_path)?;
let name = strings.add(name_str);
let parent_path = d.rel_path.parent().unwrap_or(Path::new(""));
let parent = *path_to_index.get(parent_path).unwrap_or(&0);
let idx = entries.len() as u32;
entries.push(Entry {
kind: EntryKind::Dir,
parent,
name,
mode: d.mode,
mtime_secs: d.mtime_secs,
mtime_nsec: d.mtime_nsec,
content_hash: [0; 32],
blocks: Vec::new(),
symlink_target: 0,
});
path_to_index.insert(d.rel_path.clone(), idx);
}
let mut payload_offset: u64 = 0;
for cf in &compressed_files {
let name_str = utf8_file_name(&cf.rel_path)?;
let name = strings.add(name_str);
let parent_path = cf.rel_path.parent().unwrap_or(Path::new(""));
let parent = *path_to_index.get(parent_path).unwrap_or(&0);
let idx = entries.len() as u32;
let blocks: Vec<onelf_format::Block> = cf
.blocks
.iter()
.map(|b| {
let block = onelf_format::Block {
payload_offset,
compressed_size: b.data.len() as u64,
original_size: b.original_size,
};
payload_offset += b.data.len() as u64;
block
})
.collect();
entries.push(Entry {
kind: EntryKind::File,
parent,
name,
mode: cf.mode,
mtime_secs: cf.mtime_secs,
mtime_nsec: cf.mtime_nsec,
content_hash: cf.content_hash,
blocks,
symlink_target: 0,
});
path_to_index.insert(cf.rel_path.clone(), idx);
}
for sl in &symlinks {
let name_str = utf8_file_name(&sl.rel_path)?;
let name = strings.add(name_str);
let target_str = utf8_str(&sl.target)?;
let target = strings.add(target_str);
let parent_path = sl.rel_path.parent().unwrap_or(Path::new(""));
let parent = *path_to_index.get(parent_path).unwrap_or(&0);
entries.push(Entry {
kind: EntryKind::Symlink,
parent,
name,
mode: sl.mode,
mtime_secs: sl.mtime_secs,
mtime_nsec: sl.mtime_nsec,
content_hash: [0; 32],
blocks: Vec::new(),
symlink_target: target,
});
}
let mut entrypoints: Vec<EntryPoint> = Vec::new();
let command_path = PathBuf::from(&opts.command);
let command_entry_idx = *path_to_index.get(&command_path).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("command path '{}' not found in directory", opts.command),
)
})?;
let default_name = opts
.default_entrypoint
.as_deref()
.or_else(|| command_path.file_name().and_then(|n| n.to_str()))
.unwrap_or("main");
let default_decl = opts
.default_entrypoint
.as_deref()
.and_then(|dn| opts.entrypoints.iter().find(|(n, _, _)| n == dn));
let ep_name = strings.add(default_name);
let empty_args = strings.add("");
let (ep0_target_idx, ep0_target_path) = match default_decl {
Some((_, path, _)) => {
let p = PathBuf::from(path);
let idx = *path_to_index.get(&p).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("entrypoint path '{}' not found in directory", path),
)
})?;
(idx, p)
}
None => (command_entry_idx, command_path.clone()),
};
let ep0_args = match default_decl {
Some((_, _, args)) if !args.is_empty() => strings.add(&args.join("\x1f")),
_ => empty_args,
};
let memfd_flag = match opts.memfd {
Some(true) => EntryPointFlags::MEMFD_ELIGIBLE,
Some(false) => EntryPointFlags::empty(),
None => {
let target_content = files
.iter()
.find(|f| f.rel_path == ep0_target_path)
.map(|f| f.content.as_slice());
if target_content.is_some_and(elf_has_no_deps) {
EntryPointFlags::MEMFD_ELIGIBLE
} else {
EntryPointFlags::empty()
}
}
};
entrypoints.push(EntryPoint {
name: ep_name,
target_entry: ep0_target_idx,
args: ep0_args,
working_dir: opts.working_dir,
flags: memfd_flag,
});
for (name, path, args) in &opts.entrypoints {
if opts.default_entrypoint.as_deref() == Some(name.as_str()) {
continue;
}
let ep_path = PathBuf::from(path);
let ep_entry_idx = *path_to_index.get(&ep_path).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("entrypoint path '{}' not found in directory", path),
)
})?;
let ep_name = strings.add(name);
let ep_args = if args.is_empty() {
empty_args
} else {
strings.add(&args.join("\x1f"))
};
entrypoints.push(EntryPoint {
name: ep_name,
target_entry: ep_entry_idx,
args: ep_args,
working_dir: opts.working_dir,
flags: EntryPointFlags::empty(),
});
}
let lib_dir_offsets: Vec<u32> = lib_dirs.iter().map(|d| strings.add(d)).collect();
let string_table = strings.finish();
let manifest = Manifest {
header: ManifestHeader {
version: 1,
entry_count: entries.len() as u32,
string_table_size: string_table.len() as u32,
entrypoint_count: entrypoints.len() as u16,
default_entrypoint: 0,
lib_dir_count: lib_dir_offsets.len() as u16,
name_offset,
package_id: [0; 32], },
entrypoints,
entries,
lib_dir_offsets,
string_table,
};
let mut manifest_bytes = manifest.serialize()?;
let package_id: [u8; 32] = *blake3::hash(&manifest_bytes).as_bytes();
manifest_bytes[18..50].copy_from_slice(&package_id);
let manifest_compressed = compress::compress_manifest(&manifest_bytes)?;
let manifest_checksum = xxhash_rust::xxh32::xxh32(&manifest_bytes, 0).to_le_bytes();
let total_payload: u64 = compressed_files
.iter()
.map(|f| f.blocks.iter().map(|b| b.data.len() as u64).sum::<u64>())
.sum();
let mut flags = Flags::empty();
if dict.is_some() {
flags |= Flags::HAS_DICT;
}
if opts.memfd == Some(true) {
flags |= Flags::MEMFD_HINT;
}
if opts.no_compress {
flags |= Flags::STORED;
}
let pb = ProgressBar::new_spinner();
pb.set_message("Writing output...");
let runtime_size = runtime_binary.len() as u64;
let manifest_offset = runtime_size;
let payload_start = manifest_offset + manifest_compressed.len() as u64;
let dict_offset;
let dict_size;
if let Some(ref d) = dict {
dict_offset = payload_start + total_payload;
dict_size = d.len() as u32;
} else {
dict_offset = 0;
dict_size = 0;
}
let footer = Footer {
format_version: 1,
flags,
manifest_offset,
manifest_compressed: manifest_compressed.len() as u64,
manifest_original: manifest_bytes.len() as u64,
payload_offset: payload_start,
payload_size: total_payload,
dict_offset,
dict_size,
manifest_checksum,
};
let out = File::create(&opts.output)?;
let mut w = BufWriter::new(out);
let mut runtime_patched = runtime_binary.to_vec();
if runtime_patched.len() >= 16 {
runtime_patched[9..15].copy_from_slice(b"ONELF\x00");
}
w.write_all(&runtime_patched)?;
w.write_all(&manifest_compressed)?;
for cf in &compressed_files {
for block in &cf.blocks {
w.write_all(&block.data)?;
}
}
if let Some(ref d) = dict {
w.write_all(d)?;
}
footer.write_to(&mut w)?;
w.flush()?;
drop(w);
let perms = fs::Permissions::from_mode(0o755);
fs::set_permissions(&opts.output, perms)?;
let output_size = fs::metadata(&opts.output).map(|m| m.len()).unwrap_or(0);
pb.finish_with_message(format!("Written to {}", opts.output.display()));
let total_input = total_content_size as u64;
let file_count = compressed_files.len();
let dir_count = sorted_dirs.len();
let symlink_count = symlinks.len();
eprintln!();
eprintln!(
" {} {} files, {} dirs, {} symlinks",
bold("Input:"),
file_count,
dir_count,
symlink_count
);
eprintln!(" {} {}", bold("Content:"), format_size(total_input));
if opts.no_compress {
eprintln!(
" {} {} (stored, uncompressed)",
bold("Payload:"),
format_size(total_payload)
);
} else {
eprintln!(
" {} {} (zstd level {})",
bold("Payload:"),
format_size(total_payload),
opts.level
);
}
if let Some(ref d) = dict {
eprintln!(" {} {}", bold("Dict:"), format_size(d.len() as u64));
}
eprintln!(" {} {}", bold("Runtime:"), format_size(runtime_size));
eprintln!(
" {} {} (ratio: {:.2}x)",
bold("Output:"),
format_size(output_size),
if output_size > 0 {
total_input as f64 / output_size as f64
} else {
0.0
}
);
Ok(())
}
fn bold(s: &str) -> String {
if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
format!("\x1b[1m{s}\x1b[0m")
} else {
s.to_string()
}
}
fn get_mtime(meta: &fs::Metadata, pin: Option<u64>) -> (u64, u32) {
if let Some(ts) = pin {
return (ts, 0);
}
let raw = meta
.modified()
.ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
if let Some(epoch) = source_date_epoch() {
return (raw.min(epoch), 0);
}
(raw, 0)
}
fn source_date_epoch() -> Option<u64> {
std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|v| v.trim().parse().ok())
}
fn elf_has_no_deps(data: &[u8]) -> bool {
if data.len() < 4 || data[0..4] != *b"\x7fELF" {
return false;
}
match goblin::elf::Elf::parse(data) {
Ok(elf) => elf.libraries.is_empty(),
Err(_) => false,
}
}
fn elf_interp(data: &[u8]) -> Option<String> {
if data.len() < 64 || data[0..4] != *b"\x7fELF" {
return None;
}
let class = data[4];
let (e_phoff, e_phentsize, e_phnum) = match class {
2 => {
let e_phoff = u64::from_le_bytes(data[32..40].try_into().ok()?) as usize;
let e_phentsize = u16::from_le_bytes(data[54..56].try_into().ok()?) as usize;
let e_phnum = u16::from_le_bytes(data[56..58].try_into().ok()?) as usize;
(e_phoff, e_phentsize, e_phnum)
}
1 => {
let e_phoff = u32::from_le_bytes(data[28..32].try_into().ok()?) as usize;
let e_phentsize = u16::from_le_bytes(data[42..44].try_into().ok()?) as usize;
let e_phnum = u16::from_le_bytes(data[44..46].try_into().ok()?) as usize;
(e_phoff, e_phentsize, e_phnum)
}
_ => return None,
};
for i in 0..e_phnum {
let off = e_phoff + i * e_phentsize;
if off + e_phentsize > data.len() {
break;
}
let p_type = u32::from_le_bytes(data[off..off + 4].try_into().ok()?);
if p_type != 3 {
continue;
}
let (p_offset, p_filesz) = match class {
2 => {
let o = u64::from_le_bytes(data[off + 8..off + 16].try_into().ok()?) as usize;
let s = u64::from_le_bytes(data[off + 32..off + 40].try_into().ok()?) as usize;
(o, s)
}
1 => {
let o = u32::from_le_bytes(data[off + 4..off + 8].try_into().ok()?) as usize;
let s = u32::from_le_bytes(data[off + 16..off + 20].try_into().ok()?) as usize;
(o, s)
}
_ => return None,
};
if p_offset + p_filesz > data.len() {
return None;
}
let interp = &data[p_offset..p_offset + p_filesz];
let interp = match interp.iter().position(|&b| b == 0) {
Some(pos) => &interp[..pos],
None => interp,
};
return std::str::from_utf8(interp).ok().map(String::from);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tmpdir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("onelf-pack-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
fn base_opts(dir: &Path, out: &Path, command: &str) -> PackOptions {
PackOptions {
directory: dir.to_path_buf(),
output: out.to_path_buf(),
command: command.to_string(),
name: None,
entrypoints: Vec::new(),
default_entrypoint: None,
lib_dirs: Vec::new(),
level: 3,
use_dict: false,
no_compress: false,
memfd: Some(false),
working_dir: WorkingDir::Inherit,
update_url: None,
update_key: None,
exclude: Vec::new(),
package_info: None,
mtime: Some(0),
env: Vec::new(),
preload: Vec::new(),
}
}
#[test]
fn name_round_trips_past_64kib_string_table() {
let dir = tmpdir("name64k");
let app = dir.join("app");
fs::create_dir_all(app.join("bin")).unwrap();
fs::write(app.join("bin/run"), b"#!/bin/sh\n").unwrap();
let pad = "n".repeat(210);
for i in 0..400 {
fs::write(app.join(format!("file_{i:04}_{pad}")), b"x").unwrap();
}
let out = dir.join("pkg.onelf");
let mut opts = base_opts(&app, &out, "bin/run");
opts.name = Some("round-trip-name".to_string());
pack(&opts, b"stub-runtime").unwrap();
let (_footer, manifest) = crate::info::read_footer_and_manifest(&out).unwrap();
assert!(
manifest.string_table.len() > 0x1_0000,
"test must exceed a 64 KiB string table, got {}",
manifest.string_table.len()
);
assert_eq!(manifest.name(), "round-trip-name");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn default_entrypoint_uses_declared_path_and_args_without_duplicate() {
let dir = tmpdir("default-ep");
let app = dir.join("app");
fs::create_dir_all(app.join("bin")).unwrap();
fs::write(app.join("bin/a"), b"#!/bin/sh\necho a\n").unwrap();
fs::write(app.join("bin/b"), b"#!/bin/sh\necho b\n").unwrap();
let out = dir.join("pkg.onelf");
let mut opts = base_opts(&app, &out, "bin/a");
opts.entrypoints = vec![(
"b".to_string(),
"bin/b".to_string(),
vec!["--flag".to_string(), "x".to_string()],
)];
opts.default_entrypoint = Some("b".to_string());
pack(&opts, b"stub-runtime").unwrap();
let (_footer, manifest) = crate::info::read_footer_and_manifest(&out).unwrap();
assert_eq!(manifest.entrypoints.len(), 1);
let ep = &manifest.entrypoints[0];
assert_eq!(manifest.get_string(ep.name), "b");
assert_eq!(manifest.entry_path(ep.target_entry as usize), "bin/b");
assert_eq!(manifest.get_string(ep.args), "--flag\x1fx");
assert_eq!(manifest.header.default_entrypoint, 0);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn non_utf8_file_name_errors_instead_of_panicking() {
use std::os::unix::ffi::OsStrExt;
let dir = tmpdir("nonutf8");
let app = dir.join("app");
fs::create_dir_all(app.join("bin")).unwrap();
fs::write(app.join("bin/run"), b"#!/bin/sh\n").unwrap();
let bad = std::ffi::OsStr::from_bytes(b"bad-\xff\xfe-name");
fs::write(app.join(bad), b"x").unwrap();
let out = dir.join("pkg.onelf");
let opts = base_opts(&app, &out, "bin/run");
let err = pack(&opts, b"stub-runtime").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
let _ = fs::remove_dir_all(&dir);
}
}