1use std::fs::{self};
6use std::io;
7use std::os::unix::fs::{MetadataExt, PermissionsExt};
8use std::path::Path;
9
10#[derive(Debug, Default)]
12pub struct MkdirOptions {
13 pub parents: bool,
14 pub mode: Option<u32>,
15}
16
17pub fn mkdir(path: &Path, options: &MkdirOptions) -> Result<(), String> {
19 let mode = options.mode.unwrap_or(0o777);
20
21 if options.parents {
22 mkdir_parents(path, mode)
23 } else {
24 mkdir_single(path, mode)
25 }
26}
27
28fn mkdir_single(path: &Path, mode: u32) -> Result<(), String> {
29 #[cfg(unix)]
30 {
31 use std::ffi::CString;
32
33 let path_str = path.to_string_lossy();
34 let path_c = CString::new(path_str.as_bytes()).map_err(|e| e.to_string())?;
35
36 let result = unsafe { libc::mkdir(path_c.as_ptr(), mode as libc::mode_t) };
37 if result < 0 {
38 Err(format!(
39 "cannot make directory '{}': {}",
40 path.display(),
41 io::Error::last_os_error()
42 ))
43 } else {
44 Ok(())
45 }
46 }
47
48 #[cfg(not(unix))]
49 {
50 fs::create_dir(path)
51 .map_err(|e| format!("cannot make directory '{}': {}", path.display(), e))
52 }
53}
54
55fn mkdir_parents(path: &Path, mode: u32) -> Result<(), String> {
56 if path.exists() {
57 if path.is_dir() {
58 return Ok(());
59 }
60 return Err(format!(
61 "'{}' exists but is not a directory",
62 path.display()
63 ));
64 }
65
66 if let Some(parent) = path.parent() {
67 if !parent.as_os_str().is_empty() {
68 mkdir_parents(parent, mode | 0o300)?;
69 }
70 }
71
72 mkdir_single(path, mode)
73}
74
75pub fn rmdir(path: &Path) -> Result<(), String> {
77 fs::remove_dir(path).map_err(|e| format!("cannot remove directory '{}': {}", path.display(), e))
78}
79
80#[derive(Debug, Default)]
82pub struct LinkOptions {
83 pub symbolic: bool,
84 pub force: bool,
85 pub interactive: bool,
86 pub no_dereference: bool,
87 pub allow_dir: bool,
88}
89
90pub fn link(source: &Path, target: &Path, options: &LinkOptions) -> Result<(), String> {
92 let target_path = if target.is_dir() && !options.no_dereference {
93 let filename = source
94 .file_name()
95 .ok_or_else(|| "invalid source path".to_string())?;
96 target.join(filename)
97 } else {
98 target.to_path_buf()
99 };
100
101 if target_path.exists() {
102 if options.force {
103 fs::remove_file(&target_path)
104 .map_err(|e| format!("cannot remove '{}': {}", target_path.display(), e))?;
105 } else if !options.interactive {
106 return Err(format!("'{}' already exists", target_path.display()));
107 }
108 }
109
110 #[cfg(unix)]
111 {
112 if !options.allow_dir && source.is_dir() && !options.symbolic {
113 return Err(format!(
114 "'{}': hard link not allowed for directory",
115 source.display()
116 ));
117 }
118
119 if options.symbolic {
120 std::os::unix::fs::symlink(source, &target_path)
121 .map_err(|e| format!("cannot create symlink '{}': {}", target_path.display(), e))
122 } else {
123 fs::hard_link(source, &target_path)
124 .map_err(|e| format!("cannot create hard link '{}': {}", target_path.display(), e))
125 }
126 }
127
128 #[cfg(not(unix))]
129 {
130 fs::hard_link(source, &target_path)
131 .map_err(|e| format!("cannot create link '{}': {}", target_path.display(), e))
132 }
133}
134
135#[derive(Debug, Default)]
137pub struct MoveOptions {
138 pub force: bool,
139 pub interactive: bool,
140}
141
142pub fn mv(source: &Path, target: &Path, options: &MoveOptions) -> Result<(), String> {
144 let target_path = if target.is_dir() {
145 let filename = source
146 .file_name()
147 .ok_or_else(|| "invalid source path".to_string())?;
148 target.join(filename)
149 } else {
150 target.to_path_buf()
151 };
152
153 if target_path.exists() && !options.force && !options.interactive {
154 if target_path.is_dir() {
155 return Err(format!(
156 "'{}': cannot overwrite directory",
157 target_path.display()
158 ));
159 }
160 }
161
162 fs::rename(source, &target_path).map_err(|e| {
163 format!(
164 "cannot move '{}' to '{}': {}",
165 source.display(),
166 target_path.display(),
167 e
168 )
169 })
170}
171
172#[derive(Debug, Default)]
174pub struct RemoveOptions {
175 pub force: bool,
176 pub recursive: bool,
177 pub interactive: bool,
178 pub dir: bool,
179}
180
181pub fn rm(path: &Path, options: &RemoveOptions) -> Result<(), String> {
183 if !path.exists() {
184 if options.force {
185 return Ok(());
186 }
187 return Err(format!(
188 "cannot remove '{}': No such file or directory",
189 path.display()
190 ));
191 }
192
193 if path.is_dir() {
194 if options.recursive {
195 rm_recursive(path, options)
196 } else if options.dir {
197 fs::remove_dir(path).map_err(|e| format!("cannot remove '{}': {}", path.display(), e))
198 } else if !options.force {
199 Err(format!(
200 "cannot remove '{}': Is a directory",
201 path.display()
202 ))
203 } else {
204 Ok(())
205 }
206 } else {
207 fs::remove_file(path).map_err(|e| format!("cannot remove '{}': {}", path.display(), e))
208 }
209}
210
211fn rm_recursive(path: &Path, options: &RemoveOptions) -> Result<(), String> {
212 if path.is_dir() {
213 for entry in fs::read_dir(path)
214 .map_err(|e| format!("cannot read directory '{}': {}", path.display(), e))?
215 {
216 let entry = entry.map_err(|e| e.to_string())?;
217 rm_recursive(&entry.path(), options)?;
218 }
219 fs::remove_dir(path).map_err(|e| format!("cannot remove '{}': {}", path.display(), e))
220 } else {
221 fs::remove_file(path).map_err(|e| format!("cannot remove '{}': {}", path.display(), e))
222 }
223}
224
225pub fn chmod(path: &Path, mode: u32, recursive: bool) -> Result<(), String> {
227 #[cfg(unix)]
228 {
229 use std::ffi::CString;
230
231 let path_str = path.to_string_lossy();
232 let path_c = CString::new(path_str.as_bytes()).map_err(|e| e.to_string())?;
233
234 let result = unsafe { libc::chmod(path_c.as_ptr(), mode as libc::mode_t) };
235 if result < 0 {
236 return Err(format!(
237 "cannot change mode of '{}': {}",
238 path.display(),
239 io::Error::last_os_error()
240 ));
241 }
242
243 if recursive && path.is_dir() {
244 for entry in fs::read_dir(path)
245 .map_err(|e| format!("cannot read directory '{}': {}", path.display(), e))?
246 {
247 let entry = entry.map_err(|e| e.to_string())?;
248 chmod(&entry.path(), mode, true)?;
249 }
250 }
251
252 Ok(())
253 }
254
255 #[cfg(not(unix))]
256 {
257 Err("chmod not supported on this platform".to_string())
258 }
259}
260
261#[cfg(unix)]
263pub fn chown(
264 path: &Path,
265 uid: Option<u32>,
266 gid: Option<u32>,
267 recursive: bool,
268 no_dereference: bool,
269) -> Result<(), String> {
270 use std::ffi::CString;
271
272 let path_str = path.to_string_lossy();
273 let path_c = CString::new(path_str.as_bytes()).map_err(|e| e.to_string())?;
274
275 let uid = uid
276 .map(|u| u as libc::uid_t)
277 .unwrap_or(u32::MAX as libc::uid_t);
278 let gid = gid
279 .map(|g| g as libc::gid_t)
280 .unwrap_or(u32::MAX as libc::gid_t);
281
282 let result = if no_dereference {
283 unsafe { libc::lchown(path_c.as_ptr(), uid, gid) }
284 } else {
285 unsafe { libc::chown(path_c.as_ptr(), uid, gid) }
286 };
287
288 if result < 0 {
289 return Err(format!(
290 "cannot change owner of '{}': {}",
291 path.display(),
292 io::Error::last_os_error()
293 ));
294 }
295
296 if recursive && path.is_dir() {
297 for entry in fs::read_dir(path)
298 .map_err(|e| format!("cannot read directory '{}': {}", path.display(), e))?
299 {
300 let entry = entry.map_err(|e| e.to_string())?;
301 chown(&entry.path(), Some(uid), Some(gid), true, no_dereference)?;
302 }
303 }
304
305 Ok(())
306}
307
308#[cfg(unix)]
310pub fn get_uid(username: &str) -> Option<u32> {
311 use std::ffi::CString;
312
313 if let Ok(uid) = username.parse::<u32>() {
314 return Some(uid);
315 }
316
317 let username_c = CString::new(username).ok()?;
318 unsafe {
319 let pwd = libc::getpwnam(username_c.as_ptr());
320 if pwd.is_null() {
321 None
322 } else {
323 Some((*pwd).pw_uid)
324 }
325 }
326}
327
328#[cfg(unix)]
330pub fn get_gid(groupname: &str) -> Option<u32> {
331 use std::ffi::CString;
332
333 if let Ok(gid) = groupname.parse::<u32>() {
334 return Some(gid);
335 }
336
337 let groupname_c = CString::new(groupname).ok()?;
338 unsafe {
339 let grp = libc::getgrnam(groupname_c.as_ptr());
340 if grp.is_null() {
341 None
342 } else {
343 Some((*grp).gr_gid)
344 }
345 }
346}
347
348#[cfg(unix)]
350pub fn parse_chown_spec(spec: &str) -> Result<(Option<u32>, Option<u32>), String> {
351 let (user_part, group_part) = if let Some(pos) = spec.find(':') {
352 let (u, g) = spec.split_at(pos);
353 (u, Some(&g[1..]))
354 } else if let Some(pos) = spec.find('.') {
355 let (u, g) = spec.split_at(pos);
356 (u, Some(&g[1..]))
357 } else {
358 (spec, None)
359 };
360
361 let uid = if user_part.is_empty() {
362 None
363 } else {
364 Some(get_uid(user_part).ok_or_else(|| format!("{}: no such user", user_part))?)
365 };
366
367 let gid = match group_part {
368 Some(g) if g.is_empty() => {
369 if let Some(uid_val) = uid {
370 unsafe {
371 let pwd = libc::getpwuid(uid_val);
372 if pwd.is_null() {
373 return Err(format!("{}: no such user", user_part));
374 }
375 Some((*pwd).pw_gid)
376 }
377 } else {
378 None
379 }
380 }
381 Some(g) => Some(get_gid(g).ok_or_else(|| format!("{}: no such group", g))?),
382 None => None,
383 };
384
385 Ok((uid, gid))
386}
387
388pub fn sync_fs() {
390 #[cfg(unix)]
391 unsafe {
392 libc::sync();
393 }
394}
395
396pub fn mode_to_string(mode: u32) -> String {
398 let mut result = String::with_capacity(10);
399
400 let file_type = match mode & 0o170000 {
401 0o140000 => 's',
402 0o120000 => 'l',
403 0o100000 => '-',
404 0o060000 => 'b',
405 0o040000 => 'd',
406 0o020000 => 'c',
407 0o010000 => 'p',
408 _ => '?',
409 };
410 result.push(file_type);
411
412 let perms = [
413 (mode & 0o400 != 0, 'r'),
414 (mode & 0o200 != 0, 'w'),
415 (
416 mode & 0o100 != 0,
417 if mode & 0o4000 != 0 { 's' } else { 'x' },
418 ),
419 (mode & 0o040 != 0, 'r'),
420 (mode & 0o020 != 0, 'w'),
421 (
422 mode & 0o010 != 0,
423 if mode & 0o2000 != 0 { 's' } else { 'x' },
424 ),
425 (mode & 0o004 != 0, 'r'),
426 (mode & 0o002 != 0, 'w'),
427 (
428 mode & 0o001 != 0,
429 if mode & 0o1000 != 0 { 't' } else { 'x' },
430 ),
431 ];
432
433 for (set, ch) in perms {
434 if set {
435 result.push(ch);
436 } else if ch == 's' {
437 result.push('S');
438 } else if ch == 't' {
439 result.push('T');
440 } else {
441 result.push('-');
442 }
443 }
444
445 result
446}
447
448pub fn parse_mode(s: &str) -> Option<u32> {
450 u32::from_str_radix(s, 8).ok()
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use std::fs::File;
457 use std::io::Write;
458 use tempfile::TempDir;
459
460 #[test]
461 fn test_mkdir_single() {
462 let dir = TempDir::new().unwrap();
463 let new_dir = dir.path().join("newdir");
464
465 let options = MkdirOptions::default();
466 mkdir(&new_dir, &options).unwrap();
467
468 assert!(new_dir.exists());
469 assert!(new_dir.is_dir());
470 }
471
472 #[test]
473 fn test_mkdir_parents() {
474 let dir = TempDir::new().unwrap();
475 let deep_dir = dir.path().join("a/b/c/d");
476
477 let options = MkdirOptions {
478 parents: true,
479 ..Default::default()
480 };
481 mkdir(&deep_dir, &options).unwrap();
482
483 assert!(deep_dir.exists());
484 assert!(deep_dir.is_dir());
485 }
486
487 #[test]
488 fn test_rmdir() {
489 let dir = TempDir::new().unwrap();
490 let new_dir = dir.path().join("to_remove");
491
492 fs::create_dir(&new_dir).unwrap();
493 assert!(new_dir.exists());
494
495 rmdir(&new_dir).unwrap();
496 assert!(!new_dir.exists());
497 }
498
499 #[test]
500 fn test_rm_file() {
501 let dir = TempDir::new().unwrap();
502 let file_path = dir.path().join("test.txt");
503
504 {
505 let mut f = File::create(&file_path).unwrap();
506 f.write_all(b"test").unwrap();
507 }
508
509 let options = RemoveOptions::default();
510 rm(&file_path, &options).unwrap();
511 assert!(!file_path.exists());
512 }
513
514 #[test]
515 fn test_rm_recursive() {
516 let dir = TempDir::new().unwrap();
517 let sub_dir = dir.path().join("subdir");
518 fs::create_dir(&sub_dir).unwrap();
519
520 let file_path = sub_dir.join("test.txt");
521 {
522 let mut f = File::create(&file_path).unwrap();
523 f.write_all(b"test").unwrap();
524 }
525
526 let options = RemoveOptions {
527 recursive: true,
528 ..Default::default()
529 };
530 rm(&sub_dir, &options).unwrap();
531 assert!(!sub_dir.exists());
532 }
533
534 #[test]
535 fn test_mv() {
536 let dir = TempDir::new().unwrap();
537 let src = dir.path().join("source.txt");
538 let dst = dir.path().join("dest.txt");
539
540 {
541 let mut f = File::create(&src).unwrap();
542 f.write_all(b"content").unwrap();
543 }
544
545 let options = MoveOptions::default();
546 mv(&src, &dst, &options).unwrap();
547
548 assert!(!src.exists());
549 assert!(dst.exists());
550 }
551
552 #[test]
553 #[cfg(unix)]
554 fn test_link_hard() {
555 let dir = TempDir::new().unwrap();
556 let src = dir.path().join("source.txt");
557 let dst = dir.path().join("link.txt");
558
559 {
560 let mut f = File::create(&src).unwrap();
561 f.write_all(b"content").unwrap();
562 }
563
564 let options = LinkOptions::default();
565 link(&src, &dst, &options).unwrap();
566
567 assert!(dst.exists());
568 assert_eq!(
569 fs::metadata(&src).unwrap().ino(),
570 fs::metadata(&dst).unwrap().ino()
571 );
572 }
573
574 #[test]
575 #[cfg(unix)]
576 fn test_link_symbolic() {
577 let dir = TempDir::new().unwrap();
578 let src = dir.path().join("source.txt");
579 let dst = dir.path().join("symlink.txt");
580
581 {
582 let mut f = File::create(&src).unwrap();
583 f.write_all(b"content").unwrap();
584 }
585
586 let options = LinkOptions {
587 symbolic: true,
588 ..Default::default()
589 };
590 link(&src, &dst, &options).unwrap();
591
592 assert!(dst.is_symlink());
593 }
594
595 #[test]
596 #[cfg(unix)]
597 fn test_chmod() {
598 let dir = TempDir::new().unwrap();
599 let file_path = dir.path().join("test.txt");
600
601 {
602 let mut f = File::create(&file_path).unwrap();
603 f.write_all(b"test").unwrap();
604 }
605
606 chmod(&file_path, 0o755, false).unwrap();
607
608 let meta = fs::metadata(&file_path).unwrap();
609 assert_eq!(meta.mode() & 0o777, 0o755);
610 }
611
612 #[test]
613 fn test_mode_to_string() {
614 assert_eq!(mode_to_string(0o100644), "-rw-r--r--");
615 assert_eq!(mode_to_string(0o100755), "-rwxr-xr-x");
616 assert_eq!(mode_to_string(0o040755), "drwxr-xr-x");
617 assert_eq!(mode_to_string(0o120777), "lrwxrwxrwx");
618 }
619
620 #[test]
621 fn test_parse_mode() {
622 assert_eq!(parse_mode("755"), Some(0o755));
623 assert_eq!(parse_mode("644"), Some(0o644));
624 assert_eq!(parse_mode("777"), Some(0o777));
625 assert_eq!(parse_mode("invalid"), None);
626 }
627
628 #[test]
629 #[cfg(unix)]
630 fn test_get_uid() {
631 assert!(get_uid("root").is_some() || get_uid("0").is_some());
632 assert_eq!(get_uid("0"), Some(0));
633 }
634
635 #[test]
636 #[cfg(unix)]
637 fn test_parse_chown_spec() {
638 let result = parse_chown_spec("0:0");
639 assert!(result.is_ok());
640 let (uid, gid) = result.unwrap();
641 assert_eq!(uid, Some(0));
642 assert_eq!(gid, Some(0));
643 }
644}