Skip to main content

ntfs_mac_core/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 kodephp contributors
3
4//! ntfs-mac core.
5//!
6//! Subprocess wrappers around `ntfs-3g`, `diskutil`, `hdiutil`,
7//! `newfs_ntfs`, `ntfsfix` and `fsck_ntfs`, plus a small config layer.
8//!
9//! Design rules (do not violate — they are the reason this library is
10//! kept under ~1.5k LOC):
11//!
12//! * No `unsafe`, no `unwrap` on production paths, no async runtime.
13//! * Every external command goes through [`runner::run`]; failures are
14//!   surfaced as [`error::Error`] with the underlying stderr attached.
15//! * Pure parsing helpers are pure and unit-testable; only the module
16//!   boundaries that touch `std::process::Command` are impure.
17//! * Destructive NTFS operations require an explicit [`DestructiveToken`]
18//!   to be matched before the API proceeds.
19//!
20//! Layout:
21//!
22//! ```text
23//! ntfs-mac-core
24//!   runner   Subprocess plumbing + timeouts
25//!   error    Typed errors, ExitStatus codes
26//!   config   ~/.config/ntfs-mac/config.toml
27//!   deps     Dependency discovery (ntfs-3g, macFUSE, FUSE-T, ...)
28//!   device   diskutil list -plist parsing
29//!   mount    ntfs-3g / diskutil mount and unmount
30//!   format   mkntfs / newfs_ntfs (destructive, gated)
31//!   fix      ntfsfix / fsck_ntfs
32//!   copy     rsync-based copy with progress
33//!   license  Licence metadata + embedded attribution inventory
34//! ```
35
36pub mod config;
37pub mod copy;
38pub mod daemon;
39pub mod deps;
40pub mod device;
41pub mod error;
42pub mod fix;
43pub mod format;
44pub mod hardening;
45pub mod license;
46pub mod mount;
47pub mod runner;
48
49pub use config::{Config, default_config_path, load_config, save_config};
50pub use device::Volume;
51pub use error::{ConfigError, Error, ExitCode, Result};
52pub use hardening::{
53    install_panic_hook, install_signal_handlers, is_stdin_tty, is_stdout_tty, should_exit,
54    validate_device_id, validate_label, validate_mount_point,
55};
56pub use license::{
57    LicenseSummary, ThirdPartyCounts, display_path, license_file, notice_file, third_party_counts,
58};
59pub use runner::{RunOptions, read_line_interactive, run_expect_success, which};
60
61/// Build-time constants surfaced to the CLI/GUI.
62pub const VERSION: &str = env!("CARGO_PKG_VERSION");
63pub const NAME: &str = "ntfs-mac";
64
65/// Destructive-operation token. Callers that confirm a `format` or
66/// `erase` action must pass a token whose `device_identifier` field
67/// matches the target device exactly.
68#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
69pub struct DestructiveToken {
70    /// Device identifier such as `disk2s2`.
71    pub device_identifier: String,
72    /// Volume label, when available. Empty string if the volume is unlabelled.
73    pub volume_label: String,
74}
75
76impl DestructiveToken {
77    /// Build a token for the given device + label.
78    pub fn new(device_identifier: impl Into<String>, volume_label: impl Into<String>) -> Self {
79        Self {
80            device_identifier: device_identifier.into(),
81            volume_label: volume_label.into(),
82        }
83    }
84
85    /// Human-readable confirmation phrase. Used in CLI prompts.
86    pub fn confirm_phrase(&self) -> String {
87        if self.volume_label.is_empty() {
88            format!("format {}", self.device_identifier)
89        } else {
90            format!("format {} {}", self.device_identifier, self.volume_label)
91        }
92    }
93
94    /// Validate that this token is usable as a confirmation: identifier
95    /// must be non-empty.
96    pub fn validate(&self) -> crate::error::Result<()> {
97        if self.device_identifier.trim().is_empty() {
98            return Err(Error::InvalidArgument(
99                "device_identifier cannot be empty".into(),
100            ));
101        }
102        Ok(())
103    }
104}