1#[cfg(feature = "alloc")]
2use alloc::string::String;
3#[cfg(feature = "alloc")]
4use alloc::vec::Vec;
5use core::mem::MaybeUninit;
6
7use rusl::error::Errno;
8pub use rusl::platform::Mode;
9use rusl::platform::{Dirent, OpenFlags, Stat, NULL_BYTE};
10use rusl::string::strlen::{buf_strlen, strlen};
11use rusl::string::unix_str::UnixStr;
12use rusl::unistd::UnlinkFlags;
13
14use crate::error::Error;
15use crate::error::Result;
16use crate::io::{Read, Write};
17use crate::unix::fd::{AsRawFd, BorrowedFd, OwnedFd, RawFd};
18
19#[cfg(test)]
20mod test;
21
22pub struct File(OwnedFd);
23
24impl File {
25 #[inline]
29 pub fn open(path: &UnixStr) -> Result<Self> {
30 Self::open_with_options(path, OpenOptions::new().read(true))
31 }
32
33 #[inline]
34 fn open_with_options(path: &UnixStr, opts: &OpenOptions) -> Result<Self> {
35 let flags =
36 OpenFlags::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? | opts.flags;
37 let fd = rusl::unistd::open_mode(path, flags, opts.mode)?;
38 Ok(File(OwnedFd(fd)))
39 }
40
41 fn open_at(dir_fd: RawFd, path: &UnixStr) -> Result<Self> {
42 let mut opts = OpenOptions::new();
43 opts.read(true);
44 let flags =
45 OpenFlags::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? | opts.flags;
46 let fd = rusl::unistd::open_at_mode(dir_fd, path, flags, opts.mode)?;
47 Ok(File(OwnedFd(fd)))
48 }
49
50 #[must_use]
55 pub const unsafe fn from_raw_fd(fd: RawFd) -> Self {
56 Self(OwnedFd::from_raw(fd))
57 }
58
59 #[inline]
65 pub fn set_nonblocking(&self) -> Result<()> {
66 self.0.set_nonblocking()
67 }
68
69 #[inline]
73 pub fn metadata(&self) -> Result<Metadata> {
74 let stat = rusl::unistd::stat_fd(self.as_raw_fd())?;
75 Ok(Metadata(stat))
76 }
77
78 #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
84 pub fn copy(&self, dest: &UnixStr) -> Result<Self> {
85 let this_metadata = self.metadata()?;
86 let dest = OpenOptions::new()
87 .create(true)
88 .write(true)
89 .mode(this_metadata.mode())
90 .open(dest)?;
91 let mut offset = 0;
92 let mut remaining = this_metadata.0.st_size as u64 - offset;
94 while remaining > 0 {
95 let w = rusl::unistd::copy_file_range(
96 self.as_raw_fd(),
97 offset,
98 dest.as_raw_fd(),
99 offset,
100 remaining as usize,
101 )?;
102 if w == 0 {
103 return Ok(dest);
104 }
105 offset += w as u64;
106 remaining = this_metadata.0.st_size as u64 - offset;
107 }
108 Ok(dest)
109 }
110}
111
112impl File {
113 #[inline]
114 pub(crate) fn into_inner(self) -> OwnedFd {
115 self.0
116 }
117}
118
119impl AsRawFd for File {
120 #[inline]
121 fn as_raw_fd(&self) -> RawFd {
122 self.0.as_raw_fd()
123 }
124}
125
126impl Read for File {
127 #[inline]
128 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
129 Ok(rusl::unistd::read(self.0 .0, buf)?)
130 }
131}
132
133#[cfg(feature = "alloc")]
137pub fn read(path: &UnixStr) -> Result<Vec<u8>> {
138 let mut file = File::open(path)?;
139 let mut bytes = Vec::new();
140 file.read_to_end(&mut bytes)?;
141 Ok(bytes)
142}
143
144#[cfg(feature = "alloc")]
148pub fn read_to_string(path: &UnixStr) -> Result<String> {
149 let mut file = File::open(path)?;
150 let mut string = String::new();
151 file.read_to_string(&mut string)?;
152 Ok(string)
153}
154
155pub fn write(path: &UnixStr, buf: &[u8]) -> Result<()> {
162 let mut file = OpenOptions::new()
163 .write(true)
164 .create(true)
165 .truncate(true)
166 .open(path)?;
167 file.write_all(buf)?;
168 Ok(())
169}
170#[derive(Debug, Clone)]
171pub struct Metadata(Stat);
172
173impl Metadata {
174 #[inline]
175 #[must_use]
176 pub fn is_dir(&self) -> bool {
177 Mode::from(self.0.st_mode) & Mode::S_IFMT == Mode::S_IFDIR
178 }
179
180 #[inline]
181 #[must_use]
182 pub fn is_file(&self) -> bool {
183 Mode::from(self.0.st_mode) & Mode::S_IFMT == Mode::S_IFREG
184 }
185
186 #[inline]
187 #[must_use]
188 pub fn is_symlink(&self) -> bool {
189 Mode::from(self.0.st_mode) & Mode::S_IFMT == Mode::S_IFLNK
190 }
191
192 #[inline]
193 #[must_use]
194 pub fn mode(&self) -> Mode {
195 Mode::from(self.0.st_mode)
196 }
197
198 #[inline]
199 #[must_use]
200 #[expect(clippy::len_without_is_empty, clippy::cast_sign_loss)]
201 pub fn len(&self) -> u64 {
202 self.0.st_size as u64
203 }
204}
205
206#[inline]
210pub fn metadata(path: &UnixStr) -> Result<Metadata> {
211 let res = rusl::unistd::stat(path)?;
212 Ok(Metadata(res))
213}
214
215#[inline]
220pub fn rename(src: &UnixStr, dest: &UnixStr) -> Result<()> {
221 rusl::unistd::rename(src, dest)?;
222 Ok(())
223}
224
225#[inline]
230pub fn copy_file(src: &UnixStr, dest: &UnixStr) -> Result<File> {
231 let src_file = File::open(src)?;
232 src_file.copy(dest)
233}
234
235pub fn exists(path: &UnixStr) -> Result<bool> {
240 match rusl::unistd::stat(path) {
241 Ok(_) => Ok(true),
242 Err(e) => {
243 if matches!(e.code, Some(Errno::ENOENT)) {
244 return Ok(false);
245 }
246 Err(Error::from(e))
247 }
248 }
249}
250
251#[inline]
255pub fn remove_file(path: &UnixStr) -> Result<()> {
256 rusl::unistd::unlink(path)?;
257 Ok(())
258}
259
260#[inline]
264pub fn create_dir(path: &UnixStr) -> Result<()> {
265 create_dir_mode(path, Mode::from(0o755))
266}
267
268#[inline]
272pub fn create_dir_mode(path: &UnixStr, mode: Mode) -> Result<()> {
273 rusl::unistd::mkdir(path, mode)?;
274 Ok(())
275}
276
277#[inline]
282pub fn create_dir_all(path: &UnixStr) -> Result<()> {
283 const NO_ALLOC_MAX_LEN: usize = 512;
286 const EMPTY: [MaybeUninit<u8>; NO_ALLOC_MAX_LEN] = [MaybeUninit::uninit(); NO_ALLOC_MAX_LEN];
287 unsafe {
288 let ptr = path.as_ptr();
291 let len = strlen(ptr);
292 if len == 0 {
293 return Err(Error::no_code(
294 "Can't create a directory with an empty name",
295 ));
296 }
297 #[cfg(feature = "alloc")]
298 if len > NO_ALLOC_MAX_LEN {
299 let mut owned: Vec<u8> = Vec::with_capacity(len);
300 ptr.copy_to(owned.as_mut_ptr(), len);
301 write_all_sub_paths(owned.as_mut_slice(), ptr)?;
302 return Ok(());
303 }
304 #[cfg(not(feature = "alloc"))]
305 if len > NO_ALLOC_MAX_LEN {
306 return Err(Error::no_code(
307 "Supplied path larger than 512 without an allocator present",
308 ));
309 }
310 let mut copied = EMPTY;
311 ptr.copy_to(copied.as_mut_ptr().cast(), len);
312 let initialized_section = copied[..len].as_mut_ptr().cast();
314 let buf: &mut [u8] = core::slice::from_raw_parts_mut(initialized_section, len);
315 write_all_sub_paths(buf, ptr)?;
316 }
317 Ok(())
318}
319
320#[inline]
321unsafe fn write_all_sub_paths(
322 buf: &mut [u8],
323 raw: *const u8,
324) -> core::result::Result<(), rusl::Error> {
325 let len = buf.len();
326 let mut it = 1;
327 loop {
328 let ind = len - it;
330 if ind == 0 {
331 break;
332 }
333
334 let byte = buf[ind];
335 if byte == b'/' {
336 buf[ind] = NULL_BYTE;
338
339 return match rusl::unistd::mkdir(
340 UnixStr::from_bytes_unchecked(&buf[..=ind]),
341 Mode::from(0o755),
342 ) {
343 Ok(()) => {
345 buf[ind] = b'/';
347 for i in ind + 1..len {
348 if buf[i] == b'/' {
350 buf[i] = NULL_BYTE;
352 rusl::unistd::mkdir(
353 UnixStr::from_bytes_unchecked(&buf[..=i]),
354 Mode::from(0o755),
355 )?;
356 buf[i] = b'/';
358 }
359 }
360 if unsafe { raw.add(len - 1).read() } == b'/' {
362 return Ok(());
363 }
364 rusl::unistd::mkdir(
366 UnixStr::from_bytes_unchecked(core::slice::from_raw_parts(raw, len + 1)),
367 Mode::from(0o755),
368 )?;
369 Ok(())
370 }
371 Err(e) => {
372 if let Some(code) = e.code {
373 if code == Errno::ENOENT {
374 it += 1;
375 buf[ind] = b'/';
378 continue;
379 } else if code == Errno::EEXIST {
380 return Ok(());
381 }
382 }
383 Err(e)
384 }
385 };
386 }
387 it += 1;
388 }
389 Ok(())
390}
391
392pub struct Directory(OwnedFd);
393
394impl Directory {
395 #[inline]
399 pub fn open(path: &UnixStr) -> Result<Directory> {
400 let fd = rusl::unistd::open(path, OpenFlags::O_CLOEXEC | OpenFlags::O_RDONLY)?;
401 Ok(Directory(OwnedFd(fd)))
402 }
403
404 #[inline]
405 fn open_at(dir_fd: RawFd, path: &UnixStr) -> Result<Directory> {
406 let fd = rusl::unistd::open_at(dir_fd, path, OpenFlags::O_CLOEXEC | OpenFlags::O_RDONLY)?;
407 Ok(Directory(OwnedFd(fd)))
408 }
409
410 #[must_use]
416 pub fn read<'a>(&self) -> ReadDir<'a> {
417 let buf = [0u8; 512];
418 ReadDir {
419 fd: BorrowedFd::new(self.0 .0),
420 filled_buf: buf,
421 offset: 0,
422 read_size: 0,
423 eod: false,
424 }
425 }
426
427 pub fn remove_all(&self) -> Result<()> {
432 for sub_dir in self.read() {
433 let sub_dir = sub_dir?;
434 if FileType::Directory == sub_dir.file_type() {
435 if sub_dir.is_relative_reference() {
436 continue;
437 }
438 let fname = sub_dir.file_unix_name()?;
439 let next = Self::open_at(self.0 .0, fname)?;
440 next.remove_all()?;
441 rusl::unistd::unlink_at(self.0 .0, fname, UnlinkFlags::at_removedir())?;
442 } else {
443 rusl::unistd::unlink_at(
444 self.0 .0,
445 sub_dir.file_unix_name()?,
446 UnlinkFlags::empty(),
447 )?;
448 }
449 }
450 Ok(())
451 }
452}
453
454pub struct ReadDir<'a> {
455 fd: BorrowedFd<'a>,
456 filled_buf: [u8; 512],
458 offset: usize,
459 read_size: usize,
460 eod: bool,
461}
462
463impl<'a> Iterator for ReadDir<'a> {
464 type Item = Result<DirEntry<'a>>;
465
466 fn next(&mut self) -> Option<Self::Item> {
467 if self.read_size == self.offset {
468 if self.eod {
469 return None;
470 }
471 match rusl::unistd::get_dents(self.fd.fd, &mut self.filled_buf) {
472 Ok(read) => {
473 if read == 0 {
474 self.eod = true;
475 return None;
476 }
477 self.read_size = read;
478 self.offset = 0;
480 }
481 Err(e) => {
482 self.eod = true;
483 return Some(Err(e.into()));
484 }
485 }
486 }
487 unsafe {
488 Dirent::try_from_bytes(&self.filled_buf[self.offset..]).map(|de| {
489 self.offset += de.d_reclen as usize;
490 Ok(DirEntry {
491 inner: de,
492 fd: self.fd,
493 })
494 })
495 }
496 }
497}
498
499#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
500pub enum FileType {
501 Fifo,
502 CharDevice,
503 Directory,
504 BlockDevice,
505 RegularFile,
506 Symlink,
507 Socket,
508 Unknown,
509}
510
511pub struct DirEntry<'a> {
512 inner: Dirent,
513 fd: BorrowedFd<'a>,
514}
515
516impl DirEntry<'_> {
517 #[must_use]
519 #[inline]
520 pub fn is_relative_reference(&self) -> bool {
521 &self.inner.d_name[..2] == b".\0" || &self.inner.d_name[..3] == b"..\0"
522 }
523
524 #[must_use]
525 pub fn file_type(&self) -> FileType {
526 match self.inner.d_type {
527 rusl::platform::DirType::DT_FIFO => FileType::Fifo,
528 rusl::platform::DirType::DT_CHR => FileType::CharDevice,
529 rusl::platform::DirType::DT_DIR => FileType::Directory,
530 rusl::platform::DirType::DT_BLK => FileType::BlockDevice,
531 rusl::platform::DirType::DT_REG => FileType::RegularFile,
532 rusl::platform::DirType::DT_LNK => FileType::Symlink,
533 rusl::platform::DirType::DT_SOCK => FileType::Socket,
534 _ => FileType::Unknown,
535 }
536 }
537
538 pub fn file_name(&self) -> Result<&str> {
542 let len = buf_strlen(&self.inner.d_name)?;
543 let as_str = unsafe { core::str::from_utf8(self.inner.d_name.get_unchecked(..len)) }
546 .map_err(|_| Error::no_code("File name not utf8"))?;
547 Ok(as_str)
548 }
549
550 pub fn file_unix_name(&self) -> Result<&UnixStr> {
554 let len = buf_strlen(&self.inner.d_name)?;
555 unsafe {
556 let tgt = self.inner.d_name.get_unchecked(..=len);
559 Ok(&*(core::ptr::from_ref::<[u8]>(tgt) as *const UnixStr))
561 }
562 }
563
564 #[inline]
569 pub fn open_file(&self) -> Result<File> {
570 let ft = self.file_type();
571 if ft == FileType::RegularFile || ft == FileType::CharDevice {
572 Ok(File::open_at(self.fd.fd, self.file_unix_name()?)?)
573 } else {
574 Err(Error::no_code("Tried to open non-file as file"))
575 }
576 }
577
578 #[inline]
583 pub fn open_dir(&self) -> Result<Directory> {
584 if self.file_type() == FileType::Directory {
585 Ok(Directory::open_at(self.fd.fd, self.file_unix_name()?)?)
586 } else {
587 Err(Error::no_code("Tried to open non-file as file"))
588 }
589 }
590}
591
592#[inline]
596pub fn remove_dir(path: &UnixStr) -> Result<()> {
597 rusl::unistd::unlink_flags(path, UnlinkFlags::at_removedir())?;
598 Ok(())
599}
600
601pub fn remove_dir_all(path: &UnixStr) -> Result<()> {
606 let dir = Directory::open(path)?;
607 dir.remove_all()?;
608 remove_dir(path)
609}
610
611impl Write for File {
612 #[inline]
613 fn write(&mut self, buf: &[u8]) -> Result<usize> {
614 Ok(rusl::unistd::write(self.0 .0, buf)?)
615 }
616
617 #[inline]
618 fn flush(&mut self) -> Result<()> {
619 Ok(())
620 }
621}
622
623#[derive(Clone, Debug)]
624#[expect(clippy::struct_excessive_bools)]
625pub struct OpenOptions {
626 read: bool,
627 write: bool,
628 append: bool,
629 truncate: bool,
630 create: bool,
631 create_new: bool,
632 flags: OpenFlags,
633 mode: Mode,
634}
635
636impl Default for OpenOptions {
637 #[inline]
638 fn default() -> Self {
639 Self::new()
640 }
641}
642
643impl OpenOptions {
644 #[must_use]
645 pub fn new() -> OpenOptions {
646 OpenOptions {
647 read: false,
649 write: false,
650 append: false,
651 truncate: false,
652 create: false,
653 create_new: false,
654 flags: OpenFlags::empty(),
656 mode: Mode::from(0o0_000_666),
657 }
658 }
659
660 pub fn read(&mut self, read: bool) -> &mut Self {
661 self.read = read;
662 self
663 }
664 pub fn write(&mut self, write: bool) -> &mut Self {
665 self.write = write;
666 self
667 }
668 pub fn append(&mut self, append: bool) -> &mut Self {
669 self.append = append;
670 self
671 }
672 pub fn truncate(&mut self, truncate: bool) -> &mut Self {
673 self.truncate = truncate;
674 self
675 }
676 pub fn create(&mut self, create: bool) -> &mut Self {
677 self.create = create;
678 self
679 }
680 pub fn create_new(&mut self, create_new: bool) -> &mut Self {
681 self.create_new = create_new;
682 self
683 }
684
685 pub fn custom_flags(&mut self, flags: OpenFlags) -> &mut Self {
686 self.flags = flags;
687 self
688 }
689 pub fn mode(&mut self, mode: Mode) -> &mut Self {
690 self.mode = mode;
691 self
692 }
693 #[inline]
697 pub fn open(&self, path: &UnixStr) -> Result<File> {
698 File::open_with_options(path, self)
699 }
700
701 fn get_access_mode(&self) -> Result<OpenFlags> {
702 match (self.read, self.write, self.append) {
703 (true, false, false) => Ok(OpenFlags::O_RDONLY),
704 (false, true, false) => Ok(OpenFlags::O_WRONLY),
705 (true, true, false) => Ok(OpenFlags::O_RDWR),
706 (false, _, true) => Ok(OpenFlags::O_WRONLY | OpenFlags::O_APPEND),
707 (true, _, true) => Ok(OpenFlags::O_RDWR | OpenFlags::O_APPEND),
708 (false, false, false) => Err(Error::no_code(
709 "Bad OpenOptions, no access mode (read, write, append)",
710 )),
711 }
712 }
713
714 fn get_creation_mode(&self) -> crate::error::Result<OpenFlags> {
715 match (self.write, self.append) {
716 (true, false) => {}
717 (false, false) => {
718 if self.truncate || self.create || self.create_new {
719 return Err(Error::no_code("Bad OpenOptions, used truncate, create, or create_new without access mode write or append"));
720 }
721 }
722 (_, true) => {
723 if self.truncate && !self.create_new {
724 return Err(Error::no_code(
725 "Bad OpenOptions, used truncate without create_new with access mode append",
726 ));
727 }
728 }
729 }
730
731 Ok(match (self.create, self.truncate, self.create_new) {
732 (false, false, false) => OpenFlags::empty(),
733 (true, false, false) => OpenFlags::O_CREAT,
734 (false, true, false) => OpenFlags::O_TRUNC,
735 (true, true, false) => OpenFlags::O_CREAT | OpenFlags::O_TRUNC,
736 (_, _, true) => OpenFlags::O_CREAT | OpenFlags::O_EXCL,
737 })
738 }
739}