1use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12use crate::runner::{RunOptions, run_expect_success};
13use crate::{Config, DestructiveToken, Volume};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FormatOptions {
18 pub label: String,
20 #[serde(default = "default_quick")]
22 pub quick: bool,
23 #[serde(default = "default_sector_size")]
25 pub sector_size: u32,
26 #[serde(default = "default_cluster_size")]
28 pub cluster_size: u32,
29 #[serde(default)]
31 pub extra_args: Vec<String>,
32}
33
34impl Default for FormatOptions {
35 fn default() -> Self {
36 Self {
37 label: String::new(),
38 quick: true,
39 sector_size: 4096,
40 cluster_size: 4096,
41 extra_args: Vec::new(),
42 }
43 }
44}
45
46fn default_quick() -> bool {
47 true
48}
49fn default_sector_size() -> u32 {
50 4096
51}
52fn default_cluster_size() -> u32 {
53 4096
54}
55
56pub fn format(
59 vol: &Volume,
60 opts: &FormatOptions,
61 token: &DestructiveToken,
62 _cfg: &Config,
63) -> Result<()> {
64 if vol.device_identifier != token.device_identifier {
65 return Err(Error::ConfirmationMismatch {
66 expected: token.device_identifier.clone(),
67 actual: vol.device_identifier.clone(),
68 });
69 }
70 if !vol.device_identifier.starts_with("disk") {
71 return Err(Error::InvalidArgument(format!(
72 "refusing to format `{}` — identifier must start with 'disk'",
73 vol.device_identifier
74 )));
75 }
76
77 if opts.sector_size != 512 && opts.sector_size != 4096 {
79 return Err(Error::InvalidArgument(format!(
80 "invalid sector_size {}: expected 512 or 4096",
81 opts.sector_size
82 )));
83 }
84 if !matches!(
85 opts.cluster_size,
86 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536
87 ) {
88 return Err(Error::InvalidArgument(format!(
89 "invalid cluster_size {}: expected power-of-2 between 512 and 65536",
90 opts.cluster_size
91 )));
92 }
93
94 let (bin_name, bin_path) = match crate::runner::which("newfs_ntfs") {
96 Ok(p) => ("newfs_ntfs", p),
97 Err(_) => {
98 let p = crate::runner::which("mkntfs")?;
99 ("mkntfs", p)
100 }
101 };
102
103 let mut args: Vec<String> = Vec::new();
104 if opts.quick {
105 args.push("-F".into());
106 }
107 if opts.sector_size == 4096 {
108 args.push("-f".into());
109 args.push("4096".into());
110 }
111 if opts.cluster_size == 4096 {
112 args.push("-c".into());
113 args.push("4096".into());
114 }
115 if !opts.label.is_empty() {
116 args.push("-L".into());
117 args.push(opts.label.clone());
118 }
119 for a in &opts.extra_args {
120 args.push(a.clone());
121 }
122 args.push(format!("/dev/{}", vol.device_identifier));
123
124 let bin_str = bin_path.to_str().unwrap_or(bin_name);
125 let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
126
127 run_expect_success(
128 bin_str,
129 &arg_refs,
130 &RunOptions {
131 timeout: Some(Duration::from_secs(600)),
132 ..Default::default()
133 },
134 )?;
135 Ok(())
136}
137
138pub fn validate_label(label: &str) -> Option<String> {
141 if label.is_empty() {
142 return None;
143 }
144 if label.len() > 11 {
145 return Some(format!(
146 "NTFS volume labels are limited to 11 characters; your label is {} chars",
147 label.chars().count()
148 ));
149 }
150 if label.contains([':', '\\', '/', '*', '?', '"', '<', '>', '|']) {
151 return Some("label contains invalid NTFS characters: : \\ / * ? \" < > |".into());
152 }
153 None
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn validate_label_lengths() {
162 assert!(validate_label("MyData").is_none());
163 assert!(validate_label("ABCDEFGHIJK").is_none()); assert!(validate_label("ABCDEFGHIJKL").is_some()); assert!(validate_label("").is_none());
166 }
167
168 #[test]
169 fn validate_label_invalid_chars() {
170 assert!(validate_label("a:b").is_some());
171 assert!(validate_label("a\\b").is_some());
172 assert!(validate_label("a/b").is_some());
173 assert!(validate_label("a*b").is_some());
174 assert!(validate_label("a?b").is_some());
175 assert!(validate_label("a\"b").is_some());
176 assert!(validate_label("a<b").is_some());
177 assert!(validate_label("a>b").is_some());
178 assert!(validate_label("a|b").is_some());
179 }
180
181 #[test]
182 fn format_rejects_identifier_mismatch() {
183 let vol = Volume {
184 device_identifier: "disk2s2".into(),
185 volume_name: "X".into(),
186 media_type: "com.microsoft.ntfs".into(),
187 uuid: None,
188 size_bytes: 0,
189 mounted: false,
190 mount_point: None,
191 parent_disk: None,
192 size_pretty: "0 B".into(),
193 location: "external".into(),
194 contents: None,
195 };
196 let cfg = Config::default();
197 let opts = FormatOptions::default();
198 let token = DestructiveToken::new("disk999s1", "wrong");
199 let r = format(&vol, &opts, &token, &cfg);
200 assert!(matches!(r, Err(Error::ConfirmationMismatch { .. })));
201 }
202
203 #[test]
204 fn format_rejects_non_disk_identifier() {
205 let vol = Volume {
206 device_identifier: "notdisk".into(),
207 volume_name: "X".into(),
208 media_type: "com.microsoft.ntfs".into(),
209 uuid: None,
210 size_bytes: 0,
211 mounted: false,
212 mount_point: None,
213 parent_disk: None,
214 size_pretty: "0 B".into(),
215 location: "external".into(),
216 contents: None,
217 };
218 let cfg = Config::default();
219 let opts = FormatOptions::default();
220 let token = DestructiveToken::new("notdisk", "");
221 let r = format(&vol, &opts, &token, &cfg);
222 assert!(matches!(r, Err(Error::InvalidArgument(_))));
223 }
224
225 #[test]
226 fn format_rejects_invalid_sector_size() {
227 let vol = Volume {
228 device_identifier: "disk2s2".into(),
229 volume_name: "X".into(),
230 media_type: "com.microsoft.ntfs".into(),
231 uuid: None,
232 size_bytes: 0,
233 mounted: false,
234 mount_point: None,
235 parent_disk: None,
236 size_pretty: "0 B".into(),
237 location: "external".into(),
238 contents: None,
239 };
240 let cfg = Config::default();
241 let opts = FormatOptions {
242 sector_size: 2048, ..Default::default()
244 };
245 let token = DestructiveToken::new("disk2s2", "");
246 let r = format(&vol, &opts, &token, &cfg);
247 assert!(matches!(r, Err(Error::InvalidArgument(_))));
248 }
249
250 #[test]
251 fn format_rejects_invalid_cluster_size() {
252 let vol = Volume {
253 device_identifier: "disk2s2".into(),
254 volume_name: "X".into(),
255 media_type: "com.microsoft.ntfs".into(),
256 uuid: None,
257 size_bytes: 0,
258 mounted: false,
259 mount_point: None,
260 parent_disk: None,
261 size_pretty: "0 B".into(),
262 location: "external".into(),
263 contents: None,
264 };
265 let cfg = Config::default();
266 let opts = FormatOptions {
267 cluster_size: 1000, ..Default::default()
269 };
270 let token = DestructiveToken::new("disk2s2", "");
271 let r = format(&vol, &opts, &token, &cfg);
272 assert!(matches!(r, Err(Error::InvalidArgument(_))));
273 }
274}