1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(rename_all = "lowercase")]
19pub enum FixTool {
20 #[default]
22 Ntfsfix,
23 FsckNtfs,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct FixOptions {
30 pub tool: FixTool,
31 #[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
49pub fn fix(vol: &Volume) -> Result<()> {
51 fix_with_opts(vol, &FixOptions::default())
52}
53
54pub 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}