It's wiggle time! (#1202)

* Use wiggle in place of wig in wasi-common

This is a rather massive commit that introduces `wiggle` into the
picture. We still use `wig`'s macro in `old` snapshot and to generate
`wasmtime-wasi` glue, but everything else is now autogenerated by `wiggle`.
In summary, thanks to `wiggle`, we no longer need to worry about
serialising and deserialising to and from the guest memory, and
all guest (WASI) types are now proper idiomatic Rust types.

While we're here, in preparation for the ephemeral snapshot, I went
ahead and reorganised the internal structure of the crate. Instead of
modules like `hostcalls_impl` or `hostcalls_impl::fs`, the structure
now resembles that in ephemeral with modules like `path`, `fd`, etc.
Now, I'm not requiring we leave it like this, but I reckon it looks
cleaner this way after all.

* Fix wig to use new first-class access to caller's mem

* Ignore warning in proc_exit for the moment

* Group unsafes together in args and environ calls

* Simplify pwrite; more unsafe blocks

* Simplify fd_read

* Bundle up unsafes in fd_readdir

* Simplify fd_write

* Add comment to path_readlink re zero-len buffers

* Simplify unsafes in random_get

* Hide GuestPtr<str> to &str in path::get

* Rewrite pread and pwrite using SeekFrom and read/write_vectored

I've left the implementation of VirtualFs pretty much untouched
as I don't feel that comfortable in changing the API too much.
Having said that, I reckon `pread` and `pwrite` could be refactored
out, and `preadv` and `pwritev` could be entirely rewritten using
`seek` and `read_vectored` and `write_vectored`.

* Add comment about VirtFs unsafety

* Fix all mentions of FdEntry to Entry

* Fix warnings on Win

* Add aux struct EntryTable responsible for Fds and Entries

This commit adds aux struct `EntryTable` which is private to `WasiCtx`
and is basically responsible for `Fd` alloc/dealloc as well as storing
matching `Entry`s. This struct is entirely private to `WasiCtx` and
as such as should remain transparent to `WasiCtx` users.

* Remove redundant check for empty buffer in path_readlink

* Preserve and rewind file cursor in pread/pwrite

* Use GuestPtr<[u8]>::copy_from_slice wherever copying bytes directly

* Use GuestPtr<[u8]>::copy_from_slice in fd_readdir

* Clean up unsafes around WasiCtx accessors

* Fix bugs in args_get and environ_get

* Fix conflicts after rebase
This commit is contained in:
Jakub Konka
2020-03-20 21:54:44 +01:00
committed by GitHub
parent f700efeb03
commit 32595faba5
62 changed files with 4293 additions and 5027 deletions

View File

@@ -0,0 +1,104 @@
use crate::wasi::{types, Errno, Result};
use cpu_time::{ProcessTime, ThreadTime};
use lazy_static::lazy_static;
use std::convert::TryInto;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
lazy_static! {
static ref START_MONOTONIC: Instant = Instant::now();
static ref PERF_COUNTER_RES: u64 = get_perf_counter_resolution_ns();
}
// Timer resolution on Windows is really hard. We may consider exposing the resolution of the respective
// timers as an associated function in the future.
pub(crate) fn res_get(clock_id: types::Clockid) -> Result<types::Timestamp> {
let ts = match clock_id {
// This is the best that we can do with std::time::SystemTime.
// Rust uses GetSystemTimeAsFileTime, which is said to have the resolution of
// 10ms or 55ms, [1] but MSDN doesn't confirm this in any way.
// Even the MSDN article on high resolution timestamps doesn't even mention the precision
// for this method. [3]
//
// The timer resolution can be queried using one of the functions: [2, 5]
// * NtQueryTimerResolution, which is undocumented and thus not exposed by the winapi crate
// * timeGetDevCaps, which returns the upper and lower bound for the precision, in ms.
// While the upper bound seems like something we could use, it's typically too high to be meaningful.
// For instance, the intervals return by the syscall are:
// * [1, 65536] on Wine
// * [1, 1000000] on Windows 10, which is up to (sic) 1000 seconds.
//
// It's possible to manually set the timer resolution, but this sounds like something which should
// only be done temporarily. [5]
//
// Alternatively, we could possibly use GetSystemTimePreciseAsFileTime in clock_time_get, but
// this syscall is only available starting from Windows 8.
// (we could possibly emulate it on earlier versions of Windows, see [4])
// The MSDN are not clear on the resolution of GetSystemTimePreciseAsFileTime either, but a
// Microsoft devblog entry [1] suggests that it kind of combines GetSystemTimeAsFileTime with
// QueryPeformanceCounter, which probably means that those two should have the same resolution.
//
// See also this discussion about the use of GetSystemTimePreciseAsFileTime in Python stdlib,
// which in particular contains some resolution benchmarks.
//
// [1] https://devblogs.microsoft.com/oldnewthing/20170921-00/?p=97057
// [2] http://www.windowstimestamp.com/description
// [3] https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN
// [4] https://www.codeproject.com/Tips/1011902/High-Resolution-Time-For-Windows
// [5] https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly
// [6] https://bugs.python.org/issue19007
types::Clockid::Realtime => 55_000_000,
// std::time::Instant uses QueryPerformanceCounter & QueryPerformanceFrequency internally
types::Clockid::Monotonic => *PERF_COUNTER_RES,
// The best we can do is to hardcode the value from the docs.
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes
types::Clockid::ProcessCputimeId => 100,
// The best we can do is to hardcode the value from the docs.
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes
types::Clockid::ThreadCputimeId => 100,
};
Ok(ts)
}
pub(crate) fn time_get(clock_id: types::Clockid) -> Result<types::Timestamp> {
let duration = match clock_id {
types::Clockid::Realtime => get_monotonic_time(),
types::Clockid::Monotonic => get_realtime_time()?,
types::Clockid::ProcessCputimeId => get_proc_cputime()?,
types::Clockid::ThreadCputimeId => get_thread_cputime()?,
};
let duration = duration.as_nanos().try_into()?;
Ok(duration)
}
fn get_monotonic_time() -> Duration {
// We're circumventing the fact that we can't get a Duration from an Instant
// The epoch of __WASI_CLOCKID_MONOTONIC is undefined, so we fix a time point once
// and count relative to this time point.
//
// The alternative would be to copy over the implementation of std::time::Instant
// to our source tree and add a conversion to std::time::Duration
START_MONOTONIC.elapsed()
}
fn get_realtime_time() -> Result<Duration> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Errno::Fault)
}
fn get_proc_cputime() -> Result<Duration> {
Ok(ProcessTime::try_now()?.as_duration())
}
fn get_thread_cputime() -> Result<Duration> {
Ok(ThreadTime::try_now()?.as_duration())
}
fn get_perf_counter_resolution_ns() -> u64 {
use winx::time::perf_counter_frequency;
const NANOS_PER_SEC: u64 = 1_000_000_000;
// This should always succeed starting from Windows XP, so it's fine to panic in case of an error.
let freq = perf_counter_frequency().expect("QueryPerformanceFrequency returned an error");
let epsilon = NANOS_PER_SEC / freq;
epsilon
}

View File

