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