diff --git a/Cargo.lock b/Cargo.lock index 711b59d3..cf136786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,9 +1023,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustix" -version = "0.36.5" +version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ "bitflags", "errno", @@ -1114,9 +1114,9 @@ dependencies = [ [[package]] name = "system-interface" -version = "0.25.0" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b5f685b54fe35201ca824534425d4af3562470fb67682cf20130c568b49042" +checksum = "c76db7161a415be430c1bd4d2d0c83aaeeded6f009f6d56da242a67747282f6c" dependencies = [ "bitflags", "cap-fs-ext", diff --git a/host/tests/runtime.rs b/host/tests/runtime.rs index cbe7badd..ce38ad66 100644 --- a/host/tests/runtime.rs +++ b/host/tests/runtime.rs @@ -236,6 +236,41 @@ async fn run_file_read(mut store: Store, wasi: Wasi) -> Result<()> { .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) } +async fn run_file_append(mut store: Store, wasi: Wasi) -> Result<()> { + let dir = tempfile::tempdir()?; + + std::fs::File::create(dir.path().join("bar.txt"))? + .write_all(b"'Twas brillig, and the slithy toves.\n")?; + + let descriptor = + store + .data_mut() + .push_dir(Box::new(wasi_cap_std_sync::dir::Dir::from_cap_std( + Dir::from_std_file(std::fs::File::open(dir.path())?), + )))?; + + wasi.command( + &mut store, + 0 as host::Descriptor, + 1 as host::Descriptor, + &[], + &[], + &[(descriptor, "/")], + ) + .await? + .map_err(|()| anyhow::anyhow!("command returned with failing exit status"))?; + + let contents = std::fs::read(dir.path().join("bar.txt"))?; + assert_eq!( + std::str::from_utf8(&contents).unwrap(), + "'Twas brillig, and the slithy toves.\n\ + Did gyre and gimble in the wabe;\n\ + All mimsy were the borogoves,\n\ + And the mome raths outgrabe.\n" + ); + Ok(()) +} + async fn run_exit_success(mut store: Store, wasi: Wasi) -> Result<()> { let r = wasi .command( diff --git a/src/lib.rs b/src/lib.rs index 9626de8f..9714469b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,7 @@ pub unsafe extern "C" fn command( type_: StreamType::File(File { fd: preopen.descriptor, position: Cell::new(0), + append: false, }), }))); } @@ -1091,8 +1092,13 @@ pub unsafe extern "C" fn fd_write( // If this is a file, keep the current-position pointer up to date. if let StreamType::File(file) = &streams.type_ { - file.position - .set(file.position.get() + wasi_filesystem::Filesize::from(bytes)); + // But don't update if we're in append mode. Strictly speaking, + // we should set the position to the new end of the file, but + // we don't have an API to do that atomically. + if !file.append { + file.position + .set(file.position.get() + wasi_filesystem::Filesize::from(bytes)); + } } *nwritten = bytes as usize; @@ -1245,6 +1251,7 @@ pub unsafe extern "C" fn path_open( let o_flags = o_flags_from_oflags(oflags); let flags = descriptor_flags_from_flags(fs_rights_base, fdflags); let mode = wasi_filesystem::Mode::READABLE | wasi_filesystem::Mode::WRITEABLE; + let append = fdflags & wasi::FDFLAGS_APPEND == wasi::FDFLAGS_APPEND; State::with_mut(|state| { let file = state.get_dir(fd)?; @@ -1255,6 +1262,7 @@ pub unsafe extern "C" fn path_open( type_: StreamType::File(File { fd: result, position: Cell::new(0), + append, }), }); @@ -1928,7 +1936,11 @@ impl Streams { // For files, we may have adjusted the position for seeking, so // create a new stream. StreamType::File(file) => { - let output = wasi_filesystem::write_via_stream(file.fd, file.position.get())?; + let output = if file.append { + wasi_filesystem::append_via_stream(file.fd)? + } else { + wasi_filesystem::write_via_stream(file.fd, file.position.get())? + }; self.output.set(Some(output)); Ok(output) } @@ -1982,6 +1994,9 @@ struct File { /// The current-position pointer. position: Cell, + + /// In append mode, all writes append to the file. + append: bool, } const PAGE_SIZE: usize = 65536; diff --git a/test-programs/src/bin/file_append.rs b/test-programs/src/bin/file_append.rs new file mode 100644 index 00000000..c5e3521b --- /dev/null +++ b/test-programs/src/bin/file_append.rs @@ -0,0 +1,17 @@ +use std::{ + error::Error, + fs::OpenOptions, + io::{Seek, SeekFrom, Write}, +}; + +fn main() -> Result<(), Box> { + let mut file = OpenOptions::new().append(true).open("bar.txt")?; + + file.write_all(b"Did gyre and gimble in the wabe;\n") + .unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + file.write_all(b"All mimsy were the borogoves,\n").unwrap(); + file.write_all(b"And the mome raths outgrabe.\n").unwrap(); + + Ok(()) +} diff --git a/wasi-common/cap-std-sync/src/file.rs b/wasi-common/cap-std-sync/src/file.rs index e34aabe4..42b27a90 100644 --- a/wasi-common/cap-std-sync/src/file.rs +++ b/wasi-common/cap-std-sync/src/file.rs @@ -140,6 +140,17 @@ impl WasiFile for File { fn is_write_vectored_at(&self) -> bool { self.0.is_write_vectored_at() } + async fn append<'a>(&mut self, buf: &[u8]) -> Result { + let n = self.0.append(buf)?; + Ok(n.try_into()?) + } + async fn append_vectored<'a>(&mut self, bufs: &[io::IoSlice<'a>]) -> Result { + let n = self.0.append_vectored(bufs)?; + Ok(n.try_into()?) + } + fn is_append_vectored(&self) -> bool { + self.0.is_append_vectored() + } fn isatty(&mut self) -> bool { self.0.is_terminal() } diff --git a/wasi-common/src/file.rs b/wasi-common/src/file.rs index 3bd11800..642c5f4e 100644 --- a/wasi-common/src/file.rs +++ b/wasi-common/src/file.rs @@ -107,6 +107,18 @@ pub trait WasiFile: Send + Sync { false } + async fn append<'a>(&mut self, _bufs: &[u8]) -> Result { + Err(Error::badf()) + } + + async fn append_vectored<'a>(&mut self, _bufs: &[std::io::IoSlice<'a>]) -> Result { + Err(Error::badf()) + } + + fn is_append_vectored(&self) -> bool { + false + } + async fn readable(&self) -> Result<(), Error>; async fn writable(&self) -> Result<(), Error>; @@ -185,54 +197,45 @@ pub enum Advice { NoReuse, } +enum FileStreamType { + /// Reading from a file, tracking our current position. + Read(u64), + + /// Writing to a file, tracking our current position. + Write(u64), + + /// Appending to a file. + Append, +} + pub struct FileStream { /// Which file are we streaming? file: Box, - /// Where in the file are we? - position: u64, - - /// Reading or writing? - reading: bool, + /// What type of streaming are we doing? + type_: FileStreamType, } impl FileStream { pub fn new_reader(file: Box, position: u64) -> Self { Self { file, - position, - reading: true, + type_: FileStreamType::Read(position), } } pub fn new_writer(file: Box, position: u64) -> Self { Self { file, - position, - reading: false, + type_: FileStreamType::Write(position), } } - pub fn new_appender(_file: Box) -> Self { - todo!() - } - - pub async fn seek(&mut self, pos: std::io::SeekFrom) -> Result { - match pos { - std::io::SeekFrom::Start(pos) => self.position = pos, - std::io::SeekFrom::Current(pos) => { - self.position = self.position.wrapping_add(pos as i64 as u64) - } - std::io::SeekFrom::End(pos) => { - self.position = self - .file - .get_filestat() - .await? - .size - .wrapping_add(pos as i64 as u64) - } + pub fn new_appender(file: Box) -> Self { + Self { + file, + type_: FileStreamType::Append, } - Ok(self.position) } } @@ -241,17 +244,19 @@ impl WasiStream for FileStream { fn as_any(&self) -> &dyn Any { self } + #[cfg(unix)] fn pollable_read(&self) -> Option { - if self.reading { + if let FileStreamType::Read(_) = self.type_ { self.file.pollable() } else { None } } + #[cfg(unix)] fn pollable_write(&self) -> Option { - if self.reading { + if let FileStreamType::Read(_) = self.type_ { None } else { self.file.pollable() @@ -260,15 +265,16 @@ impl WasiStream for FileStream { #[cfg(windows)] fn pollable_read(&self) -> Option { - if self.reading { + if let FileStreamType::Read(_) = self.type_ { self.file.pollable() } else { None } } + #[cfg(windows)] fn pollable_write(&self) -> Option { - if self.reading { + if let FileStreamType::Read(_) = self.type_ { None } else { self.file.pollable() @@ -276,53 +282,74 @@ impl WasiStream for FileStream { } async fn read(&mut self, buf: &mut [u8]) -> Result<(u64, bool), Error> { - if !self.reading { - return Err(Error::badf()); + if let FileStreamType::Read(position) = &mut self.type_ { + let (n, end) = self.file.read_at(buf, *position).await?; + *position = position.wrapping_add(n); + Ok((n, end)) + } else { + Err(Error::badf()) } - let (n, end) = self.file.read_at(buf, self.position).await?; - self.position = self.position.wrapping_add(n); - Ok((n, end)) } + async fn read_vectored<'a>( &mut self, bufs: &mut [io::IoSliceMut<'a>], ) -> Result<(u64, bool), Error> { - if !self.reading { - return Err(Error::badf()); + if let FileStreamType::Read(position) = &mut self.type_ { + let (n, end) = self.file.read_vectored_at(bufs, *position).await?; + *position = position.wrapping_add(n); + Ok((n, end)) + } else { + Err(Error::badf()) } - let (n, end) = self.file.read_vectored_at(bufs, self.position).await?; - self.position = self.position.wrapping_add(n); - Ok((n, end)) } + #[cfg(can_vector)] fn is_read_vectored_at(&self) -> bool { - if !self.reading { - return false; + if let FileStreamType::Read(_) = self.type_ { + self.file.is_read_vectored_at() + } else { + false } - self.file.is_read_vectored_at() } + async fn write(&mut self, buf: &[u8]) -> Result { - if self.reading { - return Err(Error::badf()); + match &mut self.type_ { + FileStreamType::Write(position) => { + let n = self.file.write_at(buf, *position).await? as i64 as u64; + *position = position.wrapping_add(n); + Ok(n) + } + FileStreamType::Append => { + let n = self.file.append(buf).await? as i64 as u64; + Ok(n) + } + FileStreamType::Read(_) => Err(Error::badf()), } - let n = self.file.write_at(buf, self.position).await? as i64 as u64; - self.position = self.position.wrapping_add(n); - Ok(n) } + async fn write_vectored<'a>(&mut self, bufs: &[io::IoSlice<'a>]) -> Result { - if self.reading { - return Err(Error::badf()); + match &mut self.type_ { + FileStreamType::Write(position) => { + let n = self.file.write_vectored_at(bufs, *position).await? as i64 as u64; + *position = position.wrapping_add(n); + Ok(n) + } + FileStreamType::Append => { + let n = self.file.append_vectored(bufs).await? as i64 as u64; + Ok(n) + } + FileStreamType::Read(_) => Err(Error::badf()), } - let n = self.file.write_vectored_at(bufs, self.position).await? as i64 as u64; - self.position = self.position.wrapping_add(n); - Ok(n) } + #[cfg(can_vector)] fn is_write_vectored_at(&self) -> bool { - if self.reading { - return false; + if let FileStreamType::Read(_) = self.type_ { + false + } else { + self.file.is_write_vectored_at() } - self.file.is_write_vectored_at() } // TODO: Optimize for file streams. @@ -342,21 +369,18 @@ impl WasiStream for FileStream { return self.file.read_at(&mut [], 0).await; } - if !self.reading { - return Err(Error::badf()); - } - - let new_position = self - .position - .checked_add(nelem) - .ok_or_else(Error::overflow)?; + if let FileStreamType::Read(position) = &mut self.type_ { + let new_position = position.checked_add(nelem).ok_or_else(Error::overflow)?; - let file_size = self.file.get_filestat().await?.size; + let file_size = self.file.get_filestat().await?.size; - let short_by = new_position.saturating_sub(file_size); + let short_by = new_position.saturating_sub(file_size); - self.position = new_position - short_by; - Ok((nelem - short_by, false)) + *position = new_position - short_by; + Ok((nelem - short_by, false)) + } else { + Err(Error::badf()) + } } // TODO: Optimize for file streams. @@ -371,23 +395,27 @@ impl WasiStream for FileStream { */ async fn num_ready_bytes(&self) -> Result { - if !self.reading { - return Err(Error::badf()); + if let FileStreamType::Read(_) = self.type_ { + // Default to saying that no data is ready. + Ok(0) + } else { + Err(Error::badf()) } - Ok(0) } async fn readable(&self) -> Result<(), Error> { - if !self.reading { - return Err(Error::badf()); + if let FileStreamType::Read(_) = self.type_ { + self.file.readable().await + } else { + Err(Error::badf()) } - self.file.readable().await } async fn writable(&self) -> Result<(), Error> { - if self.reading { - return Err(Error::badf()); + if let FileStreamType::Read(_) = self.type_ { + Err(Error::badf()) + } else { + self.file.writable().await } - self.file.writable().await } }