@@ -1,5 +1,5 @@
use crate::entry::{Descriptor, OsHandleRef};
use crate::wasi;
use crate::wasi::{types, RightsExt};
use std::fs::File;
use std::io;
use std::mem::ManuallyDrop;
@@ -63,23 +63,19 @@ pub(crate) fn descriptor_as_oshandle<'lifetime>(
/// This function is unsafe because it operates on a raw file descriptor.
pub(crate) unsafe fn determine_type_and_access_rights<Handle: AsRawHandle>(
handle: &Handle,
) -> io::Result<(
wasi::__wasi_filetype_t,
wasi::__wasi_rights_t,
wasi::__wasi_rights_t,
)> {
) -> io::Result<(types::Filetype, types::Rights, types::Rights)> {
use winx::file::{query_access_information, AccessMode};
let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(handle)?;
match file_type {
wasi::__WASI_FILETYPE_DIRECTORY | wasi::__WASI_FILETYPE_REGULAR_FILE => {
types::Filetype::Directory | types::Filetype::RegularFile => {
let mode = query_access_information(handle.as_raw_handle())?;
if mode.contains(AccessMode::FILE_GENERIC_READ) {
rights_base |= wasi::__WASI_RIGHTS_FD_READ;
rights_base |= types::Rights::FD_READ;
}
if mode.contains(AccessMode::FILE_GENERIC_WRITE) {
rights_base |= wasi::__WASI_RIGHTS_FD_WRITE;
rights_base |= types::Rights::FD_WRITE;
}
}
_ => {
@@ -96,20 +92,16 @@ pub(crate) unsafe fn determine_type_and_access_rights<Handle: AsRawHandle>(
/// This function is unsafe because it operates on a raw file descriptor.
pub(crate) unsafe fn determine_type_rights<Handle: AsRawHandle>(
handle: &Handle,
) -> io::Result<(
wasi::__wasi_filetype_t,
wasi::__wasi_rights_t,
wasi::__wasi_rights_t,
)> {
) -> io::Result<(types::Filetype, types::Rights, types::Rights)> {
let (file_type, rights_base, rights_inheriting) = {
let file_type = winx::file::get_file_type(handle.as_raw_handle())?;
if file_type.is_char() {
// character file: LPT device or console
// TODO: rule out LPT device
(
wasi::__WASI_FILETYPE_CHARACTER_DEVICE,
wasi::RIGHTS_TTY_BASE,
wasi::RIGHTS_TTY_BASE,
types::Filetype::CharacterDevice,
types::Rights::tty_base(),
types::Rights::tty_base(),
)
} else if file_type.is_disk() {
// disk file: file, dir or disk device
@@ -117,15 +109,15 @@ pub(crate) unsafe fn determine_type_rights<Handle: AsRawHandle>(
let meta = file.metadata()?;
if meta.is_dir() {
(
wasi::__WASI_FILETYPE_DIRECTORY,
wasi::RIGHTS_DIRECTORY_BASE,
wasi::RIGHTS_DIRECTORY_INHERITING,
types::Filetype::Directory,
types::Rights::directory_base(),
types::Rights::directory_inheriting(),
)
} else if meta.is_file() {
(
wasi::__WASI_FILETYPE_REGULAR_FILE,
wasi::RIGHTS_REGULAR_FILE_BASE,
wasi::RIGHTS_REGULAR_FILE_INHERITING,
types::Filetype::RegularFile,
types::Rights::regular_file_base(),
types::Rights::regular_file_inheriting(),
)
} else {
return Err(io::Error::from_raw_os_error(libc::EINVAL));
@@ -134,9 +126,9 @@ pub(crate) unsafe fn determine_type_rights<Handle: AsRawHandle>(
// pipe object: socket, named pipe or anonymous pipe
// TODO: what about pipes, etc?
(
wasi::__WASI_FILETYPE_SOCKET_STREAM,
wasi::RIGHTS_SOCKET_BASE,
wasi::RIGHTS_SOCKET_INHERITING,
types::Filetype::SocketStream,
types::Rights::socket_base(),
types::Rights::socket_inheriting(),
)
} else {
return Err(io::Error::from_raw_os_error(libc::EINVAL));

View File

@@ -0,0 +1,188 @@
use super::file_serial_no;
use crate::path;
use crate::sys::entry::OsHandle;
use crate::wasi::{types, Result};
use log::trace;
use std::convert::TryInto;
use std::fs::{File, OpenOptions};
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
use std::path::Path;
use winx::file::{AccessMode, FileModeInformation, Flags};
pub(crate) fn fdstat_get(fd: &File) -> Result<types::Fdflags> {
let mut fdflags = types::Fdflags::empty();
let handle = fd.as_raw_handle();
let access_mode = winx::file::query_access_information(handle)?;
let mode = winx::file::query_mode_information(handle)?;
// Append without write implies append-only (__WASI_FDFLAGS_APPEND)
if access_mode.contains(AccessMode::FILE_APPEND_DATA)
&& !access_mode.contains(AccessMode::FILE_WRITE_DATA)
{
fdflags |= types::Fdflags::APPEND;
}
if mode.contains(FileModeInformation::FILE_WRITE_THROUGH) {
// Only report __WASI_FDFLAGS_SYNC
// This is technically the only one of the O_?SYNC flags Windows supports.
fdflags |= types::Fdflags::SYNC;
}
// Files do not support the `__WASI_FDFLAGS_NONBLOCK` flag
Ok(fdflags)
}
pub(crate) fn fdstat_set_flags(fd: &File, fdflags: types::Fdflags) -> Result<Option<OsHandle>> {
let handle = fd.as_raw_handle();
let access_mode = winx::file::query_access_information(handle)?;
let new_access_mode = file_access_mode_from_fdflags(
fdflags,
access_mode.contains(AccessMode::FILE_READ_DATA),
access_mode.contains(AccessMode::FILE_WRITE_DATA)
| access_mode.contains(AccessMode::FILE_APPEND_DATA),
);
unsafe {
Ok(Some(OsHandle::from(File::from_raw_handle(
winx::file::reopen_file(handle, new_access_mode, fdflags.into())?,
))))
}
}
pub(crate) fn advise(
_file: &File,
_advice: types::Advice,
_offset: types::Filesize,
_len: types::Filesize,
) -> Result<()> {
Ok(())
}
fn file_access_mode_from_fdflags(fdflags: types::Fdflags, read: bool, write: bool) -> AccessMode {
let mut access_mode = AccessMode::READ_CONTROL;
// Note that `GENERIC_READ` and `GENERIC_WRITE` cannot be used to properly support append-only mode
// The file-specific flags `FILE_GENERIC_READ` and `FILE_GENERIC_WRITE` are used here instead
// These flags have the same semantic meaning for file objects, but allow removal of specific permissions (see below)
if read {
access_mode.insert(AccessMode::FILE_GENERIC_READ);
}
if write {
access_mode.insert(AccessMode::FILE_GENERIC_WRITE);
}
// For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA.
// This makes the handle "append only".
// Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior).
if fdflags.contains(&types::Fdflags::APPEND) {
access_mode.insert(AccessMode::FILE_APPEND_DATA);
access_mode.remove(AccessMode::FILE_WRITE_DATA);
}
access_mode
}
// 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 readdir(
fd: &File,
cookie: types::Dircookie,
) -> Result<impl Iterator<Item = Result<(types::Dirent, String)>>> {
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?;
let ftype = dir.file_type()?;
let name = path::from_host(dir.file_name())?;
let d_ino = File::open(dir.path()).and_then(|f| file_serial_no(&f))?;
let dirent = types::Dirent {
d_namlen: name.len().try_into()?,
d_type: ftype.into(),
d_ino,
d_next: no,
};
Ok((dirent, name))
});
// 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))
}
fn dirent_from_path<P: AsRef<Path>>(
path: P,
name: &str,
cookie: types::Dircookie,
) -> Result<(types::Dirent, String)> {
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();
let name = name.to_owned();
let dirent = types::Dirent {
d_namlen: name.len().try_into()?,
d_next: cookie,
d_type: ty.into(),
d_ino: file_serial_no(&file)?,
};
Ok((dirent, name))
}
pub(crate) fn filestat_get(file: &std::fs::File) -> Result<types::Filestat> {
let filestat = file.try_into()?;
Ok(filestat)
}

View File

@@ -1,113 +0,0 @@
//! WASI host types specific to Windows host.
use crate::host::FileType;
use crate::wasi::{self, WasiError, WasiResult};
use std::convert::TryInto;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::time::{SystemTime, UNIX_EPOCH};
use winapi::shared::winerror;
impl From<io::Error> for WasiError {
fn from(err: io::Error) -> Self {
match err.raw_os_error() {
Some(code) => match code as u32 {
winerror::ERROR_SUCCESS => Self::ESUCCESS,
winerror::ERROR_BAD_ENVIRONMENT => Self::E2BIG,
winerror::ERROR_FILE_NOT_FOUND => Self::ENOENT,
winerror::ERROR_PATH_NOT_FOUND => Self::ENOENT,
winerror::ERROR_TOO_MANY_OPEN_FILES => Self::ENFILE,
winerror::ERROR_ACCESS_DENIED => Self::EACCES,
winerror::ERROR_SHARING_VIOLATION => Self::EACCES,
winerror::ERROR_PRIVILEGE_NOT_HELD => Self::ENOTCAPABLE,
winerror::ERROR_INVALID_HANDLE => Self::EBADF,
winerror::ERROR_INVALID_NAME => Self::ENOENT,
winerror::ERROR_NOT_ENOUGH_MEMORY => Self::ENOMEM,
winerror::ERROR_OUTOFMEMORY => Self::ENOMEM,
winerror::ERROR_DIR_NOT_EMPTY => Self::ENOTEMPTY,
winerror::ERROR_NOT_READY => Self::EBUSY,
winerror::ERROR_BUSY => Self::EBUSY,
winerror::ERROR_NOT_SUPPORTED => Self::ENOTSUP,
winerror::ERROR_FILE_EXISTS => Self::EEXIST,
winerror::ERROR_BROKEN_PIPE => Self::EPIPE,
winerror::ERROR_BUFFER_OVERFLOW => Self::ENAMETOOLONG,
winerror::ERROR_NOT_A_REPARSE_POINT => Self::EINVAL,
winerror::ERROR_NEGATIVE_SEEK => Self::EINVAL,
winerror::ERROR_DIRECTORY => Self::ENOTDIR,
winerror::ERROR_ALREADY_EXISTS => Self::EEXIST,
x => {
log::debug!("unknown error value: {}", x);
Self::EIO
}
},
None => {
log::debug!("Other I/O error: {}", err);
Self::EIO
}
}
}
}
pub(crate) fn filetype_from_std(ftype: &fs::FileType) -> FileType {
if ftype.is_file() {
FileType::RegularFile
} else if ftype.is_dir() {
FileType::Directory
} else if ftype.is_symlink() {
FileType::Symlink
} else {
FileType::Unknown
}
}
fn num_hardlinks(file: &File) -> io::Result<u64> {
Ok(winx::file::get_fileinfo(file)?.nNumberOfLinks.into())
}
fn device_id(file: &File) -> io::Result<u64> {
Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into())
}
pub(crate) fn file_serial_no(file: &File) -> io::Result<u64> {
let info = winx::file::get_fileinfo(file)?;
let high = info.nFileIndexHigh;
let low = info.nFileIndexLow;
let no = (u64::from(high) << 32) | u64::from(low);
Ok(no)
}
fn change_time(file: &File) -> io::Result<i64> {
winx::file::change_time(file)
}
fn systemtime_to_timestamp(st: SystemTime) -> WasiResult<u64> {
st.duration_since(UNIX_EPOCH)
.map_err(|_| WasiError::EINVAL)? // date earlier than UNIX_EPOCH
.as_nanos()
.try_into()
.map_err(Into::into) // u128 doesn't fit into u64
}
pub(crate) fn filestat_from_win(file: &File) -> WasiResult<wasi::__wasi_filestat_t> {
let metadata = file.metadata()?;
Ok(wasi::__wasi_filestat_t {
dev: device_id(file)?,
ino: file_serial_no(file)?,
nlink: num_hardlinks(file)?.try_into()?, // u64 doesn't fit into u32
size: metadata.len(),
atim: systemtime_to_timestamp(metadata.accessed()?)?,
ctim: change_time(file)?.try_into()?, // i64 doesn't fit into u64
mtim: systemtime_to_timestamp(metadata.modified()?)?,
filetype: filetype_from_std(&metadata.file_type()).to_wasi(),
})
}
/// Creates owned WASI path from OS string.
///
/// NB WASI spec requires OS string to be valid UTF-8. Otherwise,
/// `__WASI_ERRNO_ILSEQ` error is returned.
pub(crate) fn path_from_host<S: AsRef<OsStr>>(s: S) -> WasiResult<String> {
let vec: Vec<u16> = s.as_ref().encode_wide().collect();
String::from_utf16(&vec).map_err(|_| WasiError::EILSEQ)
}

View File

@@ -1,599 +0,0 @@
#![allow(non_camel_case_types)]
#![allow(unused)]
use super::fs_helpers::*;
use crate::ctx::WasiCtx;
use crate::entry::{Descriptor, Entry};
use crate::host::{Dirent, FileType};
use crate::hostcalls_impl::{fd_filestat_set_times_impl, PathGet};
use crate::sys::entry_impl::{determine_type_rights, OsHandle};
use crate::sys::host_impl::{self, path_from_host};
use crate::sys::hostcalls_impl::fs_helpers::PathGetExt;
use crate::wasi::{self, WasiError, WasiResult};
use log::{debug, trace};
use std::convert::TryInto;
use std::fs::{File, Metadata, OpenOptions};
use std::io::{self, Seek, SeekFrom};
use std::os::windows::fs::{FileExt, OpenOptionsExt};
use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
use std::path::{Path, PathBuf};
use winapi::shared::winerror;
use winx::file::{AccessMode, CreationDisposition, FileModeInformation, Flags};
fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
// get current cursor position
let cur_pos = file.seek(SeekFrom::Current(0))?;
// perform a seek read by a specified offset
let nread = file.seek_read(buf, offset)?;
// rewind the cursor back to the original position
file.seek(SeekFrom::Start(cur_pos))?;
Ok(nread)
}
fn write_at(mut file: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
// get current cursor position
let cur_pos = file.seek(SeekFrom::Current(0))?;
// perform a seek write by a specified offset
let nwritten = file.seek_write(buf, offset)?;
// rewind the cursor back to the original position
file.seek(SeekFrom::Start(cur_pos))?;
Ok(nwritten)
}
// TODO refactor common code with unix
pub(crate) fn fd_pread(
file: &File,
buf: &mut [u8],
offset: wasi::__wasi_filesize_t,
) -> WasiResult<usize> {
read_at(file, buf, offset).map_err(Into::into)
}
// TODO refactor common code with unix
pub(crate) fn fd_pwrite(
file: &File,
buf: &[u8],
offset: wasi::__wasi_filesize_t,
) -> WasiResult<usize> {
write_at(file, buf, offset).map_err(Into::into)
}
pub(crate) fn fd_fdstat_get(fd: &File) -> WasiResult<wasi::__wasi_fdflags_t> {
let mut fdflags = 0;
let handle = unsafe { fd.as_raw_handle() };
let access_mode = winx::file::query_access_information(handle)?;
let mode = winx::file::query_mode_information(handle)?;
// Append without write implies append-only (__WASI_FDFLAGS_APPEND)
if access_mode.contains(AccessMode::FILE_APPEND_DATA)
&& !access_mode.contains(AccessMode::FILE_WRITE_DATA)
{
fdflags |= wasi::__WASI_FDFLAGS_APPEND;
}
if mode.contains(FileModeInformation::FILE_WRITE_THROUGH) {
// Only report __WASI_FDFLAGS_SYNC
// This is technically the only one of the O_?SYNC flags Windows supports.
fdflags |= wasi::__WASI_FDFLAGS_SYNC;
}
// Files do not support the `__WASI_FDFLAGS_NONBLOCK` flag
Ok(fdflags)
}
pub(crate) fn fd_fdstat_set_flags(
fd: &File,
fdflags: wasi::__wasi_fdflags_t,
) -> WasiResult<Option<OsHandle>> {
let handle = unsafe { fd.as_raw_handle() };
let access_mode = winx::file::query_access_information(handle)?;
let new_access_mode = file_access_mode_from_fdflags(
fdflags,
access_mode.contains(AccessMode::FILE_READ_DATA),
access_mode.contains(AccessMode::FILE_WRITE_DATA)
| access_mode.contains(AccessMode::FILE_APPEND_DATA),
);
unsafe {
Ok(Some(OsHandle::from(File::from_raw_handle(
winx::file::reopen_file(handle, new_access_mode, file_flags_from_fdflags(fdflags))?,
))))
}
}
pub(crate) fn fd_advise(
_file: &File,
advice: wasi::__wasi_advice_t,
_offset: wasi::__wasi_filesize_t,
_len: wasi::__wasi_filesize_t,
) -> WasiResult<()> {
match advice {
wasi::__WASI_ADVICE_DONTNEED
| wasi::__WASI_ADVICE_SEQUENTIAL
| wasi::__WASI_ADVICE_WILLNEED
| wasi::__WASI_ADVICE_NOREUSE
| wasi::__WASI_ADVICE_RANDOM
| wasi::__WASI_ADVICE_NORMAL => {}
_ => return Err(WasiError::EINVAL),
}
Ok(())
}
pub(crate) fn path_create_directory(file: &File, path: &str) -> WasiResult<()> {
let path = concatenate(file, path)?;
std::fs::create_dir(&path).map_err(Into::into)
}
pub(crate) fn path_link(
resolved_old: PathGet,
resolved_new: PathGet,
follow_symlinks: bool,
) -> WasiResult<()> {
unimplemented!("path_link")
}
pub(crate) fn path_open(
resolved: PathGet,
read: bool,
write: bool,
oflags: wasi::__wasi_oflags_t,
fdflags: wasi::__wasi_fdflags_t,
) -> WasiResult<Descriptor> {
use winx::file::{AccessMode, CreationDisposition, Flags};
let is_trunc = oflags & wasi::__WASI_OFLAGS_TRUNC != 0;
if is_trunc {
// Windows does not support append mode when opening for truncation
// This is because truncation requires `GENERIC_WRITE` access, which will override the removal
// of the `FILE_WRITE_DATA` permission.
if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 {
return Err(WasiError::ENOTSUP);
}
}
// convert open flags
// note: the calls to `write(true)` are to bypass an internal OpenOption check
// the write flag will ultimately be ignored when `access_mode` is calculated below.
let mut opts = OpenOptions::new();
match creation_disposition_from_oflags(oflags) {
CreationDisposition::CREATE_ALWAYS => {
opts.create(true).write(true);
}
CreationDisposition::CREATE_NEW => {
opts.create_new(true).write(true);
}
CreationDisposition::TRUNCATE_EXISTING => {
opts.truncate(true).write(true);
}
_ => {}
}
let path = resolved.concatenate()?;
match path.symlink_metadata().map(|metadata| metadata.file_type()) {
Ok(file_type) => {
// check if we are trying to open a symlink
if file_type.is_symlink() {
return Err(WasiError::ELOOP);
}
// check if we are trying to open a file as a dir
if file_type.is_file() && oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 {
return Err(WasiError::ENOTDIR);
}
}
Err(err) => match err.raw_os_error() {
Some(code) => {
log::debug!("path_open at symlink_metadata error code={:?}", code);
if code as u32 != winerror::ERROR_FILE_NOT_FOUND {
return Err(err.into());
}
// file not found, let it proceed to actually
// trying to open it
}
None => {
log::debug!("Inconvertible OS error: {}", err);
return Err(WasiError::EIO);
}
},
}
let mut access_mode = file_access_mode_from_fdflags(fdflags, read, write);
// Truncation requires the special `GENERIC_WRITE` bit set (this is why it doesn't work with append-only mode)
if is_trunc {
access_mode |= AccessMode::GENERIC_WRITE;
}
opts.access_mode(access_mode.bits())
.custom_flags(file_flags_from_fdflags(fdflags).bits())
.open(&path)
.map(|f| OsHandle::from(f).into())
.map_err(Into::into)
}
fn creation_disposition_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition {
if oflags & wasi::__WASI_OFLAGS_CREAT != 0 {
if oflags & wasi::__WASI_OFLAGS_EXCL != 0 {
CreationDisposition::CREATE_NEW
} else {
CreationDisposition::CREATE_ALWAYS
}
} else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 {
CreationDisposition::TRUNCATE_EXISTING
} else {
CreationDisposition::OPEN_EXISTING
}
}
fn file_access_mode_from_fdflags(
fdflags: wasi::__wasi_fdflags_t,
read: bool,
write: bool,
) -> AccessMode {
let mut access_mode = AccessMode::READ_CONTROL;
// Note that `GENERIC_READ` and `GENERIC_WRITE` cannot be used to properly support append-only mode
// The file-specific flags `FILE_GENERIC_READ` and `FILE_GENERIC_WRITE` are used here instead
// These flags have the same semantic meaning for file objects, but allow removal of specific permissions (see below)
if read {
access_mode.insert(AccessMode::FILE_GENERIC_READ);
}
if write {
access_mode.insert(AccessMode::FILE_GENERIC_WRITE);
}
// For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA.
// This makes the handle "append only".
// Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior).
if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 {
access_mode.insert(AccessMode::FILE_APPEND_DATA);
access_mode.remove(AccessMode::FILE_WRITE_DATA);
}
access_mode
}
fn file_flags_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> Flags {
// Enable backup semantics so directories can be opened as files
let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS;
// Note: __WASI_FDFLAGS_NONBLOCK is purposely being ignored for files
// While Windows does inherently support a non-blocking mode on files, the WASI API will
// treat I/O operations on files as synchronous. WASI might have an async-io API in the future.
// Technically, Windows only supports __WASI_FDFLAGS_SYNC, but treat all the flags as the same.
if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0
|| fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0
|| fdflags & wasi::__WASI_FDFLAGS_SYNC != 0
{
flags.insert(Flags::FILE_FLAG_WRITE_THROUGH);
}
flags
}
fn dirent_from_path<P: AsRef<Path>>(
path: P,
name: &str,
cookie: wasi::__wasi_dircookie_t,
) -> WasiResult<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: host_impl::filetype_from_std(&ty),
name: name.to_owned(),
cookie,
ino: host_impl::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(
fd: &File,
cookie: wasi::__wasi_dircookie_t,
) -> WasiResult<impl Iterator<Item = WasiResult<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: host_impl::filetype_from_std(&dir.file_type()?),
ino: File::open(dir.path()).and_then(|f| host_impl::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))
}
pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> WasiResult<usize> {
use winx::file::get_file_path;
let path = resolved.concatenate()?;
let target_path = path.read_link()?;
// since on Windows we are effectively emulating 'at' syscalls
// we need to strip the prefix from the absolute path
// as otherwise we will error out since WASI is not capable
// of dealing with absolute paths
let dir_path = get_file_path(&resolved.dirfd().as_os_handle())?;
let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
let target_path = target_path
.strip_prefix(dir_path)
.map_err(|_| WasiError::ENOTCAPABLE)
.and_then(|path| path.to_str().map(String::from).ok_or(WasiError::EILSEQ))?;
if buf.len() > 0 {
let mut chars = target_path.chars();
let mut nread = 0usize;
for i in 0..buf.len() {
match chars.next() {
Some(ch) => {
buf[i] = ch as u8;
nread += 1;
}
None => break,
}
}
Ok(nread)
} else {
Ok(0)
}
}
fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> WasiResult<Option<PathBuf>> {
if resolved.path().ends_with('/') {
let suffix = resolved.path().trim_end_matches('/');
concatenate(&resolved.dirfd().as_os_handle(), Path::new(suffix)).map(Some)
} else {
Ok(None)
}
}
pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> WasiResult<()> {
use std::fs;
let old_path = resolved_old.concatenate()?;
let new_path = resolved_new.concatenate()?;
// First sanity check: check we're not trying to rename dir to file or vice versa.
// NB on Windows, the former is actually permitted [std::fs::rename].
//
// [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html
if old_path.is_dir() && new_path.is_file() {
return Err(WasiError::ENOTDIR);
}
// Second sanity check: check we're not trying to rename a file into a path
// ending in a trailing slash.
if old_path.is_file() && resolved_new.path().ends_with('/') {
return Err(WasiError::ENOTDIR);
}
// TODO handle symlinks
let err = match fs::rename(&old_path, &new_path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_rename at rename error code={:?}", code);
match code as u32 {
winerror::ERROR_ACCESS_DENIED => {
// So most likely dealing with new_path == dir.
// Eliminate case old_path == file first.
if old_path.is_file() {
return Err(WasiError::EISDIR);
} else {
// Ok, let's try removing an empty dir at new_path if it exists
// and is a nonempty dir.
fs::remove_dir(&new_path)?;
fs::rename(old_path, new_path)?;
return Ok(());
}
}
winerror::ERROR_INVALID_NAME => {
// If source contains trailing slashes, check if we are dealing with
// a file instead of a dir, and if so, throw ENOTDIR.
if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? {
if path.is_file() {
return Err(WasiError::ENOTDIR);
}
}
}
_ => {}
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(WasiError::EIO)
}
}
}
pub(crate) fn fd_filestat_get(file: &std::fs::File) -> WasiResult<wasi::__wasi_filestat_t> {
host_impl::filestat_from_win(file)
}
pub(crate) fn path_filestat_get(
resolved: PathGet,
dirflags: wasi::__wasi_lookupflags_t,
) -> WasiResult<wasi::__wasi_filestat_t> {
let path = resolved.concatenate()?;
let file = File::open(path)?;
host_impl::filestat_from_win(&file)
}
pub(crate) fn path_filestat_set_times(
resolved: PathGet,
dirflags: wasi::__wasi_lookupflags_t,
st_atim: wasi::__wasi_timestamp_t,
mut st_mtim: wasi::__wasi_timestamp_t,
fst_flags: wasi::__wasi_fstflags_t,
) -> WasiResult<()> {
use winx::file::AccessMode;
let path = resolved.concatenate()?;
let file = OpenOptions::new()
.access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits())
.open(path)?;
let modifiable_fd = Descriptor::OsHandle(OsHandle::from(file));
fd_filestat_set_times_impl(&modifiable_fd, st_atim, st_mtim, fst_flags)
}
pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> WasiResult<()> {
use std::os::windows::fs::{symlink_dir, symlink_file};
let old_path = concatenate(&resolved.dirfd().as_os_handle(), Path::new(old_path))?;
let new_path = resolved.concatenate()?;
// try creating a file symlink
let err = match symlink_file(&old_path, &new_path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_symlink at symlink_file error code={:?}", code);
match code as u32 {
winerror::ERROR_NOT_A_REPARSE_POINT => {
// try creating a dir symlink instead
return symlink_dir(old_path, new_path).map_err(Into::into);
}
winerror::ERROR_ACCESS_DENIED => {
// does the target exist?
if new_path.exists() {
return Err(WasiError::EEXIST);
}
}
winerror::ERROR_INVALID_NAME => {
// does the target without trailing slashes exist?
if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? {
if path.exists() {
return Err(WasiError::EEXIST);
}
}
}
_ => {}
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(WasiError::EIO)
}
}
}
pub(crate) fn path_unlink_file(resolved: PathGet) -> WasiResult<()> {
use std::fs;
let path = resolved.concatenate()?;
let file_type = path
.symlink_metadata()
.map(|metadata| metadata.file_type())?;
// check if we're unlinking a symlink
// NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt]
// stabilises
//
// [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html
if file_type.is_symlink() {
let err = match fs::remove_file(&path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_unlink_file at symlink_file error code={:?}", code);
if code as u32 == winerror::ERROR_ACCESS_DENIED {
// try unlinking a dir symlink instead
return fs::remove_dir(path).map_err(Into::into);
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(WasiError::EIO)
}
}
} else if file_type.is_dir() {
Err(WasiError::EISDIR)
} else if file_type.is_file() {
fs::remove_file(path).map_err(Into::into)
} else {
Err(WasiError::EINVAL)
}
}
pub(crate) fn path_remove_directory(resolved: PathGet) -> WasiResult<()> {
let path = resolved.concatenate()?;
std::fs::remove_dir(&path).map_err(Into::into)
}

View File

@@ -1,145 +0,0 @@
#![allow(non_camel_case_types)]
use crate::entry::Descriptor;
use crate::hostcalls_impl::PathGet;
use crate::wasi::{self, WasiError, WasiResult};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use winapi::shared::winerror;
pub(crate) trait PathGetExt {
fn concatenate(&self) -> WasiResult<PathBuf>;
}
impl PathGetExt for PathGet {
fn concatenate(&self) -> WasiResult<PathBuf> {
match self.dirfd() {
Descriptor::OsHandle(file) => concatenate(file, Path::new(self.path())),
Descriptor::VirtualFile(_virt) => {
panic!("concatenate on a virtual base");
}
Descriptor::Stdin | Descriptor::Stdout | Descriptor::Stderr => {
unreachable!("streams do not have paths and should not be accessible via PathGet");
}
}
}
}
pub(crate) fn path_open_rights(
rights_base: wasi::__wasi_rights_t,
rights_inheriting: wasi::__wasi_rights_t,
oflags: wasi::__wasi_oflags_t,
fdflags: wasi::__wasi_fdflags_t,
) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) {
// which rights are needed on the dirfd?
let mut needed_base = wasi::__WASI_RIGHTS_PATH_OPEN;
let mut needed_inheriting = rights_base | rights_inheriting;
// convert open flags
if oflags & wasi::__WASI_OFLAGS_CREAT != 0 {
needed_base |= wasi::__WASI_RIGHTS_PATH_CREATE_FILE;
} else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 {
needed_base |= wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_SIZE;
}
// convert file descriptor flags
if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0
|| fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0
|| fdflags & wasi::__WASI_FDFLAGS_SYNC != 0
{
needed_inheriting |= wasi::__WASI_RIGHTS_FD_DATASYNC;
needed_inheriting |= wasi::__WASI_RIGHTS_FD_SYNC;
}
(needed_base, needed_inheriting)
}
pub(crate) fn openat(dirfd: &File, path: &str) -> WasiResult<File> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
use winx::file::Flags;
let path = concatenate(dirfd, Path::new(path))?;
let err = match OpenOptions::new()
.read(true)
.custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits())
.open(&path)
{
Ok(file) => return Ok(file),
Err(e) => e,
};
if let Some(code) = err.raw_os_error() {
log::debug!("openat error={:?}", code);
if code as u32 == winerror::ERROR_INVALID_NAME {
return Err(WasiError::ENOTDIR);
}
}
Err(err.into())
}
pub(crate) fn readlinkat(dirfd: &File, s_path: &str) -> WasiResult<String> {
use winx::file::get_file_path;
let path = concatenate(dirfd, Path::new(s_path))?;
let err = match path.read_link() {
Ok(target_path) => {
// since on Windows we are effectively emulating 'at' syscalls
// we need to strip the prefix from the absolute path
// as otherwise we will error out since WASI is not capable
// of dealing with absolute paths
let dir_path = get_file_path(dirfd)?;
let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
let target_path = target_path
.strip_prefix(dir_path)
.map_err(|_| WasiError::ENOTCAPABLE)?;
let target_path = target_path.to_str().ok_or(WasiError::EILSEQ)?;
return Ok(target_path.to_owned());
}
Err(e) => e,
};
if let Some(code) = err.raw_os_error() {
log::debug!("readlinkat error={:?}", code);
if code as u32 == winerror::ERROR_INVALID_NAME {
if s_path.ends_with('/') {
// strip "/" and check if exists
let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?;
if path.exists() && !path.is_dir() {
return Err(WasiError::ENOTDIR);
}
}
}
}
Err(err.into())
}
pub(crate) fn strip_extended_prefix<P: AsRef<OsStr>>(path: P) -> OsString {
let path: Vec<u16> = path.as_ref().encode_wide().collect();
if &[92, 92, 63, 92] == &path[0..4] {
OsString::from_wide(&path[4..])
} else {
OsString::from_wide(&path)
}
}
pub(crate) fn concatenate<P: AsRef<Path>>(file: &File, path: P) -> WasiResult<PathBuf> {
use winx::file::get_file_path;
// WASI is not able to deal with absolute paths
// so error out if absolute
if path.as_ref().is_absolute() {
return Err(WasiError::ENOTCAPABLE);
}
let dir_path = get_file_path(file)?;
// concatenate paths
let mut out_path = PathBuf::from(dir_path);
out_path.push(path.as_ref());
// strip extended prefix; otherwise we will error out on any relative
// components with `out_path`
let out_path = PathBuf::from(strip_extended_prefix(out_path));
log::debug!("out_path={:?}", out_path);
Ok(out_path)
}

