Implement fd_readdir (#116)
* Reimpleent fd_readdir on Linux using quasi-nix. * Implement fd_readdir on Windows. * wip * Adapt to upstream changes. * Cleanup dir.rs * Address review * Fix macos build * host -> wasi * Partially address review, more to come later. * Address more review comments * Fix compilation on Windows
This commit is contained in:
committed by
Jakub Konka
parent
22494057df
commit
12972c7fd3
1
build.rs
1
build.rs
@@ -197,6 +197,7 @@ mod wasm_tests {
|
|||||||
"dangling_symlink" => true,
|
"dangling_symlink" => true,
|
||||||
"symlink_loop" => true,
|
"symlink_loop" => true,
|
||||||
"truncation_rights" => true,
|
"truncation_rights" => true,
|
||||||
|
"path_rename_trailing_slashes" => true,
|
||||||
"fd_readdir" => true,
|
"fd_readdir" => true,
|
||||||
"poll_oneoff" => true,
|
"poll_oneoff" => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use failure::Fail;
|
|||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::num::TryFromIntError;
|
use std::num::TryFromIntError;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Fail, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Fail, Eq, PartialEq)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
@@ -143,6 +144,12 @@ impl From<Infallible> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<str::Utf8Error> for Error {
|
||||||
|
fn from(_: str::Utf8Error) -> Self {
|
||||||
|
Self::Wasi(WasiError::EILSEQ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
impl From<winx::winerror::WinError> for Error {
|
impl From<winx::winerror::WinError> for Error {
|
||||||
fn from(err: winx::winerror::WinError) -> Self {
|
fn from(err: winx::winerror::WinError) -> Self {
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ use crate::sys::{host_impl, hostcalls_impl};
|
|||||||
use crate::{helpers, host, wasi, wasi32, Error, Result};
|
use crate::{helpers, host, wasi, wasi32, Error, Result};
|
||||||
use filetime::{set_file_handle_times, FileTime};
|
use filetime::{set_file_handle_times, FileTime};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::mem;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub(crate) unsafe fn fd_close(wasi_ctx: &mut WasiCtx, fd: wasi::__wasi_fd_t) -> Result<()> {
|
pub(crate) unsafe fn fd_close(wasi_ctx: &mut WasiCtx, fd: wasi::__wasi_fd_t) -> Result<()> {
|
||||||
@@ -1046,3 +1048,44 @@ impl FileType {
|
|||||||
*self as wasi::__wasi_filetype_t
|
*self as wasi::__wasi_filetype_t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct Dirent {
|
||||||
|
pub name: String,
|
||||||
|
pub ftype: FileType,
|
||||||
|
pub ino: u64,
|
||||||
|
pub cookie: wasi::__wasi_dircookie_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dirent {
|
||||||
|
#![allow(unused)] // temporarily, until BSD catches up with this change
|
||||||
|
/// Serialize the directory entry to the format define by `__wasi_fd_readdir`,
|
||||||
|
/// so that the serialized entries can be concatenated by the implementation.
|
||||||
|
pub fn to_wasi_raw(&self) -> Result<Vec<u8>> {
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
|
let name = self.name.as_bytes();
|
||||||
|
let namlen = name.len();
|
||||||
|
let dirent_size = mem::size_of::<wasi::__wasi_dirent_t>();
|
||||||
|
let offset = dirent_size.checked_add(namlen).ok_or(Error::EOVERFLOW)?;
|
||||||
|
|
||||||
|
let mut raw = Vec::<u8>::with_capacity(offset);
|
||||||
|
raw.resize(offset, 0);
|
||||||
|
|
||||||
|
let sys_dirent = raw.as_mut_ptr() as *mut wasi::__wasi_dirent_t;
|
||||||
|
unsafe {
|
||||||
|
*sys_dirent = wasi::__wasi_dirent_t {
|
||||||
|
d_namlen: namlen.try_into()?,
|
||||||
|
d_ino: self.ino,
|
||||||
|
d_next: self.cookie,
|
||||||
|
d_type: self.ftype.to_wasi(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let sys_name = unsafe { sys_dirent.offset(1) as *mut u8 };
|
||||||
|
let sys_name = unsafe { slice::from_raw_parts_mut(sys_name, namlen) };
|
||||||
|
sys_name.copy_from_slice(&name);
|
||||||
|
|
||||||
|
Ok(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
216
src/sys/unix/dir.rs
Normal file
216
src/sys/unix/dir.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// Based on src/dir.rs from nix
|
||||||
|
#![allow(unused)] // temporarily, until BSD catches up with this change
|
||||||
|
use crate::hostcalls_impl::FileType;
|
||||||
|
use libc;
|
||||||
|
use nix::{errno::Errno, Error, Result};
|
||||||
|
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
|
||||||
|
use std::{ffi, ptr};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use libc::{dirent64 as dirent, readdir64_r as readdir_r};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
use libc::{dirent, readdir_r};
|
||||||
|
|
||||||
|
/// An open directory.
|
||||||
|
///
|
||||||
|
/// This is a lower-level interface than `std::fs::ReadDir`. Notable differences:
|
||||||
|
/// * can be opened from a file descriptor (as returned by `openat`, perhaps before knowing
|
||||||
|
/// if the path represents a file or directory).
|
||||||
|
/// * implements `AsRawFd`, so it can be passed to `fstat`, `openat`, etc.
|
||||||
|
/// The file descriptor continues to be owned by the `Dir`, so callers must not keep a `RawFd`
|
||||||
|
/// after the `Dir` is dropped.
|
||||||
|
/// * can be iterated through multiple times without closing and reopening the file
|
||||||
|
/// descriptor. Each iteration rewinds when finished.
|
||||||
|
/// * returns entries for `.` (current directory) and `..` (parent directory).
|
||||||
|
/// * returns entries' names as a `CStr` (no allocation or conversion beyond whatever libc
|
||||||
|
/// does).
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub(crate) struct Dir(ptr::NonNull<libc::DIR>);
|
||||||
|
|
||||||
|
impl Dir {
|
||||||
|
/// Converts from a descriptor-based object, closing the descriptor on success or failure.
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn from<F: IntoRawFd>(fd: F) -> Result<Self> {
|
||||||
|
unsafe { Self::from_fd(fd.into_raw_fd()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts from a file descriptor, closing it on success or failure.
|
||||||
|
unsafe fn from_fd(fd: RawFd) -> Result<Self> {
|
||||||
|
let d = libc::fdopendir(fd);
|
||||||
|
if d.is_null() {
|
||||||
|
let e = Error::last();
|
||||||
|
libc::close(fd);
|
||||||
|
return Err(e);
|
||||||
|
};
|
||||||
|
// Always guaranteed to be non-null by the previous check
|
||||||
|
Ok(Self(ptr::NonNull::new(d).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the position of the directory stream, see `seekdir(3)`.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
pub(crate) fn seek(&mut self, loc: SeekLoc) {
|
||||||
|
unsafe { libc::seekdir(self.0.as_ptr(), loc.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset directory stream, see `rewinddir(3)`.
|
||||||
|
pub(crate) fn rewind(&mut self) {
|
||||||
|
unsafe { libc::rewinddir(self.0.as_ptr()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current position in the directory stream.
|
||||||
|
///
|
||||||
|
/// If this location is given to `Dir::seek`, the entries up to the previously returned
|
||||||
|
/// will be omitted and the iteration will start from the currently pending directory entry.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn tell(&self) -> SeekLoc {
|
||||||
|
let loc = unsafe { libc::telldir(self.0.as_ptr()) };
|
||||||
|
SeekLoc(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `Dir` is not `Sync`. With the current implementation, it could be, but according to
|
||||||
|
// https://www.gnu.org/software/libc/manual/html_node/Reading_002fClosing-Directory.html,
|
||||||
|
// future versions of POSIX are likely to obsolete `readdir_r` and specify that it's unsafe to
|
||||||
|
// call `readdir` simultaneously from multiple threads.
|
||||||
|
//
|
||||||
|
// `Dir` is safe to pass from one thread to another, as it's not reference-counted.
|
||||||
|
unsafe impl Send for Dir {}
|
||||||
|
|
||||||
|
impl AsRawFd for Dir {
|
||||||
|
fn as_raw_fd(&self) -> RawFd {
|
||||||
|
unsafe { libc::dirfd(self.0.as_ptr()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Dir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { libc::closedir(self.0.as_ptr()) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct IntoIter(Dir);
|
||||||
|
impl Iterator for IntoIter {
|
||||||
|
type Item = Result<Entry>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
unsafe {
|
||||||
|
// Note: POSIX specifies that portable applications should dynamically allocate a
|
||||||
|
// buffer with room for a `d_name` field of size `pathconf(..., _PC_NAME_MAX)` plus 1
|
||||||
|
// for the NUL byte. It doesn't look like the std library does this; it just uses
|
||||||
|
// fixed-sized buffers (and libc's dirent seems to be sized so this is appropriate).
|
||||||
|
// Probably fine here too then.
|
||||||
|
//
|
||||||
|
// See `impl Iterator for ReadDir` [1] for more details.
|
||||||
|
// [1] https://github.com/rust-lang/rust/blob/master/src/libstd/sys/unix/fs.rs
|
||||||
|
let mut ent = std::mem::MaybeUninit::<dirent>::uninit();
|
||||||
|
let mut result = ptr::null_mut();
|
||||||
|
if let Err(e) = Errno::result(readdir_r(
|
||||||
|
(self.0).0.as_ptr(),
|
||||||
|
ent.as_mut_ptr(),
|
||||||
|
&mut result,
|
||||||
|
)) {
|
||||||
|
return Some(Err(e));
|
||||||
|
}
|
||||||
|
if result.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
assert_eq!(result, ent.as_mut_ptr(), "readdir_r specification violated");
|
||||||
|
Some(Ok(Entry(ent.assume_init())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Dir {
|
||||||
|
type IntoIter = IntoIter;
|
||||||
|
type Item = Result<Entry>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> IntoIter {
|
||||||
|
IntoIter(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A directory entry, similar to `std::fs::DirEntry`.
|
||||||
|
///
|
||||||
|
/// Note that unlike the std version, this may represent the `.` or `..` entries.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub(crate) struct Entry(dirent);
|
||||||
|
|
||||||
|
pub(crate) type Type = FileType;
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
/// Returns the inode number (`d_ino`) of the underlying `dirent`.
|
||||||
|
#[cfg(any(
|
||||||
|
target_os = "android",
|
||||||
|
target_os = "emscripten",
|
||||||
|
target_os = "fuchsia",
|
||||||
|
target_os = "haiku",
|
||||||
|
target_os = "ios",
|
||||||
|
target_os = "l4re",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "solaris"
|
||||||
|
))]
|
||||||
|
pub(crate) fn ino(&self) -> u64 {
|
||||||
|
self.0.d_ino.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inode number (`d_fileno`) of the underlying `dirent`.
|
||||||
|
#[cfg(not(any(
|
||||||
|
target_os = "android",
|
||||||
|
target_os = "emscripten",
|
||||||
|
target_os = "fuchsia",
|
||||||
|
target_os = "haiku",
|
||||||
|
target_os = "ios",
|
||||||
|
target_os = "l4re",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "solaris"
|
||||||
|
)))]
|
||||||
|
pub(crate) fn ino(&self) -> u64 {
|
||||||
|
u64::from(self.0.d_fileno)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bare file name of this directory entry without any other leading path component.
|
||||||
|
pub(crate) fn file_name(&self) -> &ffi::CStr {
|
||||||
|
unsafe { ::std::ffi::CStr::from_ptr(self.0.d_name.as_ptr()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of this directory entry, if known.
|
||||||
|
///
|
||||||
|
/// See platform `readdir(3)` or `dirent(5)` manpage for when the file type is known;
|
||||||
|
/// notably, some Linux filesystems don't implement this. The caller should use `stat` or
|
||||||
|
/// `fstat` if this returns `None`.
|
||||||
|
pub(crate) fn file_type(&self) -> FileType {
|
||||||
|
match self.0.d_type {
|
||||||
|
libc::DT_CHR => Type::CharacterDevice,
|
||||||
|
libc::DT_DIR => Type::Directory,
|
||||||
|
libc::DT_BLK => Type::BlockDevice,
|
||||||
|
libc::DT_REG => Type::RegularFile,
|
||||||
|
libc::DT_LNK => Type::Symlink,
|
||||||
|
/* libc::DT_UNKNOWN | libc::DT_SOCK | libc::DT_FIFO */ _ => Type::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) fn seek_loc(&self) -> SeekLoc {
|
||||||
|
unsafe { SeekLoc::from_raw(self.0.d_off) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct SeekLoc(libc::c_long);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
impl SeekLoc {
|
||||||
|
pub(crate) unsafe fn from_raw(loc: i64) -> Self {
|
||||||
|
Self(loc.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_raw(&self) -> i64 {
|
||||||
|
self.0.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
use super::super::dir::{Dir, Entry, SeekLoc};
|
||||||
use super::osfile::OsFile;
|
use super::osfile::OsFile;
|
||||||
use crate::hostcalls_impl::PathGet;
|
use crate::hostcalls_impl::{Dirent, PathGet};
|
||||||
use crate::sys::host_impl;
|
use crate::sys::host_impl;
|
||||||
use crate::sys::unix::str_to_cstring;
|
use crate::sys::unix::str_to_cstring;
|
||||||
use crate::{wasi, Error, Result};
|
use crate::{wasi, Error, Result};
|
||||||
use nix::libc::{self, c_long, c_void};
|
use log::trace;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::mem::MaybeUninit;
|
|
||||||
use std::os::unix::prelude::AsRawFd;
|
use std::os::unix::prelude::AsRawFd;
|
||||||
|
|
||||||
pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> {
|
pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> {
|
||||||
@@ -67,75 +67,74 @@ pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Resul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fd_readdir_impl(
|
||||||
|
fd: &File,
|
||||||
|
cookie: wasi::__wasi_dircookie_t,
|
||||||
|
) -> Result<impl Iterator<Item = Result<Dirent>>> {
|
||||||
|
// We need to duplicate the fd, because `opendir(3)`:
|
||||||
|
// After a successful call to fdopendir(), fd is used internally by the implementation,
|
||||||
|
// and should not otherwise be used by the application.
|
||||||
|
// `opendir(3p)` also says that it's undefined behavior to
|
||||||
|
// modify the state of the fd in a different way than by accessing DIR*.
|
||||||
|
//
|
||||||
|
// Still, rewinddir will be needed because the two file descriptors
|
||||||
|
// share progress. But we can safely execute closedir now.
|
||||||
|
let fd = fd.try_clone()?;
|
||||||
|
let mut dir = Dir::from(fd)?;
|
||||||
|
|
||||||
|
// Seek if needed. Unless cookie is wasi::__WASI_DIRCOOKIE_START,
|
||||||
|
// new items may not be returned to the caller.
|
||||||
|
//
|
||||||
|
// According to `opendir(3p)`:
|
||||||
|
// If a file is removed from or added to the directory after the most recent call
|
||||||
|
// to opendir() or rewinddir(), whether a subsequent call to readdir() returns an entry
|
||||||
|
// for that file is unspecified.
|
||||||
|
if cookie == wasi::__WASI_DIRCOOKIE_START {
|
||||||
|
trace!(" | fd_readdir: doing rewinddir");
|
||||||
|
dir.rewind();
|
||||||
|
} else {
|
||||||
|
trace!(" | fd_readdir: doing seekdir to {}", cookie);
|
||||||
|
let loc = unsafe { SeekLoc::from_raw(cookie as i64) };
|
||||||
|
dir.seek(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dir.into_iter().map(|entry| {
|
||||||
|
let entry: Entry = entry?;
|
||||||
|
Ok(Dirent {
|
||||||
|
name: entry // TODO can we reuse path_from_host for CStr?
|
||||||
|
.file_name()
|
||||||
|
.to_str()?
|
||||||
|
.to_owned(),
|
||||||
|
ino: entry.ino(),
|
||||||
|
ftype: entry.file_type().into(),
|
||||||
|
cookie: entry.seek_loc().to_raw().try_into()?,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should actually be common code with Windows,
|
||||||
|
// but there's BSD stuff remaining
|
||||||
pub(crate) fn fd_readdir(
|
pub(crate) fn fd_readdir(
|
||||||
os_file: &mut OsFile,
|
os_file: &mut OsFile,
|
||||||
host_buf: &mut [u8],
|
mut host_buf: &mut [u8],
|
||||||
cookie: wasi::__wasi_dircookie_t,
|
cookie: wasi::__wasi_dircookie_t,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
use libc::{dirent, fdopendir, readdir_r, rewinddir, seekdir};
|
let iter = fd_readdir_impl(os_file, cookie)?;
|
||||||
|
let mut used = 0;
|
||||||
let host_buf_ptr = host_buf.as_mut_ptr();
|
for dirent in iter {
|
||||||
let host_buf_len = host_buf.len();
|
let dirent_raw = dirent?.to_wasi_raw()?;
|
||||||
let dir = unsafe { fdopendir(os_file.as_raw_fd()) };
|
let offset = dirent_raw.len();
|
||||||
if dir.is_null() {
|
if host_buf.len() < offset {
|
||||||
return Err(host_impl::errno_from_nix(nix::errno::Errno::last()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if cookie != wasi::__WASI_DIRCOOKIE_START {
|
|
||||||
unsafe { seekdir(dir, cookie as c_long) };
|
|
||||||
} else {
|
|
||||||
// If cookie set to __WASI_DIRCOOKIE_START, rewind the dir ptr
|
|
||||||
// to the start of the stream.
|
|
||||||
unsafe { rewinddir(dir) };
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entry_buf = MaybeUninit::<dirent>::uninit();
|
|
||||||
let mut left = host_buf_len;
|
|
||||||
let mut host_buf_offset: usize = 0;
|
|
||||||
while left > 0 {
|
|
||||||
let mut host_entry: *mut dirent = std::ptr::null_mut();
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// `readdir_r` syscall is being deprecated so we should look into
|
|
||||||
// replacing it with `readdir` call instead.
|
|
||||||
// Also, `readdir_r` returns a positive int on failure, and doesn't
|
|
||||||
// set the errno.
|
|
||||||
let res = unsafe { readdir_r(dir, entry_buf.as_mut_ptr(), &mut host_entry) };
|
|
||||||
if res == -1 {
|
|
||||||
return Err(host_impl::errno_from_nix(nix::errno::Errno::last()));
|
|
||||||
}
|
|
||||||
if host_entry.is_null() {
|
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
host_buf[0..offset].copy_from_slice(&dirent_raw);
|
||||||
|
used += offset;
|
||||||
|
host_buf = &mut host_buf[offset..];
|
||||||
}
|
}
|
||||||
unsafe { entry_buf.assume_init() };
|
|
||||||
let entry: wasi::__wasi_dirent_t = host_impl::dirent_from_host(&unsafe { *host_entry })?;
|
|
||||||
|
|
||||||
log::debug!("fd_readdir entry = {:?}", entry);
|
|
||||||
|
|
||||||
let name_len = entry.d_namlen.try_into()?;
|
|
||||||
let required_space = std::mem::size_of_val(&entry) + name_len;
|
|
||||||
if required_space > left {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
let ptr = host_buf_ptr.offset(host_buf_offset.try_into()?) as *mut c_void
|
|
||||||
as *mut wasi::__wasi_dirent_t;
|
|
||||||
*ptr = entry;
|
|
||||||
}
|
|
||||||
host_buf_offset += std::mem::size_of_val(&entry);
|
|
||||||
let name_ptr = unsafe { *host_entry }.d_name.as_ptr();
|
|
||||||
unsafe {
|
|
||||||
std::ptr::copy_nonoverlapping(
|
|
||||||
name_ptr as *const _,
|
|
||||||
host_buf_ptr.offset(host_buf_offset.try_into()?) as *mut _,
|
|
||||||
name_len,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
host_buf_offset += name_len;
|
|
||||||
left -= required_space;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(host_buf_len - left)
|
trace!(" | *buf_used={:?}", used);
|
||||||
|
Ok(used)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fd_advise(
|
pub(crate) fn fd_advise(
|
||||||
|
|||||||
@@ -26,29 +26,7 @@ pub(crate) mod fdentry_impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod host_impl {
|
pub(crate) mod host_impl {
|
||||||
use super::super::host_impl::dirent_filetype_from_host;
|
|
||||||
use crate::{wasi, Error, Result};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
pub(crate) const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_RSYNC;
|
pub(crate) const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_RSYNC;
|
||||||
|
|
||||||
pub(crate) fn dirent_from_host(
|
|
||||||
host_entry: &nix::libc::dirent,
|
|
||||||
) -> Result<wasi::__wasi_dirent_t> {
|
|
||||||
let mut entry = unsafe { std::mem::zeroed::<wasi::__wasi_dirent_t>() };
|
|
||||||
let d_namlen = unsafe { std::ffi::CStr::from_ptr(host_entry.d_name.as_ptr()) }
|
|
||||||
.to_bytes()
|
|
||||||
.len();
|
|
||||||
if d_namlen > u32::max_value() as usize {
|
|
||||||
return Err(Error::EIO);
|
|
||||||
}
|
|
||||||
let d_type = dirent_filetype_from_host(host_entry)?;
|
|
||||||
entry.d_ino = host_entry.d_ino;
|
|
||||||
entry.d_next = u64::try_from(host_entry.d_off).map_err(|_| Error::EOVERFLOW)?;
|
|
||||||
entry.d_namlen = u32::try_from(d_namlen).map_err(|_| Error::EOVERFLOW)?;
|
|
||||||
entry.d_type = d_type;
|
|
||||||
Ok(entry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod fs_helpers {
|
pub(crate) mod fs_helpers {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ pub(crate) mod fdentry_impl;
|
|||||||
pub(crate) mod host_impl;
|
pub(crate) mod host_impl;
|
||||||
pub(crate) mod hostcalls_impl;
|
pub(crate) mod hostcalls_impl;
|
||||||
|
|
||||||
|
mod dir;
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "macos",
|
target_os = "macos",
|
||||||
target_os = "netbsd",
|
target_os = "netbsd",
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ use super::fs_helpers::*;
|
|||||||
use crate::ctx::WasiCtx;
|
use crate::ctx::WasiCtx;
|
||||||
use crate::fdentry::FdEntry;
|
use crate::fdentry::FdEntry;
|
||||||
use crate::helpers::systemtime_to_timestamp;
|
use crate::helpers::systemtime_to_timestamp;
|
||||||
use crate::hostcalls_impl::{fd_filestat_set_times_impl, FileType, PathGet};
|
use crate::hostcalls_impl::{fd_filestat_set_times_impl, Dirent, FileType, PathGet};
|
||||||
use crate::sys::fdentry_impl::{determine_type_rights, OsFile};
|
use crate::sys::fdentry_impl::{determine_type_rights, OsFile};
|
||||||
use crate::sys::host_impl;
|
use crate::sys::host_impl::{self, path_from_host};
|
||||||
use crate::sys::hostcalls_impl::fs_helpers::PathGetExt;
|
use crate::sys::hostcalls_impl::fs_helpers::PathGetExt;
|
||||||
use crate::{wasi, Error, Result};
|
use crate::{wasi, Error, Result};
|
||||||
|
use log::{debug, trace};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fs::{File, Metadata, OpenOptions};
|
use std::fs::{File, Metadata, OpenOptions};
|
||||||
use std::io::{self, Seek, SeekFrom};
|
use std::io::{self, Seek, SeekFrom};
|
||||||
use std::os::windows::fs::{FileExt, OpenOptionsExt};
|
use std::os::windows::fs::{FileExt, OpenOptionsExt};
|
||||||
use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
|
use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use winx::file::{AccessMode, Flags};
|
||||||
|
|
||||||
fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||||
// get current cursor position
|
// get current cursor position
|
||||||
@@ -166,12 +168,117 @@ pub(crate) fn path_open(
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dirent_from_path<P: AsRef<Path>>(
|
||||||
|
path: P,
|
||||||
|
name: &str,
|
||||||
|
cookie: wasi::__wasi_dircookie_t,
|
||||||
|
) -> Result<Dirent> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
trace!("dirent_from_path: opening {}", path.to_string_lossy());
|
||||||
|
|
||||||
|
// To open a directory on Windows, FILE_FLAG_BACKUP_SEMANTICS flag must be used
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits())
|
||||||
|
.read(true)
|
||||||
|
.open(path)?;
|
||||||
|
let ty = file.metadata()?.file_type();
|
||||||
|
Ok(Dirent {
|
||||||
|
ftype: filetype_from_std(&ty),
|
||||||
|
name: name.to_owned(),
|
||||||
|
cookie,
|
||||||
|
ino: file_serial_no(&file)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows there is apparently no support for seeking the directory stream in the OS.
|
||||||
|
// cf. https://github.com/WebAssembly/WASI/issues/61
|
||||||
|
//
|
||||||
|
// The implementation here may perform in O(n^2) if the host buffer is O(1)
|
||||||
|
// and the number of directory entries is O(n).
|
||||||
|
// TODO: Add a heuristic optimization to achieve O(n) time in the most common case
|
||||||
|
// where fd_readdir is resumed where it previously finished
|
||||||
|
//
|
||||||
|
// Correctness of this approach relies upon one assumption: that the order of entries
|
||||||
|
// returned by `FindNextFileW` is stable, i.e. doesn't change if the directory
|
||||||
|
// contents stay the same. This invariant is crucial to be able to implement
|
||||||
|
// any kind of seeking whatsoever without having to read the whole directory at once
|
||||||
|
// and then return the data from cache. (which leaks memory)
|
||||||
|
//
|
||||||
|
// The MSDN documentation explicitly says that the order in which the search returns the files
|
||||||
|
// is not guaranteed, and is dependent on the file system.
|
||||||
|
// cf. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew
|
||||||
|
//
|
||||||
|
// This stackoverflow post suggests that `FindNextFileW` is indeed stable and that
|
||||||
|
// the order of directory entries depends **only** on the filesystem used, but the
|
||||||
|
// MSDN documentation is not clear about this.
|
||||||
|
// cf. https://stackoverflow.com/questions/47380739/is-findfirstfile-and-findnextfile-order-random-even-for-dvd
|
||||||
|
//
|
||||||
|
// Implementation details:
|
||||||
|
// Cookies for the directory entries start from 1. (0 is reserved by wasi::__WASI_DIRCOOKIE_START)
|
||||||
|
// . gets cookie = 1
|
||||||
|
// .. gets cookie = 2
|
||||||
|
// other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies
|
||||||
|
pub(crate) fn fd_readdir_impl(
|
||||||
|
fd: &File,
|
||||||
|
cookie: wasi::__wasi_dircookie_t,
|
||||||
|
) -> Result<impl Iterator<Item = Result<Dirent>>> {
|
||||||
|
use winx::file::get_file_path;
|
||||||
|
|
||||||
|
let cookie = cookie.try_into()?;
|
||||||
|
let path = get_file_path(fd)?;
|
||||||
|
// std::fs::ReadDir doesn't return . and .., so we need to emulate it
|
||||||
|
let path = Path::new(&path);
|
||||||
|
// The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too
|
||||||
|
let parent = path.parent().unwrap_or(path);
|
||||||
|
let dot = dirent_from_path(path, ".", 1)?;
|
||||||
|
let dotdot = dirent_from_path(parent, "..", 2)?;
|
||||||
|
|
||||||
|
trace!(" | fd_readdir impl: executing std::fs::ReadDir");
|
||||||
|
let iter = path.read_dir()?.zip(3..).map(|(dir, no)| {
|
||||||
|
let dir: std::fs::DirEntry = dir?;
|
||||||
|
|
||||||
|
Ok(Dirent {
|
||||||
|
name: path_from_host(dir.file_name())?,
|
||||||
|
ftype: filetype_from_std(&dir.file_type()?),
|
||||||
|
ino: File::open(dir.path()).and_then(|f| file_serial_no(&f))?,
|
||||||
|
cookie: no,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// into_iter for arrays is broken and returns references instead of values,
|
||||||
|
// so we need to use vec![...] and do heap allocation
|
||||||
|
// See https://github.com/rust-lang/rust/issues/25725
|
||||||
|
let iter = vec![dot, dotdot].into_iter().map(Ok).chain(iter);
|
||||||
|
|
||||||
|
// Emulate seekdir(). This may give O(n^2) complexity if used with a
|
||||||
|
// small host_buf, but this is difficult to implement efficiently.
|
||||||
|
//
|
||||||
|
// See https://github.com/WebAssembly/WASI/issues/61 for more details.
|
||||||
|
Ok(iter.skip(cookie))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should actually be common code with Linux
|
||||||
pub(crate) fn fd_readdir(
|
pub(crate) fn fd_readdir(
|
||||||
fd: &mut OsFile,
|
os_file: &mut OsFile,
|
||||||
host_buf: &mut [u8],
|
mut host_buf: &mut [u8],
|
||||||
cookie: wasi::__wasi_dircookie_t,
|
cookie: wasi::__wasi_dircookie_t,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
unimplemented!("fd_readdir")
|
let iter = fd_readdir_impl(os_file, cookie)?;
|
||||||
|
let mut used = 0;
|
||||||
|
for dirent in iter {
|
||||||
|
let dirent_raw = dirent?.to_wasi_raw()?;
|
||||||
|
let offset = dirent_raw.len();
|
||||||
|
if host_buf.len() < offset {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
host_buf[0..offset].copy_from_slice(&dirent_raw);
|
||||||
|
used += offset;
|
||||||
|
host_buf = &mut host_buf[offset..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(" | *buf_used={:?}", used);
|
||||||
|
Ok(used)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result<usize> {
|
pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result<usize> {
|
||||||
@@ -288,7 +395,7 @@ pub(crate) fn device_id(file: &File, _metadata: &Metadata) -> io::Result<u64> {
|
|||||||
Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into())
|
Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn file_serial_no(file: &File, _metadata: &Metadata) -> io::Result<u64> {
|
pub(crate) fn file_serial_no(file: &File) -> io::Result<u64> {
|
||||||
let info = winx::file::get_fileinfo(file)?;
|
let info = winx::file::get_fileinfo(file)?;
|
||||||
let high = info.nFileIndexHigh;
|
let high = info.nFileIndexHigh;
|
||||||
let low = info.nFileIndexLow;
|
let low = info.nFileIndexLow;
|
||||||
@@ -304,19 +411,18 @@ pub(crate) fn fd_filestat_get_impl(file: &std::fs::File) -> Result<wasi::__wasi_
|
|||||||
let metadata = file.metadata()?;
|
let metadata = file.metadata()?;
|
||||||
Ok(wasi::__wasi_filestat_t {
|
Ok(wasi::__wasi_filestat_t {
|
||||||
st_dev: device_id(file, &metadata)?,
|
st_dev: device_id(file, &metadata)?,
|
||||||
st_ino: file_serial_no(file, &metadata)?,
|
st_ino: file_serial_no(file)?,
|
||||||
st_nlink: num_hardlinks(file, &metadata)?.try_into()?, // u64 doesn't fit into u32
|
st_nlink: num_hardlinks(file, &metadata)?.try_into()?, // u64 doesn't fit into u32
|
||||||
st_size: metadata.len(),
|
st_size: metadata.len(),
|
||||||
st_atim: systemtime_to_timestamp(metadata.accessed()?)?,
|
st_atim: systemtime_to_timestamp(metadata.accessed()?)?,
|
||||||
st_ctim: change_time(file, &metadata)?.try_into()?, // i64 doesn't fit into u64
|
st_ctim: change_time(file, &metadata)?.try_into()?, // i64 doesn't fit into u64
|
||||||
st_mtim: systemtime_to_timestamp(metadata.modified()?)?,
|
st_mtim: systemtime_to_timestamp(metadata.modified()?)?,
|
||||||
st_filetype: filetype(file, &metadata)?.to_wasi(),
|
st_filetype: filetype_from_std(&metadata.file_type()).to_wasi(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filetype(_file: &File, metadata: &Metadata) -> Result<FileType> {
|
pub(crate) fn filetype_from_std(ftype: &std::fs::FileType) -> FileType {
|
||||||
let ftype = metadata.file_type();
|
if ftype.is_file() {
|
||||||
let ret = if ftype.is_file() {
|
|
||||||
FileType::RegularFile
|
FileType::RegularFile
|
||||||
} else if ftype.is_dir() {
|
} else if ftype.is_dir() {
|
||||||
FileType::Directory
|
FileType::Directory
|
||||||
@@ -324,9 +430,7 @@ fn filetype(_file: &File, metadata: &Metadata) -> Result<FileType> {
|
|||||||
FileType::Symlink
|
FileType::Symlink
|
||||||
} else {
|
} else {
|
||||||
FileType::Unknown
|
FileType::Unknown
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(ret)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn path_filestat_get(
|
pub(crate) fn path_filestat_get(
|
||||||
|
|||||||
Reference in New Issue
Block a user