macbinary-rs 0.1.0

Transparent access to MacBinary-encoded files
Documentation
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 {
    /// Append suffix '.rsrc' to resource fork files
    Suffix,
    /// Put resource forks in a hidden directory called '.rsrc'
    HiddenDirectory,
    #[cfg(target_os = "macos")]
    /// Write to actual resource fork using '..namedfork/rsrc' syntax
    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"))]
    /// Determine how resource forks should be treated during extraction
    rsrc: ResourceForkExtractionStrategy,

    /// Input path to extract
    #[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()
    }
}