use std::{fs, io::Read, path::PathBuf, process};
use clap::Parser;
use serde::Serialize;
#[derive(clap::ValueEnum, Clone, Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ResourceForkExtractionStrategy {
Suffix,
HiddenDirectory,
#[cfg(target_os = "macos")]
NamedFork,
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[cfg_attr(target_os = "macos", arg(long, default_value = "named-fork"))]
#[cfg_attr(not(target_os = "macos"), arg(long, default_value = "suffix"))]
rsrc: ResourceForkExtractionStrategy,
#[arg(required = true)]
path: PathBuf,
}
pub fn main() {
pretty_env_logger::init();
log::debug!("Startup");
let cli = Cli::parse();
let mut file = match macbinary::MacBinary::open(&cli.path) {
Ok(file) => file,
Err(e) => {
eprintln!("Could not open {} as macbinary", cli.path.display());
log::error!("{e:?}");
process::exit(1);
}
};
let Some(header) = file.header() else {
println!("File is not macbinary encoded, skipping extraction");
process::exit(0);
};
let original_name = header.name.clone().clean_mac_filename();
let Some(input_parent) = cli.path.parent() else {
eprintln!("Could not form output path");
process::exit(2);
};
let data_fork_len = header.data_fork_len;
let resource_fork_len = header.resource_fork_len;
let output_path = input_parent.join(&original_name);
if header.data_fork_len != 0 {
let mut buffer = vec![0u8; data_fork_len as usize];
let mut reader = match file.data_fork() {
Ok(reader) => reader,
Err(e) => {
eprintln!("Could not open data fork of macbinary");
log::error!("{e:?}");
process::exit(2);
}
};
match reader.read_exact(&mut buffer) {
Ok(_) => {}
Err(e) => {
eprintln!("Could not read all data for data fork of macbinary");
log::error!("{e:?}");
process::exit(2);
}
};
match fs::write(&output_path, buffer) {
Ok(_) => {}
Err(e) => {
eprintln!(
"Could not write output file {} for data fork",
output_path.display()
);
log::error!("{e:?}");
process::exit(2);
}
}
println!("Data fork written to {}", output_path.display());
}
if resource_fork_len != 0 {
let output_path = match cli.rsrc {
ResourceForkExtractionStrategy::Suffix => {
input_parent.join(format!("{}.rsrc", &original_name))
}
ResourceForkExtractionStrategy::HiddenDirectory => {
let directory = input_parent.join(".rsrc");
if fs::create_dir_all(&directory).is_err() {
eprintln!("Could not create output directory for resource fork");
process::exit(2);
}
directory.join(&original_name)
}
#[cfg(target_os = "macos")]
ResourceForkExtractionStrategy::NamedFork => output_path.join("..namedfork/rsrc"),
};
let mut buffer = vec![0u8; resource_fork_len as usize];
let mut reader = match file.resource_fork() {
Ok(reader) => reader,
Err(e) => {
eprintln!("Could not open resource fork of macbinary");
log::error!("{e:?}");
process::exit(2);
}
};
match reader.read_exact(&mut buffer) {
Ok(_) => {}
Err(e) => {
eprintln!("Could not read all data for resource fork of macbinary");
log::error!("{e:?}");
process::exit(2);
}
};
match fs::write(&output_path, buffer) {
Ok(_) => {}
Err(e) => {
eprintln!(
"Could not write output file {} for resource fork",
output_path.display()
);
log::error!("{e:?}");
process::exit(2);
}
}
println!("Resource fork written to {}", output_path.display());
}
}
pub(crate) trait Clean {
fn clean_mac_filename(self) -> String;
}
impl Clean for String {
fn clean_mac_filename(self) -> String {
self.replace("\r", "\\r").replace("/", ":")
}
}
impl Clean for &str {
fn clean_mac_filename(self) -> String {
self.to_string().clean_mac_filename()
}
}
impl Clean for std::path::Display<'_> {
fn clean_mac_filename(self) -> String {
self.to_string().clean_mac_filename()
}
}