View File

@@ -1,8 +0,0 @@
//! Windows-specific hostcalls that implement
//! [WASI](https://github.com/WebAssembly/WASI).
mod fs;
pub(crate) mod fs_helpers;
mod misc;
pub(crate) use self::fs::*;
pub(crate) use self::misc::*;

View File

@@ -1,16 +1,23 @@
pub(crate) mod entry_impl;
pub(crate) mod host_impl;
pub(crate) mod hostcalls_impl;
pub(crate) mod clock;
pub(crate) mod entry;
pub(crate) mod fd;
pub(crate) mod path;
pub(crate) mod poll;
use crate::wasi::{types, Errno, Result};
use std::convert::{TryFrom, TryInto};
use std::fs::{File, OpenOptions};
use std::io::Result;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{io, string};
use winapi::shared::winerror;
use winx::file::{CreationDisposition, Flags};
pub(crate) fn dev_null() -> Result<File> {
pub(crate) fn dev_null() -> io::Result<File> {
OpenOptions::new().read(true).write(true).open("NUL")
}
pub fn preopen_dir<P: AsRef<Path>>(path: P) -> Result<File> {
pub fn preopen_dir<P: AsRef<Path>>(path: P) -> io::Result<File> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
use winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS;
@@ -25,3 +32,132 @@ pub fn preopen_dir<P: AsRef<Path>>(path: P) -> Result<File> {
.attributes(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
}
pub(crate) fn file_serial_no(file: &File) -> io::Result<u64> {
let info = winx::file::get_fileinfo(file)?;
let high = info.nFileIndexHigh;
let low = info.nFileIndexLow;
let no = (u64::from(high) << 32) | u64::from(low);
Ok(no)
}
impl From<io::Error> for Errno {
fn from(err: io::Error) -> Self {
match err.raw_os_error() {
Some(code) => match code as u32 {
winerror::ERROR_SUCCESS => Self::Success,
winerror::ERROR_BAD_ENVIRONMENT => Self::TooBig,
winerror::ERROR_FILE_NOT_FOUND => Self::Noent,
winerror::ERROR_PATH_NOT_FOUND => Self::Noent,
winerror::ERROR_TOO_MANY_OPEN_FILES => Self::Nfile,
winerror::ERROR_ACCESS_DENIED => Self::Acces,
winerror::ERROR_SHARING_VIOLATION => Self::Acces,
winerror::ERROR_PRIVILEGE_NOT_HELD => Self::Notcapable,
winerror::ERROR_INVALID_HANDLE => Self::Badf,
winerror::ERROR_INVALID_NAME => Self::Noent,
winerror::ERROR_NOT_ENOUGH_MEMORY => Self::Nomem,
winerror::ERROR_OUTOFMEMORY => Self::Nomem,
winerror::ERROR_DIR_NOT_EMPTY => Self::Notempty,
winerror::ERROR_NOT_READY => Self::Busy,
winerror::ERROR_BUSY => Self::Busy,
winerror::ERROR_NOT_SUPPORTED => Self::Notsup,
winerror::ERROR_FILE_EXISTS => Self::Exist,
winerror::ERROR_BROKEN_PIPE => Self::Pipe,
winerror::ERROR_BUFFER_OVERFLOW => Self::Nametoolong,
winerror::ERROR_NOT_A_REPARSE_POINT => Self::Inval,
winerror::ERROR_NEGATIVE_SEEK => Self::Inval,
winerror::ERROR_DIRECTORY => Self::Notdir,
winerror::ERROR_ALREADY_EXISTS => Self::Exist,
x => {
log::debug!("unknown error value: {}", x);
Self::Io
}
},
None => {
log::debug!("Other I/O error: {}", err);
Self::Io
}
}
}
}
impl From<string::FromUtf16Error> for Errno {
fn from(_err: string::FromUtf16Error) -> Self {
Self::Ilseq
}
}
fn num_hardlinks(file: &File) -> io::Result<u64> {
Ok(winx::file::get_fileinfo(file)?.nNumberOfLinks.into())
}
fn device_id(file: &File) -> io::Result<u64> {
Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into())
}
fn change_time(file: &File) -> io::Result<i64> {
winx::file::change_time(file)
}
fn systemtime_to_timestamp(st: SystemTime) -> Result<u64> {
st.duration_since(UNIX_EPOCH)
.map_err(|_| Errno::Inval)? // date earlier than UNIX_EPOCH
.as_nanos()
.try_into()
.map_err(Into::into) // u128 doesn't fit into u64
}
impl TryFrom<&File> for types::Filestat {
type Error = Errno;
fn try_from(file: &File) -> Result<Self> {
let metadata = file.metadata()?;
Ok(types::Filestat {
dev: device_id(file)?,
ino: file_serial_no(file)?,
nlink: num_hardlinks(file)?.try_into()?, // u64 doesn't fit into u32
size: metadata.len(),
atim: systemtime_to_timestamp(metadata.accessed()?)?,
ctim: change_time(file)?.try_into()?, // i64 doesn't fit into u64
mtim: systemtime_to_timestamp(metadata.modified()?)?,
filetype: metadata.file_type().into(),
})
}
}
impl From<types::Oflags> for CreationDisposition {
fn from(oflags: types::Oflags) -> Self {
if oflags.contains(&types::Oflags::CREAT) {
if oflags.contains(&types::Oflags::EXCL) {
CreationDisposition::CREATE_NEW
} else {
CreationDisposition::CREATE_ALWAYS
}
} else if oflags.contains(&types::Oflags::TRUNC) {
CreationDisposition::TRUNCATE_EXISTING
} else {
CreationDisposition::OPEN_EXISTING
}
}
}
impl From<types::Fdflags> for Flags {
fn from(fdflags: types::Fdflags) -> Self {
// Enable backup semantics so directories can be opened as files
let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS;
// Note: __WASI_FDFLAGS_NONBLOCK is purposely being ignored for files
// While Windows does inherently support a non-blocking mode on files, the WASI API will
// treat I/O operations on files as synchronous. WASI might have an async-io API in the future.
// Technically, Windows only supports __WASI_FDFLAGS_SYNC, but treat all the flags as the same.
if fdflags.contains(&types::Fdflags::DSYNC)
|| fdflags.contains(&types::Fdflags::RSYNC)
|| fdflags.contains(&types::Fdflags::SYNC)
{
flags.insert(Flags::FILE_FLAG_WRITE_THROUGH);
}
flags
}
}

View File

@@ -0,0 +1,506 @@
use crate::entry::Descriptor;
use crate::fd;
use crate::path::PathGet;
use crate::sys::entry::OsHandle;
use crate::wasi::{types, Errno, Result};
use std::convert::TryInto;
use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use winapi::shared::winerror;
use winx::file::AccessMode;
/// Creates owned WASI path from OS string.
///
/// NB WASI spec requires OS string to be valid UTF-8. Otherwise,
/// `__WASI_ERRNO_ILSEQ` error is returned.
pub(crate) fn from_host<S: AsRef<OsStr>>(s: S) -> Result<String> {
let vec: Vec<u16> = s.as_ref().encode_wide().collect();
let s = String::from_utf16(&vec)?;
Ok(s)
}
pub(crate) trait PathGetExt {
fn concatenate(&self) -> Result<PathBuf>;
}
impl PathGetExt for PathGet {
fn concatenate(&self) -> Result<PathBuf> {
match self.dirfd() {
Descriptor::OsHandle(file) => concatenate(file, Path::new(self.path())),
Descriptor::VirtualFile(_virt) => {
panic!("concatenate on a virtual base");
}
Descriptor::Stdin | Descriptor::Stdout | Descriptor::Stderr => {
unreachable!("streams do not have paths and should not be accessible via PathGet");
}
}
}
}
pub(crate) fn open_rights(
rights_base: types::Rights,
rights_inheriting: types::Rights,
oflags: types::Oflags,
fdflags: types::Fdflags,
) -> (types::Rights, types::Rights) {
// which rights are needed on the dirfd?
let mut needed_base = types::Rights::PATH_OPEN;
let mut needed_inheriting = rights_base | rights_inheriting;
// convert open flags
if oflags.contains(&types::Oflags::CREAT) {
needed_base |= types::Rights::PATH_CREATE_FILE;
} else if oflags.contains(&types::Oflags::TRUNC) {
needed_base |= types::Rights::PATH_FILESTAT_SET_SIZE;
}
// convert file descriptor flags
if fdflags.contains(&types::Fdflags::DSYNC)
|| fdflags.contains(&types::Fdflags::RSYNC)
|| fdflags.contains(&types::Fdflags::SYNC)
{
needed_inheriting |= types::Rights::FD_DATASYNC;
needed_inheriting |= types::Rights::FD_SYNC;
}
(needed_base, needed_inheriting)
}
pub(crate) fn openat(dirfd: &File, path: &str) -> Result<File> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
use winx::file::Flags;
let path = concatenate(dirfd, Path::new(path))?;
let err = match OpenOptions::new()
.read(true)
.custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits())
.open(&path)
{
Ok(file) => return Ok(file),
Err(e) => e,
};
if let Some(code) = err.raw_os_error() {
log::debug!("openat error={:?}", code);
if code as u32 == winerror::ERROR_INVALID_NAME {
return Err(Errno::Notdir);
}
}
Err(err.into())
}
pub(crate) fn readlinkat(dirfd: &File, s_path: &str) -> Result<String> {
use winx::file::get_file_path;
let path = concatenate(dirfd, Path::new(s_path))?;
let err = match path.read_link() {
Ok(target_path) => {
// since on Windows we are effectively emulating 'at' syscalls
// we need to strip the prefix from the absolute path
// as otherwise we will error out since WASI is not capable
// of dealing with absolute paths
let dir_path = get_file_path(dirfd)?;
let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
let target_path = target_path
.strip_prefix(dir_path)
.map_err(|_| Errno::Notcapable)?;
let target_path = target_path.to_str().ok_or(Errno::Ilseq)?;
return Ok(target_path.to_owned());
}
Err(e) => e,
};
if let Some(code) = err.raw_os_error() {
log::debug!("readlinkat error={:?}", code);
if code as u32 == winerror::ERROR_INVALID_NAME {
if s_path.ends_with('/') {
// strip "/" and check if exists
let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?;
if path.exists() && !path.is_dir() {
return Err(Errno::Notdir);
}
}
}
}
Err(err.into())
}
fn strip_extended_prefix<P: AsRef<OsStr>>(path: P) -> OsString {
let path: Vec<u16> = path.as_ref().encode_wide().collect();
if &[92, 92, 63, 92] == &path[0..4] {
OsString::from_wide(&path[4..])
} else {
OsString::from_wide(&path)
}
}
fn concatenate<P: AsRef<Path>>(file: &File, path: P) -> Result<PathBuf> {
use winx::file::get_file_path;
// WASI is not able to deal with absolute paths
// so error out if absolute
if path.as_ref().is_absolute() {
return Err(Errno::Notcapable);
}
let dir_path = get_file_path(file)?;
// concatenate paths
let mut out_path = PathBuf::from(dir_path);
out_path.push(path.as_ref());
// strip extended prefix; otherwise we will error out on any relative
// components with `out_path`
let out_path = PathBuf::from(strip_extended_prefix(out_path));
log::debug!("out_path={:?}", out_path);
Ok(out_path)
}
pub(crate) fn create_directory(file: &File, path: &str) -> Result<()> {
let path = concatenate(file, path)?;
std::fs::create_dir(&path)?;
Ok(())
}
pub(crate) fn link(
_resolved_old: PathGet,
_resolved_new: PathGet,
_follow_symlinks: bool,
) -> Result<()> {
unimplemented!("path_link")
}
pub(crate) fn open(
resolved: PathGet,
read: bool,
write: bool,
oflags: types::Oflags,
fdflags: types::Fdflags,
) -> Result<Descriptor> {
use winx::file::{AccessMode, CreationDisposition, Flags};
let is_trunc = oflags.contains(&types::Oflags::TRUNC);
if is_trunc {
// Windows does not support append mode when opening for truncation
// This is because truncation requires `GENERIC_WRITE` access, which will override the removal
// of the `FILE_WRITE_DATA` permission.
if fdflags.contains(&types::Fdflags::APPEND) {
return Err(Errno::Notsup);
}
}
// convert open flags
// note: the calls to `write(true)` are to bypass an internal OpenOption check
// the write flag will ultimately be ignored when `access_mode` is calculated below.
let mut opts = OpenOptions::new();
match oflags.into() {
CreationDisposition::CREATE_ALWAYS => {
opts.create(true).write(true);
}
CreationDisposition::CREATE_NEW => {
opts.create_new(true).write(true);
}
CreationDisposition::TRUNCATE_EXISTING => {
opts.truncate(true).write(true);
}
_ => {}
}
let path = resolved.concatenate()?;
match path.symlink_metadata().map(|metadata| metadata.file_type()) {
Ok(file_type) => {
// check if we are trying to open a symlink
if file_type.is_symlink() {
return Err(Errno::Loop);
}
// check if we are trying to open a file as a dir
if file_type.is_file() && oflags.contains(&types::Oflags::DIRECTORY) {
return Err(Errno::Notdir);
}
}
Err(err) => match err.raw_os_error() {
Some(code) => {
log::debug!("path_open at symlink_metadata error code={:?}", code);
if code as u32 != winerror::ERROR_FILE_NOT_FOUND {
return Err(err.into());
}
// file not found, let it proceed to actually
// trying to open it
}
None => {
log::debug!("Inconvertible OS error: {}", err);
return Err(Errno::Io);
}
},
}
let mut access_mode = file_access_mode_from_fdflags(fdflags, read, write);
// Truncation requires the special `GENERIC_WRITE` bit set (this is why it doesn't work with append-only mode)
if is_trunc {
access_mode |= AccessMode::GENERIC_WRITE;
}
let flags: Flags = fdflags.into();
opts.access_mode(access_mode.bits())
.custom_flags(flags.bits())
.open(&path)
.map(|f| OsHandle::from(f).into())
.map_err(Into::into)
}
fn file_access_mode_from_fdflags(fdflags: types::Fdflags, read: bool, write: bool) -> AccessMode {
let mut access_mode = AccessMode::READ_CONTROL;
// Note that `GENERIC_READ` and `GENERIC_WRITE` cannot be used to properly support append-only mode
// The file-specific flags `FILE_GENERIC_READ` and `FILE_GENERIC_WRITE` are used here instead
// These flags have the same semantic meaning for file objects, but allow removal of specific permissions (see below)
if read {
access_mode.insert(AccessMode::FILE_GENERIC_READ);
}
if write {
access_mode.insert(AccessMode::FILE_GENERIC_WRITE);
}
// For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA.
// This makes the handle "append only".
// Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior).
if fdflags.contains(&types::Fdflags::APPEND) {
access_mode.insert(AccessMode::FILE_APPEND_DATA);
access_mode.remove(AccessMode::FILE_WRITE_DATA);
}
access_mode
}
pub(crate) fn readlink(resolved: PathGet, buf: &mut [u8]) -> Result<usize> {
use winx::file::get_file_path;
let path = resolved.concatenate()?;
let target_path = path.read_link()?;
// since on Windows we are effectively emulating 'at' syscalls
// we need to strip the prefix from the absolute path
// as otherwise we will error out since WASI is not capable
// of dealing with absolute paths
let dir_path = get_file_path(&resolved.dirfd().as_os_handle())?;
let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
let target_path = target_path
.strip_prefix(dir_path)
.map_err(|_| Errno::Notcapable)
.and_then(|path| path.to_str().map(String::from).ok_or(Errno::Ilseq))?;
if buf.len() > 0 {
let mut chars = target_path.chars();
let mut nread = 0usize;
for i in 0..buf.len() {
match chars.next() {
Some(ch) => {
buf[i] = ch as u8;
nread += 1;
}
None => break,
}
}
Ok(nread)
} else {
Ok(0)
}
}
fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result<Option<PathBuf>> {
if resolved.path().ends_with('/') {
let suffix = resolved.path().trim_end_matches('/');
concatenate(&resolved.dirfd().as_os_handle(), Path::new(suffix)).map(Some)
} else {
Ok(None)
}
}
pub(crate) fn rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> {
use std::fs;
let old_path = resolved_old.concatenate()?;
let new_path = resolved_new.concatenate()?;
// First sanity check: check we're not trying to rename dir to file or vice versa.
// NB on Windows, the former is actually permitted [std::fs::rename].
//
// [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html
if old_path.is_dir() && new_path.is_file() {
return Err(Errno::Notdir);
}
// Second sanity check: check we're not trying to rename a file into a path
// ending in a trailing slash.
if old_path.is_file() && resolved_new.path().ends_with('/') {
return Err(Errno::Notdir);
}
// TODO handle symlinks
let err = match fs::rename(&old_path, &new_path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_rename at rename error code={:?}", code);
match code as u32 {
winerror::ERROR_ACCESS_DENIED => {
// So most likely dealing with new_path == dir.
// Eliminate case old_path == file first.
if old_path.is_file() {
return Err(Errno::Isdir);
} else {
// Ok, let's try removing an empty dir at new_path if it exists
// and is a nonempty dir.
fs::remove_dir(&new_path)?;
fs::rename(old_path, new_path)?;
return Ok(());
}
}
winerror::ERROR_INVALID_NAME => {
// If source contains trailing slashes, check if we are dealing with
// a file instead of a dir, and if so, throw ENOTDIR.
if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? {
if path.is_file() {
return Err(Errno::Notdir);
}
}
}
_ => {}
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(Errno::Io)
}
}
}
pub(crate) fn filestat_get(
resolved: PathGet,
_dirflags: types::Lookupflags,
) -> Result<types::Filestat> {
let path = resolved.concatenate()?;
let file = File::open(path)?;
let filestat = (&file).try_into()?;
Ok(filestat)
}
pub(crate) fn filestat_set_times(
resolved: PathGet,
_dirflags: types::Lookupflags,
st_atim: types::Timestamp,
st_mtim: types::Timestamp,
fst_flags: types::Fstflags,
) -> Result<()> {
use winx::file::AccessMode;
let path = resolved.concatenate()?;
let file = OpenOptions::new()
.access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits())
.open(path)?;
let modifiable_fd = Descriptor::OsHandle(OsHandle::from(file));
fd::filestat_set_times_impl(&modifiable_fd, st_atim, st_mtim, fst_flags)
}
pub(crate) fn symlink(old_path: &str, resolved: PathGet) -> Result<()> {
use std::os::windows::fs::{symlink_dir, symlink_file};
let old_path = concatenate(&resolved.dirfd().as_os_handle(), Path::new(old_path))?;
let new_path = resolved.concatenate()?;
// try creating a file symlink
let err = match symlink_file(&old_path, &new_path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_symlink at symlink_file error code={:?}", code);
match code as u32 {
winerror::ERROR_NOT_A_REPARSE_POINT => {
// try creating a dir symlink instead
return symlink_dir(old_path, new_path).map_err(Into::into);
}
winerror::ERROR_ACCESS_DENIED => {
// does the target exist?
if new_path.exists() {
return Err(Errno::Exist);
}
}
winerror::ERROR_INVALID_NAME => {
// does the target without trailing slashes exist?
if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? {
if path.exists() {
return Err(Errno::Exist);
}
}
}
_ => {}
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(Errno::Io)
}
}
}
pub(crate) fn unlink_file(resolved: PathGet) -> Result<()> {
use std::fs;
let path = resolved.concatenate()?;
let file_type = path
.symlink_metadata()
.map(|metadata| metadata.file_type())?;
// check if we're unlinking a symlink
// NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt]
// stabilises
//
// [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html
if file_type.is_symlink() {
let err = match fs::remove_file(&path) {
Ok(()) => return Ok(()),
Err(e) => e,
};
match err.raw_os_error() {
Some(code) => {
log::debug!("path_unlink_file at symlink_file error code={:?}", code);
if code as u32 == winerror::ERROR_ACCESS_DENIED {
// try unlinking a dir symlink instead
return fs::remove_dir(path).map_err(Into::into);
}
Err(err.into())
}
None => {
log::debug!("Inconvertible OS error: {}", err);
Err(Errno::Io)
}
}
} else if file_type.is_dir() {
Err(Errno::Isdir)
} else if file_type.is_file() {
fs::remove_file(path).map_err(Into::into)
} else {
Err(Errno::Inval)
}
}
pub(crate) fn remove_directory(resolved: PathGet) -> Result<()> {
let path = resolved.concatenate()?;
std::fs::remove_dir(&path).map_err(Into::into)
}

