Skip to content

Commit

Permalink
Add support for seekdir().
Browse files Browse the repository at this point in the history
Fixes: #1222.
  • Loading branch information
nrath-js committed Dec 3, 2024
1 parent e38d2af commit 92fdac2
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/backend/libc/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ impl Dir {
unsafe { c::rewinddir(self.libc_dir.as_ptr()) }
}

/// `seekdir(self, offset)`
#[inline]
pub fn seekdir(&mut self, offset: i64) -> io::Result<()> {
self.any_errors = false;
unsafe { c::seekdir(self.libc_dir.as_ptr(), offset as c::c_long) }
Ok(())
}

/// `readdir(self)`, where `None` means the end of the directory.
pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
// If we've seen errors, don't continue to try to read anything further.
Expand Down Expand Up @@ -171,6 +179,9 @@ impl Dir {
#[cfg(not(any(freebsdlike, netbsdlike, target_os = "vita")))]
d_ino: dirent.d_ino,

#[cfg(any(linux_like))]
d_off: dirent.d_off,

#[cfg(any(freebsdlike, netbsdlike))]
d_fileno: dirent.d_fileno,

Expand Down Expand Up @@ -277,6 +288,9 @@ pub struct DirEntry {
d_fileno: c::ino_t,

name: CString,

#[cfg(any(linux_like))]
d_off: c::off_t,
}

impl DirEntry {
Expand All @@ -286,6 +300,15 @@ impl DirEntry {
&self.name
}

/// Returns the "offset" of this directory entry. Note that this is not
/// a true numerical offset but an opaque cookie that identifies a
/// position in the given stream.
#[cfg(any(linux_like))]
#[inline]
pub fn offset(&self) -> i64 {
self.d_off as i64
}

/// Returns the type of this directory entry.
#[cfg(not(any(
solarish,
Expand Down
42 changes: 40 additions & 2 deletions src/backend/linux_raw/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ impl Dir {
self.pos = self.buf.len();
}

/// `seekdir(self, offset)`
#[inline]
pub fn seekdir(&mut self, offset: i64) -> io::Result<()> {
self.any_errors = false;
match io::retry_on_intr(|| {
crate::backend::fs::syscalls::_seek(self.fd.as_fd(), offset, SEEK_SET)
}) {
Ok(_) => Ok(()),
Err(err) => {
self.any_errors = true;
Err(err)
}
}
}

/// `readdir(self)`, where `None` means the end of the directory.
pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
// If we've seen errors, don't continue to try to read anything further.
Expand Down Expand Up @@ -112,6 +127,7 @@ impl Dir {
let offsetof_d_reclen = (as_ptr(&z.d_reclen) as usize) - base;
let offsetof_d_name = (as_ptr(&z.d_name) as usize) - base;
let offsetof_d_ino = (as_ptr(&z.d_ino) as usize) - base;
let offsetof_d_off = (as_ptr(&z.d_off) as usize) - base;
let offsetof_d_type = (as_ptr(&z.d_type) as usize) - base;

// Test if we need more entries, and if so, read more.
Expand Down Expand Up @@ -145,7 +161,7 @@ impl Dir {
let name = name.to_owned();
assert!(name.as_bytes().len() <= self.buf.len() - name_start);

// Do an unaligned u64 load.
// Do an unaligned u64 load for `d_ino`.
let d_ino = u64::from_ne_bytes([
self.buf[pos + offsetof_d_ino],
self.buf[pos + offsetof_d_ino + 1],
Expand All @@ -157,19 +173,32 @@ impl Dir {
self.buf[pos + offsetof_d_ino + 7],
]);

// Do an unaligned i64 load for `d_off`
let d_off = i64::from_ne_bytes([
self.buf[pos + offsetof_d_off],
self.buf[pos + offsetof_d_off + 1],
self.buf[pos + offsetof_d_off + 2],
self.buf[pos + offsetof_d_off + 3],
self.buf[pos + offsetof_d_off + 4],
self.buf[pos + offsetof_d_off + 5],
self.buf[pos + offsetof_d_off + 6],
self.buf[pos + offsetof_d_off + 7],
]);

let d_type = self.buf[pos + offsetof_d_type];

// Check that our types correspond to the `linux_dirent64` types.
let _ = linux_dirent64 {
d_ino,
d_off: 0,
d_off,
d_type,
d_reclen,
d_name: Default::default(),
};

Some(Ok(DirEntry {
d_ino,
d_off,
d_type,
name,
}))
Expand Down Expand Up @@ -261,6 +290,7 @@ impl fmt::Debug for Dir {
pub struct DirEntry {
d_ino: u64,
d_type: u8,
d_off: i64,
name: CString,
}

Expand All @@ -271,6 +301,14 @@ impl DirEntry {
&self.name
}

/// Returns the "offset" of this directory entry. Note that this is not
/// a true numerical offset but an opaque cookie that identifies a
/// position in the given stream.
#[inline]
pub fn offset(&self) -> i64 {
self.d_off
}

/// Returns the type of this directory entry.
#[inline]
pub fn file_type(&self) -> FileType {
Expand Down
62 changes: 62 additions & 0 deletions tests/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,68 @@ fn test_dir_read_from() {
assert!(saw_cargo_toml);
}

#[cfg(any(linux_like))]
#[test]
fn test_dir_seek() {
use std::io::Write;

let tempdir = tempfile::tempdir().unwrap();

// Create many files so that we exhaust the readdir buffer at least once.
let count = 500;
let prefix = "file_with_a_very_long_name_to_make_sure_that_we_fill_up_the_buffer";
let test_string = "This is a test string.";
let mut filenames = Vec::<String>::with_capacity(count);
for i in 0..count {
let filename = format!("{}-{}.txt", prefix, i);
let mut file = std::fs::File::create(&tempdir.path().join(&filename)).unwrap();
filenames.push(filename);
file.write_all(test_string.as_bytes()).unwrap();
}

let t = rustix::fs::open(
tempdir.path(),
rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::empty(),
)
.unwrap();

let mut dir = rustix::fs::Dir::read_from(&t).unwrap();

// Read the first half of directory entries and record offset
for _ in 0..count / 2 {
dir.read().unwrap().unwrap();
}
let offset = dir.read().unwrap().unwrap().offset();

// Read the rest of the directory entries and record the names
let mut entries = Vec::new();
while let Some(entry) = dir.read() {
let entry = entry.unwrap();
entries.push(entry.file_name().to_string_lossy().into_owned());
}
assert!(entries.len() >= count / 2);

// Seek to the stored position
dir.seekdir(offset).unwrap();

// Confirm that we're getting the same results as before
let mut entries2 = Vec::new();
while let Some(entry) = dir.read() {
let entry = entry.unwrap();
entries2.push(entry.file_name().to_string_lossy().into_owned());
}

// On arm-linux, the order of entries can apparently change when seeking
// within the same directory stream?!
if cfg!(all(target_arch = "arm", target_os = "linux")) {
entries.sort();
entries2.sort();
}

assert_eq!(entries, entries2);
}

#[test]
fn test_dir_new() {
let t = rustix::fs::openat(
Expand Down

0 comments on commit 92fdac2

Please sign in to comment.