use crate::{BufLease, Handle, ReadBuf, Result, WriteBuf};
use std::{path::Path, sync::Arc};
#[derive(Debug)]
struct PathInner {
path: std::path::PathBuf,
#[cfg(unix)]
native: std::ffi::CString,
#[cfg(windows)]
native: Box<[u16]>,
#[cfg(target_os = "wasi")]
text: Box<str>,
}
#[derive(Clone, Debug)]
pub struct FsPath(Arc<PathInner>);
impl FsPath {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
#[cfg(unix)]
let native = {
use std::os::unix::ffi::OsStrExt;
std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| invalid())?
};
#[cfg(windows)]
let native = {
use std::os::windows::ffi::OsStrExt;
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.contains(&0) {
return Err(invalid());
}
wide.push(0);
wide.into_boxed_slice()
};
#[cfg(target_os = "wasi")]
let text = {
let text = path.to_str().ok_or_else(invalid)?;
if text.contains('\0') {
return Err(invalid());
}
text.into()
};
#[cfg(not(any(unix, windows, target_os = "wasi")))]
if path.to_str().is_none_or(|s| s.contains('\0')) {
return Err(invalid());
}
Ok(Self(Arc::new(PathInner {
path: path.to_path_buf(),
#[cfg(any(unix, windows))]
native,
#[cfg(target_os = "wasi")]
text,
})))
}
pub fn as_path(&self) -> &Path {
&self.0.path
}
#[cfg(unix)]
pub(crate) fn native(&self) -> &std::ffi::CStr {
&self.0.native
}
#[cfg(windows)]
pub(crate) fn wide(&self) -> &[u16] {
&self.0.native
}
#[cfg(target_os = "wasi")]
#[allow(dead_code)]
pub(crate) fn text(&self) -> &str {
&self.0.text
}
}
fn invalid() -> crate::Error {
crate::Error::new(crate::ErrorKind::InvalidInput)
}
#[derive(Clone, Copy, Debug)]
pub struct FileOptions {
pub read: bool,
pub write: bool,
pub create: bool,
pub exclusive: bool,
pub truncate: bool,
pub append: bool,
pub sync: bool,
pub data_sync: bool,
pub follow_symlinks: bool,
pub mode: u32,
}
impl Default for FileOptions {
fn default() -> Self {
Self {
read: true,
write: false,
create: false,
exclusive: false,
truncate: false,
append: false,
sync: false,
data_sync: false,
follow_symlinks: true,
mode: 0o666,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum FileType {
Unknown = 0,
File = 1,
Directory = 2,
Symlink = 3,
BlockDevice = 4,
CharDevice = 5,
Fifo = 6,
Socket = 7,
}
impl FileType {
fn from_tag(tag: u8) -> Self {
match tag {
1 => Self::File,
2 => Self::Directory,
3 => Self::Symlink,
4 => Self::BlockDevice,
5 => Self::CharDevice,
6 => Self::Fifo,
7 => Self::Socket,
_ => Self::Unknown,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct FileTime {
pub seconds: i64,
pub nanoseconds: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileMetadata {
pub kind: FileType,
pub size: u64,
pub mode: Option<u32>,
pub device: Option<u64>,
pub inode: Option<u64>,
pub links: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub rdev: Option<u64>,
pub block_size: Option<u64>,
pub blocks: Option<u64>,
pub accessed: Option<FileTime>,
pub modified: Option<FileTime>,
pub changed: Option<FileTime>,
pub created: Option<FileTime>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AccessMode {
pub read: bool,
pub write: bool,
pub execute: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SymlinkKind {
#[default]
File,
Directory,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimeChange {
Keep,
Now,
At(FileTime),
}
#[derive(Clone, Debug)]
pub enum FsTarget {
Path {
path: FsPath,
follow_symlinks: bool,
},
File(Handle),
}
#[derive(Debug)]
pub enum FsRequest {
Open {
path: FsPath,
options: FileOptions,
},
OpenDir {
path: FsPath,
},
Close {
file: Handle,
},
Read {
file: Handle,
buffer: ReadBuf,
offset: Option<u64>,
},
Write {
file: Handle,
buffer: WriteBuf,
offset: Option<u64>,
},
Sync {
file: Handle,
data_only: bool,
},
Truncate {
file: Handle,
size: u64,
},
Stat {
path: FsPath,
follow_symlinks: bool,
},
Fstat {
file: Handle,
},
ReadDir {
dir: Handle,
buffer: ReadBuf,
},
Mkdir {
path: FsPath,
mode: u32,
},
Rmdir {
path: FsPath,
},
Unlink {
path: FsPath,
},
Rename {
from: FsPath,
to: FsPath,
},
Link {
existing: FsPath,
link: FsPath,
},
Symlink {
target: FsPath,
link: FsPath,
kind: SymlinkKind,
},
ReadLink {
path: FsPath,
buffer: ReadBuf,
},
RealPath {
path: FsPath,
buffer: ReadBuf,
},
Access {
path: FsPath,
mode: AccessMode,
},
Chmod {
target: FsTarget,
mode: u32,
},
Chown {
target: FsTarget,
uid: Option<u32>,
gid: Option<u32>,
},
SetTimes {
target: FsTarget,
accessed: TimeChange,
modified: TimeChange,
},
CopyFile {
from: FsPath,
to: FsPath,
exclusive: bool,
},
}
impl FsRequest {
pub(crate) fn handle(&self) -> Option<Handle> {
match self {
Self::Close { file }
| Self::Read { file, .. }
| Self::Write { file, .. }
| Self::Sync { file, .. }
| Self::Truncate { file, .. }
| Self::Fstat { file }
| Self::ReadDir { dir: file, .. }
| Self::Chmod {
target: FsTarget::File(file),
..
}
| Self::Chown {
target: FsTarget::File(file),
..
}
| Self::SetTimes {
target: FsTarget::File(file),
..
} => Some(*file),
_ => None,
}
}
pub(crate) fn opens(&self) -> bool {
matches!(self, Self::Open { .. } | Self::OpenDir { .. })
}
#[cfg_attr(
any(turnloop_backend = "web", turnloop_backend = "unsupported"),
allow(dead_code)
)]
pub(crate) fn read_buffer(&mut self) -> Option<&mut ReadBuf> {
match self {
Self::Read { buffer, .. }
| Self::ReadDir { buffer, .. }
| Self::ReadLink { buffer, .. }
| Self::RealPath { buffer, .. } => Some(buffer),
_ => None,
}
}
}
#[derive(Debug)]
pub enum FsResult {
Opened(Handle),
Read {
n: usize,
lease: Option<BufLease>,
},
Wrote(usize),
Metadata(Metadata),
Directory {
n: usize,
lease: Option<BufLease>,
eof: bool,
},
Bytes {
n: usize,
lease: Option<BufLease>,
},
Done,
}
#[allow(clippy::vec_box)]
pub struct Metadata {
value: Option<Box<FileMetadata>>,
pool: std::rc::Rc<std::cell::RefCell<Vec<Box<FileMetadata>>>>,
}
impl std::ops::Deref for Metadata {
type Target = FileMetadata;
fn deref(&self) -> &FileMetadata {
self.value.as_ref().expect("live metadata")
}
}
impl std::fmt::Debug for Metadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
(**self).fmt(f)
}
}
impl Drop for Metadata {
fn drop(&mut self) {
let mut pool = self.pool.borrow_mut();
if let Some(value) = self.value.take()
&& pool.len() < pool.capacity()
{
pool.push(value);
}
}
}
#[derive(Clone)]
#[allow(clippy::vec_box)]
pub(crate) struct MetadataPool(std::rc::Rc<std::cell::RefCell<Vec<Box<FileMetadata>>>>);
impl MetadataPool {
pub fn new(capacity: usize) -> Self {
Self(std::rc::Rc::new(std::cell::RefCell::new(
Vec::with_capacity(capacity),
)))
}
pub fn lease(&self, metadata: FileMetadata) -> Metadata {
let value = match self.0.borrow_mut().pop() {
Some(mut boxed) => {
*boxed = metadata;
boxed
}
None => Box::new(metadata),
};
Metadata {
value: Some(value),
pool: self.0.clone(),
}
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum FsOutput {
Opened,
Read(usize),
Wrote(usize),
Metadata(FileMetadata),
Directory {
n: usize,
eof: bool,
},
Bytes(usize),
Done,
}
pub const RECORD_HEADER: usize = 4;
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) fn put_record(output: &mut [u8], used: &mut usize, tag: u8, name: &[u8]) -> bool {
let Ok(len) = u16::try_from(name.len()) else {
return false;
};
let end = *used + RECORD_HEADER + name.len();
if end > output.len() {
return false;
}
output[*used] = tag;
output[*used + 1] = 0;
output[*used + 2..*used + 4].copy_from_slice(&len.to_le_bytes());
output[*used + RECORD_HEADER..end].copy_from_slice(name);
*used = end;
true
}
#[derive(Clone, Debug)]
struct Records<'a>(&'a [u8]);
impl<'a> Iterator for Records<'a> {
type Item = (u8, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
if self.0.len() < RECORD_HEADER {
return None;
}
let len = u16::from_le_bytes([self.0[2], self.0[3]]) as usize;
let end = (RECORD_HEADER + len).min(self.0.len());
let record = (self.0[0], &self.0[RECORD_HEADER..end]);
self.0 = &self.0[end..];
Some(record)
}
}
#[derive(Clone, Debug)]
pub struct DirEntries<'a>(Records<'a>);
impl<'a> DirEntries<'a> {
pub fn new(bytes: &'a [u8]) -> Self {
Self(Records(bytes))
}
}
impl<'a> Iterator for DirEntries<'a> {
type Item = (FileType, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
self.0
.next()
.map(|(tag, name)| (FileType::from_tag(tag), name))
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct WatchOptions {
pub recursive: bool,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum WatchKind {
Rename = 1,
Change = 2,
}
#[derive(Clone, Debug)]
pub struct WatchEvents<'a>(Records<'a>);
impl<'a> WatchEvents<'a> {
pub fn new(bytes: &'a [u8]) -> Self {
Self(Records(bytes))
}
}
impl<'a> Iterator for WatchEvents<'a> {
type Item = (WatchKind, &'a [u8]);
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|(tag, name)| {
(
if tag == WatchKind::Change as u8 {
WatchKind::Change
} else {
WatchKind::Rename
},
name,
)
})
}
}
#[cfg(not(target_arch = "wasm32"))]
mod service;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use service::{Reply, Service};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod watch;
#[cfg(windows)]
pub(crate) fn wtf8(units: &[u16], out: &mut [u8], used: &mut usize) -> bool {
let start = *used;
for c in char::decode_utf16(units.iter().copied()) {
let code = match c {
Ok(c) => c as u32,
Err(e) => u32::from(e.unpaired_surrogate()),
};
let mut bytes = [0u8; 4];
let len = if code < 0x80 {
bytes[0] = code as u8;
1
} else if code < 0x800 {
bytes[0] = 0xc0 | (code >> 6) as u8;
bytes[1] = 0x80 | (code & 0x3f) as u8;
2
} else if code < 0x10000 {
bytes[0] = 0xe0 | (code >> 12) as u8;
bytes[1] = 0x80 | ((code >> 6) & 0x3f) as u8;
bytes[2] = 0x80 | (code & 0x3f) as u8;
3
} else {
bytes[0] = 0xf0 | (code >> 18) as u8;
bytes[1] = 0x80 | ((code >> 12) & 0x3f) as u8;
bytes[2] = 0x80 | ((code >> 6) & 0x3f) as u8;
bytes[3] = 0x80 | (code & 0x3f) as u8;
4
};
if *used + len > out.len() {
*used = start;
return false;
}
out[*used..*used + len].copy_from_slice(&bytes[..len]);
*used += len;
}
true
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
#[test]
fn records_round_trip_and_truncated_input_stops() {
let mut page = [0u8; 32];
let mut used = 0;
assert!(put_record(&mut page, &mut used, 2, b"dir"));
assert!(put_record(&mut page, &mut used, 1, b""));
assert!(!put_record(&mut page, &mut used, 1, &[b'x'; 30]));
let entries: Vec<_> = DirEntries::new(&page[..used]).collect();
assert_eq!(
entries,
[
(FileType::Directory, &b"dir"[..]),
(FileType::File, &b""[..])
]
);
let events: Vec<_> = WatchEvents::new(&page[..used]).collect();
assert_eq!(events[0].0, WatchKind::Change);
assert_eq!(events[1].0, WatchKind::Rename);
assert_eq!(DirEntries::new(&page[..3]).count(), 0);
}
#[cfg(windows)]
#[test]
fn wtf8_preserves_unpaired_surrogates() {
let mut out = [0u8; 16];
let mut used = 0;
assert!(wtf8(&[0x61, 0xd800, 0x20ac], &mut out, &mut used));
assert_eq!(&out[..used], b"a\xed\xa0\x80\xe2\x82\xac");
}
}