Skip to main content

ntfs_mac_core/
fix.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 kodephp contributors
3
4//! NTFS repair. `ntfsfix -d` unmounts the volume, clears the dirty
5//! flag and runs a light chkdsk-like pass. `fsck_ntfs` is a heavier
6//! alternative.
7
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12use crate::Volume;
13use crate::error::{Error, Result};
14use crate::runner::{RunOptions, run_expect_success};
15
16/// Which fix tool to use.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(rename_all = "lowercase")]
19pub enum FixTool {
20    /// Fast: `ntfsfix -d /dev/diskN`.
21    #[default]
22    Ntfsfix,
23    /// Slower, more thorough: `fsck_ntfs /dev/diskN`.
24    FsckNtfs,
25}
26
27/// Options for `fix`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct FixOptions {
30    pub tool: FixTool,
31    /// Pass the `-d` (drop dirty flag) to ntfsfix.
32    #[serde(default = "default_true")]
33    pub drop_dirty_flag: bool,
34}
35
36impl Default for FixOptions {
37    fn default() -> Self {
38        Self {
39            tool: FixTool::Ntfsfix,
40            drop_dirty_flag: true,
41        }
42    }
43}
44
45fn default_true() -> bool {
46    true
47}
48
49/// Run a repair pass on `vol`.
50pub fn fix(vol: &Volume) -> Result<()> {
51    fix_with_opts(vol, &FixOptions::default())
52}
53
54/// Run a repair pass with explicit options.
55pub fn fix_with_opts(vol: &Volume, opts: &FixOptions) -> Result<()> {
56    if vol.mounted {
57        return Err(Error::InvalidArgument(format!(
58            "volume `{}` is mounted at `{}`; unmount before running fix",
59            vol.device_identifier,
60            vol.mount_point.as_deref().unwrap_or("<unknown>")
61        )));
62    }
63    let dev_path = format!("/dev/{}", vol.device_identifier);
64    match opts.tool {
65        FixTool::Ntfsfix => {
66            let mut args: Vec<String> = Vec::new();
67            if opts.drop_dirty_flag {
68                args.push("-d".into());
69            }
70            args.push(dev_path);
71            let refs: Vec<&str> = args.iter().map(String::as_str).collect();
72            run_expect_success(
73                "ntfsfix",
74                &refs,
75                &RunOptions {
76                    timeout: Some(Duration::from_secs(1800)),
77                    ..Default::default()
78                },
79            )?;
80        }
81        FixTool::FsckNtfs => {
82            run_expect_success(
83                "fsck_ntfs",
84                &[dev_path.as_str()],
85                &RunOptions {
86                    timeout: Some(Duration::from_secs(3600)),
87                    ..Default::default()
88                },
89            )?;
90        }
91    }
92    Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn fix_refuses_mounted_volume() {
101        let vol = Volume {
102            device_identifier: "disk2s2".into(),
103            volume_name: "X".into(),
104            media_type: "com.microsoft.ntfs".into(),
105            uuid: None,
106            size_bytes: 0,
107            mounted: true,
108            mount_point: Some("/Volumes/X".into()),
109            parent_disk: None,
110            size_pretty: "0 B".into(),
111            location: "external".into(),
112            contents: None,
113        };
114        let r = fix(&vol);
115        assert!(matches!(r, Err(Error::InvalidArgument(_))));
116    }
117
118    #[test]
119    fn fix_tool_defaults_to_ntfsfix() {
120        let opts = FixOptions::default();
121        assert_eq!(opts.tool, FixTool::Ntfsfix);
122        assert!(opts.drop_dirty_flag);
123    }
124}