Skip to main content

ntfs_mac_core/
fix.rs

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