1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
15#[serde(rename_all = "lowercase")]
16pub enum FixTool {
17 #[default]
19 Ntfsfix,
20 FsckNtfs,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct FixOptions {
27 pub tool: FixTool,
28 #[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
46pub fn fix(vol: &Volume) -> Result<()> {
48 fix_with_opts(vol, &FixOptions::default())
49}
50
51pub 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}