View File

@@ -1,22 +1,14 @@
#![allow(non_camel_case_types)]
#![allow(unused_unsafe)]
#![allow(unused)]
use crate::entry::Descriptor;
use crate::hostcalls_impl::{ClockEventData, FdEventData};
use crate::memory::*;
use crate::sys::host_impl;
use crate::wasi::{self, WasiError, WasiResult};
use crate::wasi32;
use cpu_time::{ProcessTime, ThreadTime};
use crate::poll::{ClockEventData, FdEventData};
use crate::wasi::{types, Errno, Result};
use lazy_static::lazy_static;
use log::{debug, error, trace, warn};
use std::convert::TryInto;
use std::io;
use std::os::windows::io::AsRawHandle;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::time::Duration;
struct StdinPoll {
request_tx: Sender<()>,
@@ -27,7 +19,7 @@ enum PollState {
Ready,
NotReady, // it's not ready, but we didn't wait
TimedOut, // it's not ready and a timeout has occurred
Error(WasiError),
Error(Errno),
}
enum WaitMode {
@@ -83,7 +75,7 @@ impl StdinPoll {
// Linux returns `POLLIN` in both cases, and we imitate this behavior.
let resp = match std::io::stdin().lock().fill_buf() {
Ok(_) => PollState::Ready,
Err(e) => PollState::Error(WasiError::from(e)),
Err(e) => PollState::Error(Errno::from(e)),
};
// Notify the requestor about data in stdin. They may have already timed out,
@@ -94,8 +86,6 @@ impl StdinPoll {
}
lazy_static! {
static ref START_MONOTONIC: Instant = Instant::now();
static ref PERF_COUNTER_RES: u64 = get_perf_counter_resolution_ns();
static ref STDIN_POLL: Mutex<StdinPoll> = {
let (request_tx, request_rx) = mpsc::channel();
let (notify_tx, notify_rx) = mpsc::channel();
@@ -107,92 +97,30 @@ lazy_static! {
};
}
// Timer resolution on Windows is really hard. We may consider exposing the resolution of the respective
// timers as an associated function in the future.
pub(crate) fn clock_res_get(
clock_id: wasi::__wasi_clockid_t,
) -> WasiResult<wasi::__wasi_timestamp_t> {
Ok(match clock_id {
// This is the best that we can do with std::time::SystemTime.
// Rust uses GetSystemTimeAsFileTime, which is said to have the resolution of
// 10ms or 55ms, [1] but MSDN doesn't confirm this in any way.
// Even the MSDN article on high resolution timestamps doesn't even mention the precision
// for this method. [3]
//
// The timer resolution can be queried using one of the functions: [2, 5]
// * NtQueryTimerResolution, which is undocumented and thus not exposed by the winapi crate
// * timeGetDevCaps, which returns the upper and lower bound for the precision, in ms.
// While the upper bound seems like something we could use, it's typically too high to be meaningful.
// For instance, the intervals return by the syscall are:
// * [1, 65536] on Wine
// * [1, 1000000] on Windows 10, which is up to (sic) 1000 seconds.
//
// It's possible to manually set the timer resolution, but this sounds like something which should
// only be done temporarily. [5]
//
// Alternatively, we could possibly use GetSystemTimePreciseAsFileTime in clock_time_get, but
// this syscall is only available starting from Windows 8.
// (we could possibly emulate it on earlier versions of Windows, see [4])
// The MSDN are not clear on the resolution of GetSystemTimePreciseAsFileTime either, but a
// Microsoft devblog entry [1] suggests that it kind of combines GetSystemTimeAsFileTime with
// QueryPeformanceCounter, which probably means that those two should have the same resolution.
//
// See also this discussion about the use of GetSystemTimePreciseAsFileTime in Python stdlib,
// which in particular contains some resolution benchmarks.
//
// [1] https://devblogs.microsoft.com/oldnewthing/20170921-00/?p=97057
// [2] http://www.windowstimestamp.com/description
// [3] https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN
// [4] https://www.codeproject.com/Tips/1011902/High-Resolution-Time-For-Windows
// [5] https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly
// [6] https://bugs.python.org/issue19007
wasi::__WASI_CLOCKID_REALTIME => 55_000_000,
// std::time::Instant uses QueryPerformanceCounter & QueryPerformanceFrequency internally
wasi::__WASI_CLOCKID_MONOTONIC => *PERF_COUNTER_RES,
// The best we can do is to hardcode the value from the docs.
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes
wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => 100,
// The best we can do is to hardcode the value from the docs.
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes
wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => 100,
_ => return Err(WasiError::EINVAL),
})
}
pub(crate) fn clock_time_get(
clock_id: wasi::__wasi_clockid_t,
) -> WasiResult<wasi::__wasi_timestamp_t> {
let duration = match clock_id {
wasi::__WASI_CLOCKID_REALTIME => get_monotonic_time(),
wasi::__WASI_CLOCKID_MONOTONIC => get_realtime_time()?,
wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => get_proc_cputime()?,
wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => get_thread_cputime()?,
_ => return Err(WasiError::EINVAL),
};
duration.as_nanos().try_into().map_err(Into::into)
}
fn make_rw_event(event: &FdEventData, nbytes: WasiResult<u64>) -> wasi::__wasi_event_t {
fn make_rw_event(event: &FdEventData, nbytes: Result<u64>) -> types::Event {
let (nbytes, error) = match nbytes {
Ok(nbytes) => (nbytes, WasiError::ESUCCESS),
Ok(nbytes) => (nbytes, Errno::Success),
Err(e) => (u64::default(), e),
};
wasi::__wasi_event_t {
types::Event {
userdata: event.userdata,
r#type: event.r#type,
error: error.as_raw_errno(),
fd_readwrite: wasi::__wasi_event_fd_readwrite_t { nbytes, flags: 0 },
type_: event.r#type,
error,
fd_readwrite: types::EventFdReadwrite {
nbytes,
flags: types::Eventrwflags::empty(),
},
}
}
fn make_timeout_event(timeout: &ClockEventData) -> wasi::__wasi_event_t {
wasi::__wasi_event_t {
fn make_timeout_event(timeout: &ClockEventData) -> types::Event {
types::Event {
userdata: timeout.userdata,
r#type: wasi::__WASI_EVENTTYPE_CLOCK,
error: wasi::__WASI_ERRNO_SUCCESS,
fd_readwrite: wasi::__wasi_event_fd_readwrite_t {
type_: types::Eventtype::Clock,
error: Errno::Success,
fd_readwrite: types::EventFdReadwrite {
nbytes: 0,
flags: 0,
flags: types::Eventrwflags::empty(),
},
}
}
@@ -200,21 +128,22 @@ fn make_timeout_event(timeout: &ClockEventData) -> wasi::__wasi_event_t {
fn handle_timeout(
timeout_event: ClockEventData,
timeout: Duration,
events: &mut Vec<wasi::__wasi_event_t>,
events: &mut Vec<types::Event>,
) {
thread::sleep(timeout);
handle_timeout_event(timeout_event, events);
}
fn handle_timeout_event(timeout_event: ClockEventData, events: &mut Vec<wasi::__wasi_event_t>) {
fn handle_timeout_event(timeout_event: ClockEventData, events: &mut Vec<types::Event>) {
let new_event = make_timeout_event(&timeout_event);
events.push(new_event);
}
fn handle_rw_event(event: FdEventData, out_events: &mut Vec<wasi::__wasi_event_t>) {
let size = match event.descriptor {
fn handle_rw_event(event: FdEventData, out_events: &mut Vec<types::Event>) {
let descriptor: &Descriptor = &event.descriptor;
let size = match descriptor {
Descriptor::OsHandle(os_handle) => {
if event.r#type == wasi::__WASI_EVENTTYPE_FD_READ {
if event.r#type == types::Eventtype::FdRead {
os_handle.metadata().map(|m| m.len()).map_err(Into::into)
} else {
// The spec is unclear what nbytes should actually be for __WASI_EVENTTYPE_FD_WRITE and
@@ -237,23 +166,16 @@ fn handle_rw_event(event: FdEventData, out_events: &mut Vec<wasi::__wasi_event_t
out_events.push(new_event);
}
fn handle_error_event(
event: FdEventData,
error: WasiError,
out_events: &mut Vec<wasi::__wasi_event_t>,
) {
fn handle_error_event(event: FdEventData, error: Errno, out_events: &mut Vec<types::Event>) {
let new_event = make_rw_event(&event, Err(error));
out_events.push(new_event);
}
pub(crate) fn poll_oneoff(
pub(crate) fn oneoff(
timeout: Option<ClockEventData>,
fd_events: Vec<FdEventData>,
events: &mut Vec<wasi::__wasi_event_t>,
) -> WasiResult<()> {
use std::fs::Metadata;
use std::thread;
events: &mut Vec<types::Event>,
) -> Result<()> {
let timeout = timeout
.map(|event| {
event
@@ -279,8 +201,9 @@ pub(crate) fn poll_oneoff(
let mut pipe_events = vec![];
for event in fd_events {
match event.descriptor {
Descriptor::Stdin if event.r#type == wasi::__WASI_EVENTTYPE_FD_READ => {
let descriptor: &Descriptor = &event.descriptor;
match descriptor {
Descriptor::Stdin if event.r#type == types::Eventtype::FdRead => {
stdin_events.push(event)
}
// stdout/stderr are always considered ready to write because there seems to
@@ -295,7 +218,7 @@ pub(crate) fn poll_oneoff(
let ftype = unsafe { winx::file::get_file_type(os_handle.as_raw_handle()) }?;
if ftype.is_unknown() || ftype.is_char() {
debug!("poll_oneoff: unsupported file type: {:?}", ftype);
handle_error_event(event, WasiError::ENOTSUP, events);
handle_error_event(event, Errno::Notsup, events);
} else if ftype.is_disk() {
immediate_events.push(event);
} else if ftype.is_pipe() {
@@ -314,12 +237,11 @@ pub(crate) fn poll_oneoff(
// Process all the events that do not require waiting.
if immediate {
trace!(" | have immediate events, will return immediately");
for mut event in immediate_events {
for event in immediate_events {
handle_rw_event(event, events);
}
}
if !stdin_events.is_empty() {
// During the firt request to poll stdin, we spin up a separate thread to
// waiting for data to arrive on stdin. This thread will not terminate.
//
// We'd like to do the following:
@@ -345,7 +267,7 @@ pub(crate) fn poll_oneoff(
} else {
trace!(" | passively waiting on stdin");
match timeout {
Some((event, dur)) => WaitMode::Timeout(dur),
Some((_event, dur)) => WaitMode::Timeout(dur),
None => WaitMode::Infinite,
}
};
@@ -371,43 +293,10 @@ pub(crate) fn poll_oneoff(
}
None => {
error!("Polling only pipes with no timeout not supported on Windows.");
return Err(WasiError::ENOTSUP);
return Err(Errno::Notsup);
}
}
}
Ok(())
}
fn get_monotonic_time() -> Duration {
// We're circumventing the fact that we can't get a Duration from an Instant
// The epoch of __WASI_CLOCKID_MONOTONIC is undefined, so we fix a time point once
// and count relative to this time point.
//
// The alternative would be to copy over the implementation of std::time::Instant
// to our source tree and add a conversion to std::time::Duration
START_MONOTONIC.elapsed()
}
fn get_realtime_time() -> WasiResult<Duration> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| WasiError::EFAULT)
}
fn get_proc_cputime() -> WasiResult<Duration> {
Ok(ProcessTime::try_now()?.as_duration())
}
fn get_thread_cputime() -> WasiResult<Duration> {
Ok(ThreadTime::try_now()?.as_duration())
}
fn get_perf_counter_resolution_ns() -> u64 {
use winx::time::perf_counter_frequency;
const NANOS_PER_SEC: u64 = 1_000_000_000;
// This should always succeed starting from Windows XP, so it's fine to panic in case of an error.
let freq = perf_counter_frequency().expect("QueryPerformanceFrequency returned an error");
let epsilon = NANOS_PER_SEC / freq;
epsilon
}