diff --git a/Cargo.lock b/Cargo.lock index 2e4c72c..5fb1ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,11 +158,24 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "brioche-autowrap" +version = "0.1.0" +dependencies = [ + "brioche-pack", + "brioche-resources", + "bstr", + "goblin 0.8.2", + "thiserror", +] + [[package]] name = "brioche-ld" version = "0.1.1" dependencies = [ + "brioche-autowrap", "brioche-pack", + "brioche-resources", "bstr", "goblin 0.7.1", "thiserror", @@ -192,6 +205,7 @@ name = "brioche-packed-plain-exec" version = "0.1.1" dependencies = [ "brioche-pack", + "brioche-resources", "bstr", "libc", "thiserror", @@ -203,6 +217,7 @@ version = "0.1.1" dependencies = [ "bincode", "brioche-pack", + "brioche-resources", "bstr", "cfg-if", "libc", @@ -214,12 +229,25 @@ dependencies = [ name = "brioche-packer" version = "0.1.0" dependencies = [ + "brioche-autowrap", "brioche-pack", + "brioche-resources", "clap", "serde_json", "thiserror", ] +[[package]] +name = "brioche-resources" +version = "0.1.0" +dependencies = [ + "blake3", + "bstr", + "pathdiff", + "thiserror", + "ulid", +] + [[package]] name = "bstr" version = "1.9.1" diff --git a/Cargo.toml b/Cargo.toml index 37d5005..7bd5004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] resolver = "2" -members = [ +members = [ "crates/brioche-autowrap", "crates/brioche-ld", "crates/brioche-packed-plain-exec", "crates/brioche-packed-userland-exec", - "crates/brioche-packer", + "crates/brioche-packer", "crates/brioche-resources", "crates/runnable", "crates/runnable-core", "crates/start-runnable", diff --git a/crates/brioche-autowrap/Cargo.toml b/crates/brioche-autowrap/Cargo.toml new file mode 100644 index 0000000..18eda61 --- /dev/null +++ b/crates/brioche-autowrap/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "brioche-autowrap" +version = "0.1.0" +edition = "2021" + +[dependencies] +brioche-pack = { workspace = true } +brioche-resources = { path = "../brioche-resources" } +bstr = "1.9.1" +goblin = "0.8.2" +thiserror = "1.0.61" diff --git a/crates/brioche-autowrap/src/lib.rs b/crates/brioche-autowrap/src/lib.rs new file mode 100644 index 0000000..1d9ee6f --- /dev/null +++ b/crates/brioche-autowrap/src/lib.rs @@ -0,0 +1,382 @@ +use std::{ + collections::{HashSet, VecDeque}, + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use bstr::ByteSlice as _; + +#[derive(Debug, Clone, Copy)] +pub struct AutowrapOptions<'a> { + pub program_path: &'a Path, + pub packed_exec_path: &'a Path, + pub resource_dir: &'a Path, + pub all_resource_dirs: &'a [PathBuf], + pub sysroot: &'a Path, + pub library_search_paths: &'a [PathBuf], + pub input_paths: &'a [PathBuf], + pub skip_libs: &'a [String], + pub skip_unknown_libs: bool, + pub runtime_library_dirs: &'a [PathBuf], +} + +pub fn autowrap(options: AutowrapOptions) -> Result<(), AutowrapError> { + let program_file = std::fs::read(options.program_path)?; + let program_object = goblin::Object::parse(&program_file)?; + + match program_object { + goblin::Object::Elf(elf) => { + if let Some(program_interpreter) = elf.interpreter { + let program_interpreter_path = PathBuf::from(program_interpreter); + let relative_interpreter_path = + program_interpreter_path.strip_prefix("/").map_err(|_| { + AutowrapError::UnsupportedInterpreterPath(program_interpreter.to_owned()) + })?; + let interpreter_path = options.sysroot.join(relative_interpreter_path); + if !interpreter_path.is_file() { + return Err(AutowrapError::UnsupportedInterpreterPath( + program_interpreter.to_owned(), + )); + } + + let pack = dynamic_ld_linux_elf_pack(DynamicLdLinuxElfPackOptions { + program_path: options.program_path, + program_contents: &program_file, + resource_dir: options.resource_dir, + all_resource_dirs: options.all_resource_dirs, + interpreter_path: &interpreter_path, + library_search_paths: options.library_search_paths, + input_paths: options.input_paths, + skip_libs: options.skip_libs, + skip_unknown_libs: options.skip_unknown_libs, + elf: &elf, + runtime_library_dirs: options.runtime_library_dirs, + })?; + + let mut packed = std::fs::File::open(options.packed_exec_path)?; + let mut packed_program = std::fs::File::create(options.program_path)?; + std::io::copy(&mut packed, &mut packed_program)?; + + brioche_pack::inject_pack(&mut packed_program, &pack)?; + } else { + let pack = static_elf_pack(StaticElfPackOptions { + resource_dir: options.resource_dir, + all_resource_dirs: options.all_resource_dirs, + library_search_paths: options.library_search_paths, + input_paths: options.input_paths, + skip_libs: options.skip_libs, + skip_unknown_libs: options.skip_unknown_libs, + elf: &elf, + })?; + + if pack.should_add_to_executable() { + let mut program = std::fs::OpenOptions::new() + .append(true) + .open(options.program_path)?; + brioche_pack::inject_pack(&mut program, &pack)?; + } + } + } + goblin::Object::Archive(_) => { + // Nothing to do + } + _ => { + unimplemented!("unsupported output type"); + } + } + + Ok(()) +} + +struct DynamicLdLinuxElfPackOptions<'a> { + program_path: &'a Path, + program_contents: &'a [u8], + resource_dir: &'a Path, + all_resource_dirs: &'a [PathBuf], + interpreter_path: &'a Path, + library_search_paths: &'a [PathBuf], + input_paths: &'a [PathBuf], + skip_libs: &'a [String], + skip_unknown_libs: bool, + elf: &'a goblin::elf::Elf<'a>, + runtime_library_dirs: &'a [PathBuf], +} + +fn dynamic_ld_linux_elf_pack( + options: DynamicLdLinuxElfPackOptions, +) -> Result { + let program_name = options + .program_path + .file_name() + .ok_or_else(|| AutowrapError::InvalidPath)?; + let resource_program_path = brioche_resources::add_named_blob( + options.resource_dir, + std::io::Cursor::new(&options.program_contents), + is_path_executable(options.program_path)?, + Path::new(program_name), + )?; + + let interpreter_name = options + .interpreter_path + .file_name() + .ok_or_else(|| AutowrapError::InvalidPath)?; + let interpreter = std::fs::File::open(options.interpreter_path)?; + let resource_interpreter_path = brioche_resources::add_named_blob( + options.resource_dir, + interpreter, + is_path_executable(options.interpreter_path)?, + Path::new(interpreter_name), + )?; + let resource_interpreter_path = <[u8]>::from_path(&resource_interpreter_path) + .ok_or_else(|| AutowrapError::InvalidPath)? + .into(); + + let find_library_options = FindLibraryOptions { + resource_dir: options.resource_dir, + all_resource_dirs: options.all_resource_dirs, + library_search_paths: options.library_search_paths, + input_paths: options.input_paths, + skip_libs: options.skip_libs, + skip_unknown_libs: options.skip_unknown_libs, + }; + + let resource_library_dirs = collect_all_library_dirs(&find_library_options, options.elf)?; + + let resource_program_path = <[u8]>::from_path(&resource_program_path) + .ok_or_else(|| AutowrapError::InvalidPath)? + .into(); + let runtime_library_dirs = options + .runtime_library_dirs + .iter() + .map(|dir| { + let dir = <[u8]>::from_path(dir).ok_or(AutowrapError::InvalidPath)?; + Ok::<_, AutowrapError>(dir.to_vec()) + }) + .collect::, _>>()?; + let pack = brioche_pack::Pack::LdLinux { + program: resource_program_path, + interpreter: resource_interpreter_path, + library_dirs: resource_library_dirs, + runtime_library_dirs, + }; + + Ok(pack) +} + +struct StaticElfPackOptions<'a> { + resource_dir: &'a Path, + all_resource_dirs: &'a [PathBuf], + library_search_paths: &'a [PathBuf], + input_paths: &'a [PathBuf], + skip_libs: &'a [String], + skip_unknown_libs: bool, + elf: &'a goblin::elf::Elf<'a>, +} + +fn static_elf_pack(options: StaticElfPackOptions) -> Result { + let find_library_options = FindLibraryOptions { + resource_dir: options.resource_dir, + all_resource_dirs: options.all_resource_dirs, + library_search_paths: options.library_search_paths, + input_paths: options.input_paths, + skip_libs: options.skip_libs, + skip_unknown_libs: options.skip_unknown_libs, + }; + + let resource_library_dirs = collect_all_library_dirs(&find_library_options, options.elf)?; + + let pack = brioche_pack::Pack::Static { + library_dirs: resource_library_dirs, + }; + + Ok(pack) +} + +struct FindLibraryOptions<'a> { + resource_dir: &'a Path, + all_resource_dirs: &'a [PathBuf], + library_search_paths: &'a [PathBuf], + input_paths: &'a [PathBuf], + skip_libs: &'a [String], + skip_unknown_libs: bool, +} + +fn collect_all_library_dirs( + options: &FindLibraryOptions, + elf: &goblin::elf::Elf, +) -> Result>, AutowrapError> { + let mut all_search_paths = options.library_search_paths.to_vec(); + + let mut resource_library_dirs = vec![]; + let mut needed_libraries = elf + .libraries + .iter() + .map(|lib| lib.to_string()) + .collect::>(); + let mut found_libraries = HashSet::new(); + let skip_libraries = options.skip_libs.iter().collect::>(); + + while let Some(original_library_name) = needed_libraries.pop_front() { + if found_libraries.contains(&original_library_name) { + continue; + } + + let library_path_result = find_library( + &FindLibraryOptions { + input_paths: options.input_paths, + library_search_paths: &all_search_paths, + resource_dir: options.resource_dir, + all_resource_dirs: options.all_resource_dirs, + skip_libs: options.skip_libs, + skip_unknown_libs: options.skip_unknown_libs, + }, + &original_library_name, + ); + let library_path = match library_path_result { + Ok(library_path) => library_path, + Err(AutowrapError::LibraryNotFound(_)) if options.skip_unknown_libs => { + continue; + } + Err(err) => { + return Err(err); + } + }; + let library_name = std::path::PathBuf::from(&original_library_name); + let library_name = library_name + .file_name() + .ok_or_else(|| AutowrapError::InvalidPath)?; + + found_libraries.insert(original_library_name.clone()); + + // Don't add the library if it's been skipped. We still do everything + // else so we can add transitive dependencies even if a library has + // been skipped. + if !skip_libraries.contains(&original_library_name) { + let library = std::fs::File::open(&library_path)?; + let resource_library_path = brioche_resources::add_named_blob( + options.resource_dir, + library, + is_path_executable(&library_path)?, + Path::new(library_name), + )?; + let resource_library_dir = resource_library_path + .parent() + .expect("no parent dir for library path"); + let resource_library_dir = <[u8]>::from_path(resource_library_dir) + .ok_or_else(|| AutowrapError::InvalidPath)? + .into(); + + resource_library_dirs.push(resource_library_dir); + } + + // Try to get dynamic dependencies from the library itself + + let Ok(library_file) = std::fs::read(&library_path) else { + continue; + }; + let Ok(library_object) = goblin::Object::parse(&library_file) else { + continue; + }; + + // TODO: Support other object files + let library_elf = match library_object { + goblin::Object::Elf(elf) => elf, + _ => continue, + }; + needed_libraries.extend(library_elf.libraries.iter().map(|lib| lib.to_string())); + + if let Ok(library_pack) = brioche_pack::extract_pack(&library_file[..]) { + let library_dirs = match &library_pack { + brioche_pack::Pack::LdLinux { library_dirs, .. } => &library_dirs[..], + brioche_pack::Pack::Static { library_dirs } => &library_dirs[..], + brioche_pack::Pack::Metadata { .. } => &[], + }; + + for library_dir in library_dirs { + let Ok(library_dir) = library_dir.to_path() else { + continue; + }; + let Some(library_dir_path) = brioche_resources::find_in_resource_dirs( + options.all_resource_dirs, + library_dir, + ) else { + continue; + }; + + all_search_paths.push(library_dir_path); + } + } + } + + Ok(resource_library_dirs) +} + +fn find_library( + options: &FindLibraryOptions, + library_name: &str, +) -> Result { + // Search for the library from the search paths passed to `ld`, searching + // for a filename match. + for lib_path in options.library_search_paths { + let lib_path = lib_path.join(library_name); + + if lib_path.is_file() { + return Ok(lib_path); + } + } + + // Search for the library from the input files passed to `ld`, searching + // for a filename match. + for input_path in options.input_paths { + let library_name = OsStr::new(library_name); + if input_path == library_name || input_path.file_name() == Some(library_name) { + return Ok(input_path.to_owned()); + } + } + + // Search for the library from the input files passed to `ld`, searching + // for an ELF file with a matching `DT_SONAME` section. + for input_path in options.input_paths { + if let Ok(bytes) = std::fs::read(input_path) { + if let Ok(elf) = goblin::elf::Elf::parse(&bytes) { + if elf.soname == Some(library_name) { + return Ok(input_path.to_owned()); + } + } + } + } + + Err(AutowrapError::LibraryNotFound(library_name.to_string())) +} + +#[derive(Debug, thiserror::Error)] +pub enum AutowrapError { + #[error("invalid path")] + InvalidPath, + #[error("unsupported interpreter path: {0}")] + UnsupportedInterpreterPath(String), + #[error("{0}")] + IoError(#[from] std::io::Error), + #[error("{0}")] + GoblinError(#[from] goblin::error::Error), + #[error("error when finding resource dir")] + ResourceDirError(#[from] brioche_resources::PackResourceDirError), + #[error("library not found in search paths: {0}")] + LibraryNotFound(String), + #[error("error writing packed program")] + InjectPackError(#[from] brioche_pack::InjectPackError), + #[error("error adding blob: {0}")] + AddBlobError(#[from] brioche_resources::AddBlobError), +} + +fn is_path_executable(path: &Path) -> std::io::Result { + use std::os::unix::prelude::PermissionsExt as _; + + let metadata = std::fs::metadata(path)?; + + let permissions = metadata.permissions(); + let mode = permissions.mode(); + let is_executable = mode & 0o111 != 0; + + Ok(is_executable) +} diff --git a/crates/brioche-ld/Cargo.toml b/crates/brioche-ld/Cargo.toml index 331412f..dacbb31 100644 --- a/crates/brioche-ld/Cargo.toml +++ b/crates/brioche-ld/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.1" edition = "2021" [dependencies] +brioche-autowrap = { path = "../brioche-autowrap" } brioche-pack = { workspace = true } +brioche-resources = { path = "../brioche-resources" } bstr = "1.8.0" goblin = "0.7.1" thiserror = "1.0.51" diff --git a/crates/brioche-ld/src/main.rs b/crates/brioche-ld/src/main.rs index a0f443f..69ae0df 100644 --- a/crates/brioche-ld/src/main.rs +++ b/crates/brioche-ld/src/main.rs @@ -73,9 +73,9 @@ fn run() -> Result { let autowrap_mode = match std::env::var("BRIOCHE_LD_AUTOWRAP").as_deref() { Ok("false") => Mode::AutowrapDisabled, _ => { - let resource_dir = brioche_pack::find_output_resource_dir(&output_path) + let resource_dir = brioche_resources::find_output_resource_dir(&output_path) .map_err(LdError::ResourceDirError)?; - let all_resource_dirs = brioche_pack::find_resource_dirs(¤t_exe, true) + let all_resource_dirs = brioche_resources::find_resource_dirs(¤t_exe, true) .map_err(LdError::ResourceDirError)?; Mode::AutowrapEnabled { resource_dir, @@ -106,7 +106,7 @@ fn run() -> Result { resource_dir, all_resource_dirs, } => { - brioche_pack::autowrap::autowrap(brioche_pack::autowrap::AutowrapOptions { + brioche_autowrap::autowrap(brioche_autowrap::AutowrapOptions { program_path: &output_path, packed_exec_path: &packed_path, resource_dir: &resource_dir, @@ -130,7 +130,7 @@ fn run() -> Result { #[derive(Debug, thiserror::Error)] enum LdError { #[error("error wrapping binary: {0}")] - AutowrapError(#[from] brioche_pack::autowrap::AutowrapError), + AutowrapError(#[from] brioche_autowrap::AutowrapError), #[error("invalid arg")] InvalidArg, #[error("invalid path")] @@ -146,9 +146,9 @@ enum LdError { #[error("{0}")] GoblinError(#[from] goblin::error::Error), #[error("error when finding resource dir")] - ResourceDirError(#[from] brioche_pack::PackResourceDirError), + ResourceDirError(#[from] brioche_resources::PackResourceDirError), #[error("error writing packed program")] InjectPackError(#[from] brioche_pack::InjectPackError), #[error("error adding blob: {0}")] - AddBlobError(#[from] brioche_pack::resources::AddBlobError), + AddBlobError(#[from] brioche_resources::AddBlobError), } diff --git a/crates/brioche-packed-plain-exec/Cargo.toml b/crates/brioche-packed-plain-exec/Cargo.toml index e5bd0bd..d01a0ab 100644 --- a/crates/brioche-packed-plain-exec/Cargo.toml +++ b/crates/brioche-packed-plain-exec/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] brioche-pack = { workspace = true } +brioche-resources = { path = "../brioche-resources" } bstr = "1.8.0" libc = "0.2.151" thiserror = "1.0.51" diff --git a/crates/brioche-packed-plain-exec/src/main.rs b/crates/brioche-packed-plain-exec/src/main.rs index 4dab95a..059b162 100644 --- a/crates/brioche-packed-plain-exec/src/main.rs +++ b/crates/brioche-packed-plain-exec/src/main.rs @@ -18,7 +18,7 @@ pub fn main() -> ExitCode { fn run() -> Result<(), PackedError> { let path = std::env::current_exe()?; let parent_path = path.parent().ok_or(PackedError::InvalidPath)?; - let resource_dirs = brioche_pack::find_resource_dirs(&path, true)?; + let resource_dirs = brioche_resources::find_resource_dirs(&path, true)?; let mut program = std::fs::File::open(&path)?; let pack = brioche_pack::extract_pack(&mut program)?; @@ -34,7 +34,7 @@ fn run() -> Result<(), PackedError> { let interpreter = interpreter .to_path() .map_err(|_| PackedError::InvalidPath)?; - let interpreter = brioche_pack::find_in_resource_dirs(&resource_dirs, interpreter) + let interpreter = brioche_resources::find_in_resource_dirs(&resource_dirs, interpreter) .ok_or(PackedError::ResourceNotFound)?; let mut command = std::process::Command::new(interpreter); @@ -52,8 +52,9 @@ fn run() -> Result<(), PackedError> { let library_dir = library_dir .to_path() .map_err(|_| PackedError::InvalidPath)?; - let library_dir = brioche_pack::find_in_resource_dirs(&resource_dirs, library_dir) - .ok_or(PackedError::ResourceNotFound)?; + let library_dir = + brioche_resources::find_in_resource_dirs(&resource_dirs, library_dir) + .ok_or(PackedError::ResourceNotFound)?; resolved_library_dirs.push(library_dir); } @@ -92,7 +93,7 @@ fn run() -> Result<(), PackedError> { } let program = program.to_path().map_err(|_| PackedError::InvalidPath)?; - let program = brioche_pack::find_in_resource_dirs(&resource_dirs, program) + let program = brioche_resources::find_in_resource_dirs(&resource_dirs, program) .ok_or(PackedError::ResourceNotFound)?; let program = program.canonicalize()?; command.arg(program); @@ -115,7 +116,7 @@ fn run() -> Result<(), PackedError> { enum PackedError { IoError(#[from] std::io::Error), ExtractPackError(#[from] brioche_pack::ExtractPackError), - PackResourceDirError(#[from] brioche_pack::PackResourceDirError), + PackResourceDirError(#[from] brioche_resources::PackResourceDirError), ResourceNotFound, InvalidPath, } @@ -142,11 +143,13 @@ fn error_summary(error: &PackedError) -> &'static str { brioche_pack::ExtractPackError::InvalidPack(_) => "failed to parse pack: bincode error", }, PackedError::PackResourceDirError(error) => match error { - brioche_pack::PackResourceDirError::NotFound => "brioche pack resource dir not found", - brioche_pack::PackResourceDirError::DepthLimitReached => { + brioche_resources::PackResourceDirError::NotFound => { + "brioche pack resource dir not found" + } + brioche_resources::PackResourceDirError::DepthLimitReached => { "reached depth limit while searching for brioche pack resource dir" } - brioche_pack::PackResourceDirError::IoError(_) => { + brioche_resources::PackResourceDirError::IoError(_) => { "error while searching for brioche pack resource dir: io error" } }, diff --git a/crates/brioche-packed-userland-exec/Cargo.toml b/crates/brioche-packed-userland-exec/Cargo.toml index 95132f5..f5cc9be 100644 --- a/crates/brioche-packed-userland-exec/Cargo.toml +++ b/crates/brioche-packed-userland-exec/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] bincode = "2.0.0-rc.3" brioche-pack = { workspace = true } +brioche-resources = { path = "../brioche-resources" } bstr = "1.8.0" cfg-if = "1.0.0" libc = "0.2.151" diff --git a/crates/brioche-packed-userland-exec/src/linux.rs b/crates/brioche-packed-userland-exec/src/linux.rs index 4ae935a..96221da 100644 --- a/crates/brioche-packed-userland-exec/src/linux.rs +++ b/crates/brioche-packed-userland-exec/src/linux.rs @@ -52,7 +52,7 @@ pub unsafe fn entrypoint(argc: libc::c_int, argv: *const *const libc::c_char) -> fn run(args: &[&CStr], env_vars: &[&CStr]) -> Result<(), PackedError> { let path = std::env::current_exe()?; let parent_path = path.parent().ok_or(PackedError::InvalidPath)?; - let resource_dirs = brioche_pack::find_resource_dirs(&path, true)?; + let resource_dirs = brioche_resources::find_resource_dirs(&path, true)?; let mut program = std::fs::File::open(&path)?; let pack = brioche_pack::extract_pack(&mut program)?; @@ -66,11 +66,11 @@ fn run(args: &[&CStr], env_vars: &[&CStr]) -> Result<(), PackedError> { let interpreter = interpreter .to_path() .map_err(|_| PackedError::InvalidPath)?; - let interpreter = brioche_pack::find_in_resource_dirs(&resource_dirs, interpreter) + let interpreter = brioche_resources::find_in_resource_dirs(&resource_dirs, interpreter) .ok_or(PackedError::ResourceNotFound)?; let program = program.to_path().map_err(|_| PackedError::InvalidPath)?; - let program = brioche_pack::find_in_resource_dirs(&resource_dirs, program) + let program = brioche_resources::find_in_resource_dirs(&resource_dirs, program) .ok_or(PackedError::ResourceNotFound)?; let program = program.canonicalize()?; let mut exec = userland_execve::ExecOptions::new(&interpreter); @@ -93,8 +93,9 @@ fn run(args: &[&CStr], env_vars: &[&CStr]) -> Result<(), PackedError> { let library_dir = library_dir .to_path() .map_err(|_| PackedError::InvalidPath)?; - let library_dir = brioche_pack::find_in_resource_dirs(&resource_dirs, library_dir) - .ok_or(PackedError::ResourceNotFound)?; + let library_dir = + brioche_resources::find_in_resource_dirs(&resource_dirs, library_dir) + .ok_or(PackedError::ResourceNotFound)?; resolved_library_dirs.push(library_dir); } @@ -158,7 +159,7 @@ fn run(args: &[&CStr], env_vars: &[&CStr]) -> Result<(), PackedError> { enum PackedError { IoError(#[from] std::io::Error), ExtractPackError(#[from] brioche_pack::ExtractPackError), - PackResourceDirError(#[from] brioche_pack::PackResourceDirError), + PackResourceDirError(#[from] brioche_resources::PackResourceDirError), InvalidPath, ResourceNotFound, } @@ -185,11 +186,13 @@ fn error_summary(error: &PackedError) -> &'static str { brioche_pack::ExtractPackError::InvalidPack(_) => "failed to parse pack: bincode error", }, PackedError::PackResourceDirError(error) => match error { - brioche_pack::PackResourceDirError::NotFound => "brioche pack resource dir not found", - brioche_pack::PackResourceDirError::DepthLimitReached => { + brioche_resources::PackResourceDirError::NotFound => { + "brioche pack resource dir not found" + } + brioche_resources::PackResourceDirError::DepthLimitReached => { "reached depth limit while searching for brioche pack resource dir" } - brioche_pack::PackResourceDirError::IoError(_) => { + brioche_resources::PackResourceDirError::IoError(_) => { "error while searching for brioche pack resource dir: io error" } }, diff --git a/crates/brioche-packer/Cargo.toml b/crates/brioche-packer/Cargo.toml index 5c04a58..fde2e53 100644 --- a/crates/brioche-packer/Cargo.toml +++ b/crates/brioche-packer/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] +brioche-autowrap = { path = "../brioche-autowrap" } brioche-pack = { workspace = true } +brioche-resources = { path = "../brioche-resources" } clap = { version = "4.4.11", features = ["derive"] } serde_json = { version = "1.0.108" } thiserror = "1.0.51" diff --git a/crates/brioche-packer/src/main.rs b/crates/brioche-packer/src/main.rs index b08c556..4a7549c 100644 --- a/crates/brioche-packer/src/main.rs +++ b/crates/brioche-packer/src/main.rs @@ -70,20 +70,18 @@ fn run() -> Result<(), PackerError> { } => { for program in &programs { let resource_dir = - brioche_pack::find_output_resource_dir(program).map_err(|error| { + brioche_resources::find_output_resource_dir(program).map_err(|error| { PackerError::PackResourceDir { program: program.clone(), error, } })?; - let all_resource_dirs = - brioche_pack::find_resource_dirs(program, true).map_err(|error| { - PackerError::PackResourceDir { - program: program.clone(), - error, - } + let all_resource_dirs = brioche_resources::find_resource_dirs(program, true) + .map_err(|error| PackerError::PackResourceDir { + program: program.clone(), + error, })?; - brioche_pack::autowrap::autowrap(brioche_pack::autowrap::AutowrapOptions { + brioche_autowrap::autowrap(brioche_autowrap::AutowrapOptions { program_path: program, packed_exec_path: &packed_exec, resource_dir: &resource_dir, @@ -130,12 +128,12 @@ enum PackerError { PackResourceDir { program: PathBuf, #[source] - error: brioche_pack::PackResourceDirError, + error: brioche_resources::PackResourceDirError, }, #[error("error wrapping {program}: {error}")] Autowrap { program: PathBuf, #[source] - error: brioche_pack::autowrap::AutowrapError, + error: brioche_autowrap::AutowrapError, }, } diff --git a/crates/brioche-resources/Cargo.toml b/crates/brioche-resources/Cargo.toml new file mode 100644 index 0000000..f021bf8 --- /dev/null +++ b/crates/brioche-resources/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "brioche-resources" +version = "0.1.0" +edition = "2021" + +[dependencies] +blake3 = "1.5.1" +bstr = "1.9.1" +pathdiff = "0.2.1" +thiserror = "1.0.61" +ulid = "1.1.2" diff --git a/crates/brioche-resources/src/lib.rs b/crates/brioche-resources/src/lib.rs new file mode 100644 index 0000000..a60846e --- /dev/null +++ b/crates/brioche-resources/src/lib.rs @@ -0,0 +1,154 @@ +use std::{ + os::unix::fs::OpenOptionsExt as _, + path::{Path, PathBuf}, +}; + +use bstr::ByteSlice as _; + +const SEARCH_DEPTH_LIMIT: u32 = 64; + +pub fn find_resource_dirs( + program: &Path, + include_readonly: bool, +) -> Result, PackResourceDirError> { + let mut paths = vec![]; + if let Some(pack_resource_dir) = std::env::var_os("BRIOCHE_RESOURCE_DIR") { + paths.push(PathBuf::from(pack_resource_dir)); + } + + if include_readonly { + if let Some(input_resource_dirs) = std::env::var_os("BRIOCHE_INPUT_RESOURCE_DIRS") { + if let Some(input_resource_dirs) = <[u8]>::from_os_str(&input_resource_dirs) { + for input_resource_dir in input_resource_dirs.split_str(b":") { + if let Ok(path) = input_resource_dir.to_path() { + paths.push(path.to_owned()); + } + } + } + + for input_resource_dir in std::env::split_paths(&input_resource_dirs) { + paths.push(input_resource_dir); + } + } + } + + match find_resource_dir_from_program(program) { + Ok(pack_resource_dir) => paths.push(pack_resource_dir), + Err(PackResourceDirError::NotFound) => {} + Err(error) => { + return Err(error); + } + } + + if !paths.is_empty() { + Ok(paths) + } else { + Err(PackResourceDirError::NotFound) + } +} + +pub fn find_output_resource_dir(program: &Path) -> Result { + let resource_dirs = find_resource_dirs(program, false)?; + let resource_dir = resource_dirs + .into_iter() + .next() + .ok_or(PackResourceDirError::NotFound)?; + Ok(resource_dir) +} + +pub fn find_in_resource_dirs(resource_dirs: &[PathBuf], subpath: &Path) -> Option { + for resource_dir in resource_dirs { + let path = resource_dir.join(subpath); + if path.exists() { + return Some(path); + } + } + + None +} + +fn find_resource_dir_from_program(program: &Path) -> Result { + let program = std::env::current_dir()?.join(program); + + let Some(mut current_dir) = program.parent() else { + return Err(PackResourceDirError::NotFound); + }; + + for _ in 0..SEARCH_DEPTH_LIMIT { + let pack_resource_dir = current_dir.join("brioche-resources.d"); + if pack_resource_dir.is_dir() { + return Ok(pack_resource_dir); + } + + let Some(parent) = current_dir.parent() else { + return Err(PackResourceDirError::NotFound); + }; + + current_dir = parent; + } + + Err(PackResourceDirError::DepthLimitReached) +} + +pub fn add_named_blob( + resource_dir: &Path, + mut contents: impl std::io::Seek + std::io::Read, + executable: bool, + name: &Path, +) -> Result { + let mut hasher = blake3::Hasher::new(); + std::io::copy(&mut contents, &mut hasher)?; + let hash = hasher.finalize(); + + let blob_suffix = if executable { ".x" } else { "" }; + let blob_name = format!("{hash}{blob_suffix}"); + + contents.seek(std::io::SeekFrom::Start(0))?; + + let blob_dir = resource_dir.join("blobs"); + let blob_path = blob_dir.join(&blob_name); + let blob_temp_id = ulid::Ulid::new(); + let blob_temp_path = blob_dir.join(format!("{blob_name}-{blob_temp_id}")); + std::fs::create_dir_all(&blob_dir)?; + + let mut blob_file_options = std::fs::OpenOptions::new(); + blob_file_options.create_new(true).write(true); + if executable { + blob_file_options.mode(0o777); + } + let mut blob_file = blob_file_options.open(&blob_temp_path)?; + std::io::copy(&mut contents, &mut blob_file)?; + drop(blob_file); + std::fs::rename(&blob_temp_path, &blob_path)?; + + let alias_dir = resource_dir.join("aliases").join(name).join(&blob_name); + std::fs::create_dir_all(&alias_dir)?; + + let temp_alias_path = alias_dir.join(format!("{}-{blob_temp_id}", name.display())); + let alias_path = alias_dir.join(name); + let blob_pack_relative_path = pathdiff::diff_paths(&blob_path, &alias_dir) + .expect("blob path is not a prefix of alias path"); + std::os::unix::fs::symlink(blob_pack_relative_path, &temp_alias_path)?; + std::fs::rename(&temp_alias_path, &alias_path)?; + + let alias_path = alias_path + .strip_prefix(resource_dir) + .expect("alias path is not in resource dir"); + Ok(alias_path.to_owned()) +} + +#[derive(Debug, thiserror::Error)] +pub enum PackResourceDirError { + #[error("brioche pack resource dir not found")] + NotFound, + #[error("error while searching for brioche pack resource dir: {0}")] + IoError(#[from] std::io::Error), + #[error("reached depth limit while searching for brioche pack resource dir")] + DepthLimitReached, +} + +#[derive(Debug, thiserror::Error)] +pub enum AddBlobError { + #[error("{0}")] + IoError(#[from] std::io::Error), +}