aboutsummaryrefslogtreecommitdiff
path: root/src/read_dir.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/read_dir.rs')
-rw-r--r--src/read_dir.rs150
1 files changed, 150 insertions, 0 deletions
diff --git a/src/read_dir.rs b/src/read_dir.rs
new file mode 100644
index 0000000..eb8a549
--- /dev/null
+++ b/src/read_dir.rs
@@ -0,0 +1,150 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use std::ffi::CStr;
+use std::io;
+use std::mem::size_of;
+use std::os::unix::io::AsRawFd;
+
+use data_model::DataInit;
+
+use crate::syscall;
+
+#[repr(C, packed)]
+#[derive(Clone, Copy)]
+struct LinuxDirent64 {
+ d_ino: libc::ino64_t,
+ d_off: libc::off64_t,
+ d_reclen: libc::c_ushort,
+ d_ty: libc::c_uchar,
+}
+unsafe impl DataInit for LinuxDirent64 {}
+
+pub struct DirEntry<'r> {
+ pub ino: libc::ino64_t,
+ pub offset: u64,
+ pub type_: u8,
+ pub name: &'r CStr,
+}
+
+pub struct ReadDir<'d, D> {
+ buf: [u8; 256],
+ dir: &'d mut D,
+ current: usize,
+ end: usize,
+}
+
+impl<'d, D: AsRawFd> ReadDir<'d, D> {
+ /// Return the next directory entry. This is implemented as a separate method rather than via
+ /// the `Iterator` trait because rust doesn't currently support generic associated types.
+ #[allow(clippy::should_implement_trait)]
+ pub fn next(&mut self) -> Option<io::Result<DirEntry>> {
+ if self.current >= self.end {
+ let res = syscall!(unsafe {
+ libc::syscall(
+ libc::SYS_getdents64,
+ self.dir.as_raw_fd(),
+ self.buf.as_mut_ptr() as *mut LinuxDirent64,
+ self.buf.len() as libc::c_int,
+ )
+ });
+ match res {
+ Ok(end) => {
+ self.current = 0;
+ self.end = end as usize;
+ }
+ Err(e) => return Some(Err(e)),
+ }
+ }
+
+ let rem = &self.buf[self.current..self.end];
+ if rem.is_empty() {
+ return None;
+ }
+
+ // We only use debug asserts here because these values are coming from the kernel and we
+ // trust them implicitly.
+ debug_assert!(
+ rem.len() >= size_of::<LinuxDirent64>(),
+ "not enough space left in `rem`"
+ );
+
+ let (front, back) = rem.split_at(size_of::<LinuxDirent64>());
+
+ let dirent64 =
+ LinuxDirent64::from_slice(front).expect("unable to get LinuxDirent64 from slice");
+
+ let namelen = dirent64.d_reclen as usize - size_of::<LinuxDirent64>();
+ debug_assert!(namelen <= back.len(), "back is smaller than `namelen`");
+
+ // The kernel will pad the name with additional nul bytes until it is 8-byte aligned so
+ // we need to strip those off here.
+ let name = strip_padding(&back[..namelen]);
+ let entry = DirEntry {
+ ino: dirent64.d_ino,
+ offset: dirent64.d_off as u64,
+ type_: dirent64.d_ty,
+ name,
+ };
+
+ debug_assert!(
+ rem.len() >= dirent64.d_reclen as usize,
+ "rem is smaller than `d_reclen`"
+ );
+ self.current += dirent64.d_reclen as usize;
+ Some(Ok(entry))
+ }
+}
+
+pub fn read_dir<D: AsRawFd>(dir: &mut D, offset: libc::off64_t) -> io::Result<ReadDir<D>> {
+ // Safe because this doesn't modify any memory and we check the return value.
+ syscall!(unsafe { libc::lseek64(dir.as_raw_fd(), offset, libc::SEEK_SET) })?;
+
+ Ok(ReadDir {
+ buf: [0u8; 256],
+ dir,
+ current: 0,
+ end: 0,
+ })
+}
+
+// Like `CStr::from_bytes_with_nul` but strips any bytes after the first '\0'-byte. Panics if `b`
+// doesn't contain any '\0' bytes.
+fn strip_padding(b: &[u8]) -> &CStr {
+ // It would be nice if we could use memchr here but that's locked behind an unstable gate.
+ let pos = b
+ .iter()
+ .position(|&c| c == 0)
+ .expect("`b` doesn't contain any nul bytes");
+
+ // Safe because we are creating this string with the first nul-byte we found so we can
+ // guarantee that it is nul-terminated and doesn't contain any interior nuls.
+ unsafe { CStr::from_bytes_with_nul_unchecked(&b[..pos + 1]) }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn padded_cstrings() {
+ assert_eq!(strip_padding(b".\0\0\0\0\0\0\0").to_bytes(), b".");
+ assert_eq!(strip_padding(b"..\0\0\0\0\0\0").to_bytes(), b"..");
+ assert_eq!(
+ strip_padding(b"normal cstring\0").to_bytes(),
+ b"normal cstring"
+ );
+ assert_eq!(strip_padding(b"\0\0\0\0").to_bytes(), b"");
+ assert_eq!(
+ strip_padding(b"interior\0nul bytes\0\0\0").to_bytes(),
+ b"interior"
+ );
+ }
+
+ #[test]
+ #[should_panic(expected = "`b` doesn't contain any nul bytes")]
+ fn no_nul_byte() {
+ strip_padding(b"no nul bytes in string");
+ }
+}