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