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