Skip to main content

fs_mac/
fork_export.rs

1//! Fork-preserving single-file export, shared by the browse view's "Extract"
2//! and Commander's copy/export-to-host.
3//!
4//! Reads a file's data fork (`read_file`) and resource fork
5//! (`write_resource_fork_to`) off any [`Filesystem`] and writes it into a host
6//! directory in the chosen [`ResourceForkMode`]:
7//! - **MacBinary** — single `.bin` carrying both forks (only when a resource
8//!   fork is present; otherwise falls through to a plain data-fork write).
9//! - **BinHex** — single `.hqx` text file (always applicable; carries Finder
10//!   info even with no resource fork).
11//! - **AppleDouble** — data fork under the plain name + a `._name` sidecar
12//!   (emitted whenever there's a resource fork *or* type/creator to preserve).
13//! - **Native** — data fork + the macOS `..namedfork/rsrc` write (macOS only).
14//! - **SeparateRsrc** — data fork + a `name.rsrc` sidecar.
15//! - **DataForkOnly** — just the data fork.
16//!
17//! This mirrors the browse view's `extract_entry` per-file logic in one place
18//! so Commander gets identical output, and it works over the remote wire too
19//! (`RemoteFilesystem` implements `write_resource_fork_to` via the daemon's
20//! resource-fork read op).
21
22use std::path::Path;
23
24use anyhow::{Context, Result};
25
26use crate::binhex;
27use crate::resource_fork::{self, MacFileDates, ResourceForkMode};
28use disk_core::entry::FileEntry;
29use disk_core::filesystem::Filesystem;
30
31/// The default sanitized host filename for `entry` (no ProDOS type suffix).
32/// Callers with no special naming needs pass this as `out_name`.
33pub fn safe_name(entry: &FileEntry) -> String {
34    resource_fork::sanitize_filename(&entry.name)
35}
36
37/// The host path [`export_file_with_fork`] writes `entry`'s data to under
38/// `mode`, so a caller can see a collision before anything is overwritten.
39pub fn primary_output_path(
40    entry: &FileEntry,
41    dest_dir: &Path,
42    out_name: &str,
43    mode: ResourceForkMode,
44) -> std::path::PathBuf {
45    let has_rsrc = entry.resource_fork_size.map(|s| s > 0).unwrap_or(false);
46    match mode {
47        ResourceForkMode::MacBinary if has_rsrc => dest_dir.join(format!("{out_name}.bin")),
48        ResourceForkMode::BinHex => dest_dir.join(format!("{out_name}.hqx")),
49        _ => dest_dir.join(out_name),
50    }
51}
52
53/// Export one FILE `entry` into `dest_dir` under the base name `out_name`
54/// (already sanitized / suffixed by the caller — e.g. the browse view appends a
55/// ProDOS `#TTAAAA` suffix), using `mode`. Returns the number of bytes written
56/// (data + resource) for progress accounting. The caller handles directories
57/// (create + recurse). Use [`safe_name`] for the default sanitized name.
58pub fn export_file_with_fork(
59    fs: &mut dyn Filesystem,
60    entry: &FileEntry,
61    dest_dir: &Path,
62    out_name: &str,
63    mode: ResourceForkMode,
64) -> Result<u64> {
65    let opts = ForkExportOptions::default();
66    export_file_with_fork_opts(fs, entry, dest_dir, out_name, mode, opts)
67}
68
69/// Choices [`export_file_with_fork_opts`] adds to [`export_file_with_fork`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ForkExportOptions {
72    /// AppleDouble mode: write a `._name` sidecar for type / creator codes alone (no fork).
73    pub finder_info_sidecar: bool,
74}
75
76impl Default for ForkExportOptions {
77    fn default() -> Self {
78        ForkExportOptions {
79            finder_info_sidecar: true,
80        }
81    }
82}
83
84/// [`export_file_with_fork`] with its [`ForkExportOptions`] spelled out.
85pub fn export_file_with_fork_opts(
86    fs: &mut dyn Filesystem,
87    entry: &FileEntry,
88    dest_dir: &Path,
89    out_name: &str,
90    mode: ResourceForkMode,
91    opts: ForkExportOptions,
92) -> Result<u64> {
93    let safe = out_name;
94    let has_rsrc = entry.resource_fork_size.map(|s| s > 0).unwrap_or(false);
95    let type_code = entry.type_code.unwrap_or([0; 4]);
96    let creator_code = entry.creator_code.unwrap_or([0; 4]);
97    // HFS/HFS+ catalog dates are already raw Mac-1904 seconds — the exact
98    // MacBinary / AppleDouble encoding — so carry them straight through.
99    let dates = entry
100        .mac_dates
101        .map(|(created, modified, _backup)| MacFileDates { created, modified })
102        .unwrap_or_default();
103
104    let read_data = |fs: &mut dyn Filesystem| -> Result<Vec<u8>> {
105        fs.read_file(entry, usize::MAX)
106            .with_context(|| format!("reading '{}'", entry.name))
107    };
108    let read_rsrc = |fs: &mut dyn Filesystem| -> Result<Vec<u8>> {
109        let mut buf = Vec::new();
110        fs.write_resource_fork_to(entry, &mut buf)
111            .with_context(|| format!("reading resource fork of '{}'", entry.name))?;
112        Ok(buf)
113    };
114
115    if mode == ResourceForkMode::MacBinary && has_rsrc {
116        let data = read_data(fs)?;
117        let rsrc = read_rsrc(fs)?;
118        let mb =
119            resource_fork::build_macbinary(safe, &type_code, &creator_code, dates, &data, &rsrc);
120        let out = dest_dir.join(format!("{safe}.bin"));
121        std::fs::write(&out, &mb).with_context(|| format!("writing {}", out.display()))?;
122        return Ok(data.len() as u64 + rsrc.len() as u64);
123    }
124
125    if mode == ResourceForkMode::BinHex {
126        let data = read_data(fs)?;
127        let rsrc = if has_rsrc { read_rsrc(fs)? } else { Vec::new() };
128        let bh = binhex::BinHexFile {
129            name: entry.name.clone(),
130            type_code,
131            creator_code,
132            flags: 0,
133            data_fork: data,
134            resource_fork: rsrc,
135        };
136        let text = binhex::build_binhex(&bh);
137        let out = dest_dir.join(format!("{safe}.hqx"));
138        std::fs::write(&out, text.as_bytes())
139            .with_context(|| format!("writing {}", out.display()))?;
140        return Ok(bh.data_fork.len() as u64 + bh.resource_fork.len() as u64);
141    }
142
143    // Data fork to the plain name...
144    let out = dest_dir.join(safe);
145    let mut f =
146        std::fs::File::create(&out).with_context(|| format!("creating {}", out.display()))?;
147    let written = fs
148        .write_file_to(entry, &mut f)
149        .with_context(|| format!("extracting '{}'", entry.name))?;
150    drop(f);
151    // Preserve the source's mtime on the extracted host file when the
152    // source recorded one — a plain image->host extract now round-trips
153    // the date the same way tar's `-p` flag does. Best-effort: a
154    // filesystem that doesn't record dates leaves this None and the OS
155    // stamps the current time as before. Errors are non-fatal (some
156    // network filesystems / SMB shares reject setting mtime).
157    apply_host_mtime(&out, entry);
158
159    // ...plus a resource-fork sidecar for the fork-carrying "beside the data
160    // fork" modes. MacBinary-without-rsrc and DataForkOnly land here as a plain
161    // data-fork write with no sidecar.
162    let mut extra = 0u64;
163    let has_finfo = opts.finder_info_sidecar && (type_code != [0; 4] || creator_code != [0; 4]);
164    match mode {
165        ResourceForkMode::Native if has_rsrc => {
166            let rsrc = read_rsrc(fs)?;
167            let p = out.join("..namedfork/rsrc");
168            std::fs::write(&p, &rsrc)
169                .with_context(|| format!("writing native resource fork of '{}'", entry.name))?;
170            extra = rsrc.len() as u64;
171        }
172        ResourceForkMode::AppleDouble if has_rsrc || has_finfo => {
173            let rsrc = if has_rsrc { read_rsrc(fs)? } else { Vec::new() };
174            let ad = resource_fork::build_appledouble(&type_code, &creator_code, dates, &rsrc);
175            let p = dest_dir.join(format!("._{safe}"));
176            std::fs::write(&p, &ad)
177                .with_context(|| format!("writing AppleDouble sidecar of '{}'", entry.name))?;
178            extra = rsrc.len() as u64;
179        }
180        ResourceForkMode::SeparateRsrc if has_rsrc => {
181            let rsrc = read_rsrc(fs)?;
182            let p = dest_dir.join(format!("{safe}.rsrc"));
183            std::fs::write(&p, &rsrc)
184                .with_context(|| format!("writing .rsrc sidecar of '{}'", entry.name))?;
185            extra = rsrc.len() as u64;
186        }
187        _ => {}
188    }
189
190    Ok(written + extra)
191}
192
193/// Stamp the extracted host file's mtime/atime from `entry.modified_unix`.
194/// A best-effort call: filesystems that can't set times (network mounts,
195/// some FUSE-backed FSes) just leave the OS default, they don't fail the
196/// export.
197fn apply_host_mtime(path: &Path, entry: &FileEntry) {
198    let Some(secs) = entry.modified_unix else {
199        return;
200    };
201    let ft = filetime::FileTime::from_unix_time(secs as i64, 0);
202    let _ = filetime::set_file_times(path, ft, ft);
203}