diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ce44fbd..d1e9910 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,6 +48,8 @@ jobs: wasm-tools component wit "${component}" echo "::endgroup::" done + - name: Build test components + run: make tests publish: if: github.event_name == 'push' && ( startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' ) diff --git a/Cargo.lock b/Cargo.lock index 0842d54..6eeaba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -68,6 +118,52 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -80,6 +176,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "filesystem-cli" +version = "0.1.0" +dependencies = [ + "clap", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -92,12 +195,48 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -111,11 +250,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-sink", "futures-task", "pin-project-lite", "slab", ] +[[package]] +name = "gate" +version = "0.1.0" +dependencies = [ + "chrono", + "heck", + "wit-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.17.0" @@ -173,6 +328,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.14" @@ -190,6 +351,70 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "latch-deny-all" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-glob" +version = "0.1.0" +dependencies = [ + "path-matchers", + "wit-bindgen", +] + +[[package]] +name = "latch-grant-all" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-n" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-n2" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n3" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n4" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n5" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-readonly" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -231,6 +456,21 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "path-matchers" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36cd9b72a47679ec193a5f0229d9ab686b7bd45e1fbc59ccf953c9f3d83f7b2b" +dependencies = [ + "glob", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -249,18 +489,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -343,11 +583,17 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" -version = "2.0.98" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -375,6 +621,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasm-bindgen" version = "0.2.123" @@ -513,6 +765,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -583,6 +844,7 @@ version = "0.58.0" source = "git+https://github.com/bytecodealliance/wit-bindgen.git?branch=main#646d3b4be2b0b02cfdace6fa0614c504ed53c801" dependencies = [ "bitflags", + "futures", "wit-bindgen-rust-macro", ] diff --git a/Cargo.toml b/Cargo.toml index 8f15ac0..510a676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,12 @@ resolver = "2" members = [ "components/*", + "crates/*", + "tests/*", ] [workspace.dependencies] chrono = { git = "https://github.com/chronotope/chrono.git", branch = "0.5.x" } heck = "0.5" -wit-bindgen = { version = "0.58.0", git = "https://github.com/bytecodealliance/wit-bindgen.git", branch = "main" } +latch-n = { path = "./crates/latch-n" } +wit-bindgen = { version = "0.58.0", features = ["async-spawn"], git = "https://github.com/bytecodealliance/wit-bindgen.git", branch = "main" } diff --git a/Makefile b/Makefile index 3ac7c7c..8fb14c7 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 COMPONENTS = $(shell ls -1 components) +TEST_COMPONENTS = $(shell ls -1 tests) .PHONY: all all: components @@ -13,9 +14,11 @@ clean: cargo clean rm -rf lib/*.wasm rm -rf lib/*.wasm.md + rm -rf lib/tests/*.wasm + rm -rf lib/tests/*.wasm.md .PHONY: components -components: $(foreach component,$(COMPONENTS),lib/$(component).wasm $(foreach component,$(COMPONENTS),lib/$(component).debug.wasm)) +components: $(foreach component,$(COMPONENTS),lib/$(component).wasm) $(foreach component,$(COMPONENTS),lib/$(component).debug.wasm) define BUILD_COMPONENT @@ -36,6 +39,64 @@ endef $(foreach component,$(COMPONENTS),$(eval $(call BUILD_COMPONENT,$(component)))) +define TEST_COMPONENT + +lib/tests/$1.wasm: Cargo.toml Cargo.lock wit/deps $(shell find tests/$1 -type f) + cargo component build -p $1 --target wasm32-wasip2 --release + cp target/wasm32-wasip2/release/$1.wasm lib/tests/$1.wasm + +lib/tests/$1.debug.wasm: Cargo.toml Cargo.lock wit/deps $(shell find tests/$1 -type f) + cargo component build -p $1 --target wasm32-wasip2 + cp target/wasm32-wasip2/debug/$1.wasm lib/tests/$1.debug.wasm + +endef + +$(foreach component,$(TEST_COMPONENTS),$(eval $(call TEST_COMPONENT,$(component)))) + +lib/tests/logging-to-stdout.wasm: + wkg oci pull ghcr.io/componentized/logging/to-stdout:v0.2.1 -o "lib/tests/logging-to-stdout.wasm" + +lib/tests/grant.wasm: lib/gate.wasm lib/latch-grant-all.wasm + wac plug lib/gate.wasm \ + --plug lib/latch-grant-all.wasm \ + -o lib/tests/grant.wasm + +lib/tests/filesystem-cli-grant.wasm: lib/tests/filesystem-cli.wasm lib/tests/grant.wasm lib/tests/logging-to-stdout.wasm + wac plug lib/tests/filesystem-cli.wasm \ + --plug <( \ + wac plug lib/tests/grant.wasm \ + --plug lib/tests/logging-to-stdout.wasm \ + ) \ + -o lib/tests/filesystem-cli-grant.wasm + +lib/tests/deny.wasm: lib/gate.wasm lib/latch-deny-all.wasm + wac plug lib/gate.wasm \ + --plug lib/latch-deny-all.wasm \ + -o lib/tests/deny.wasm + +lib/tests/filesystem-cli-deny.wasm: lib/tests/filesystem-cli.wasm lib/tests/deny.wasm lib/tests/logging-to-stdout.wasm + wac plug lib/tests/filesystem-cli.wasm \ + --plug <( \ + wac plug lib/tests/deny.wasm \ + --plug lib/tests/logging-to-stdout.wasm \ + ) \ + -o lib/tests/filesystem-cli-deny.wasm + +lib/tests/readonly.wasm: lib/gate.wasm lib/latch-n2.wasm lib/latch-readonly.wasm lib/latch-grant-all.wasm + wac plug lib/gate.wasm \ + --plug lib/latch-readonly.wasm \ + -o lib/tests/readonly.wasm + +lib/tests/filesystem-cli-readonly.wasm: lib/tests/filesystem-cli.wasm lib/tests/readonly.wasm lib/tests/logging-to-stdout.wasm + wac plug lib/tests/filesystem-cli.wasm \ + --plug <( \ + wac plug lib/tests/readonly.wasm \ + --plug lib/tests/logging-to-stdout.wasm \ + ) \ + -o lib/tests/filesystem-cli-readonly.wasm + +.PHONY: tests +tests: $(foreach component,$(TEST_COMPONENTS),lib/tests/$(component).wasm) lib/tests/filesystem-cli-grant.wasm lib/tests/filesystem-cli-deny.wasm lib/tests/filesystem-cli-readonly.wasm .PHONY: wit wit: wit/deps diff --git a/README.md b/README.md index 13c337c..ce797f7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,15 @@ A collection of utility components that remix wasi:filesystem types and interfac ## Components - [`chroot`](./components/chroot/) -- [`readonly`](./components/readonly/) +- [`gate`](./components/gate/) +- [`latch-n2`](./components/latch-n2/) +- [`latch-n3`](./components/latch-n3/) +- [`latch-n4`](./components/latch-n4/) +- [`latch-deny-all`](./components/latch-deny-all/) +- [`latch-grant-all`](./components/latch-grant-all/) +- [`latch-glob`](./components/latch-glob/) +- [`latch-readonly`](./components/latch-readonly/) +- ~~[`readonly`](./components/readonly/)~~ (deprecated, favor gate with readonly latch) - [`tracing`](./components/tracing/) ## Build @@ -23,6 +31,7 @@ A collection of utility components that remix wasi:filesystem types and interfac Prereqs: - a rust toolchain - [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) +- [`wac`](https://github.com/bytecodealliance/wac) - [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) ```sh diff --git a/components/gate/Cargo.toml b/components/gate/Cargo.toml new file mode 100644 index 0000000..fe14ecf --- /dev/null +++ b/components/gate/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gate" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +chrono = { workspace = true } +heck = { workspace = true } +wit-bindgen = { workspace = true } diff --git a/components/gate/README.md b/components/gate/README.md new file mode 100644 index 0000000..2a982e1 --- /dev/null +++ b/components/gate/README.md @@ -0,0 +1,3 @@ +# `gate` + +Filesystem gate access control. diff --git a/components/gate/src/lib.rs b/components/gate/src/lib.rs new file mode 100644 index 0000000..a5f9e1a --- /dev/null +++ b/components/gate/src/lib.rs @@ -0,0 +1,874 @@ +#![no_main] + +use std::fmt::Display; +use std::path::PathBuf; + +use chrono::DateTime; +use heck::ToKebabCase; + +use crate::componentized::filesystem::latch::Decision::Denied; +use crate::componentized::filesystem::latch::{ + self, authorize, DescriptorOperation, Operation, PreopensOperation, +}; +use crate::exports::wasi::filesystem::preopens::Guest as Preopens; +use crate::exports::wasi::filesystem::types::{ + Advice, Descriptor, DescriptorBorrow, DescriptorFlags, DescriptorStat, DescriptorType, + DirectoryEntry, ErrorCode, Filesize, Guest as Types, MetadataHashValue, NewTimestamp, + OpenFlags, PathFlags, +}; +use crate::wasi::filesystem::preopens; +use crate::wasi::filesystem::types; +use crate::wasi::logging::logging::{log, Level}; + +macro_rules! warn { + ($dst:expr, $($arg:tt)*) => { + log(Level::Warn, "componentized-gate", &format!($dst, $($arg)*)); + }; + ($dst:expr) => { + log(Level::Warn, "componentized-gate", &format!($dst)); + }; +} + +macro_rules! trace { + ($dst:expr, $($arg:tt)*) => { + log(Level::Trace, "componentized-gate", &format!($dst, $($arg)*)); + }; + ($dst:expr) => { + log(Level::Trace, "componentized-gate", &format!($dst)); + }; +} + +struct GatedFilesystem {} + +impl Preopens for GatedFilesystem { + #[doc = "/ Return the set of preopened directories, and their paths."] + #[allow(async_fn_in_trait)] + fn get_directories() -> Vec<(Descriptor, String)> { + preopens::get_directories() + .into_iter() + .filter(|(fs, path)| { + match authorize(&Operation::Preopens(PreopensOperation::GetDirectoriesItem((fs, path.clone())))) { + Some(Denied(reason)) => { + trace!("Denied REASON={reason} OPERATION=wasi:filesystem/preopens#get-directories PATH={path}"); + false + } + _ => true, + } + }) + .map(|(fd, path)| { + let fd = Descriptor::new(GatedFileDescriptor::new(fd, PathBuf::from(path.clone()))); + (fd, path) + }) + .collect() + } +} + +impl Types for GatedFilesystem { + type Descriptor = GatedFileDescriptor; +} + +struct GatedFileDescriptor { + fd: types::Descriptor, + path: PathBuf, +} + +impl GatedFileDescriptor { + fn new(fd: types::Descriptor, path: PathBuf) -> Self { + Self { fd, path } + } +} + +impl exports::wasi::filesystem::types::GuestDescriptor for GatedFileDescriptor { + #[doc = "/ Return a stream for reading from a file."] + #[doc = "/"] + #[doc = "/ Multiple read, write, and append streams may be active on the same open"] + #[doc = "/ file and they do not interfere with each other."] + #[doc = "/"] + #[doc = "/ This function returns a `stream` which provides the data received from the"] + #[doc = "/ file, and a `future` providing additional error information in case an"] + #[doc = "/ error is encountered."] + #[doc = "/"] + #[doc = "/ If no error is encountered, `stream.read` on the `stream` will return"] + #[doc = "/ `read-status::closed` with no `error-context` and the future resolves to"] + #[doc = "/ the value `ok`. If an error is encountered, `stream.read` on the"] + #[doc = "/ `stream` returns `read-status::closed` with an `error-context` and the future"] + #[doc = "/ resolves to `err` with an `error-code`."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `pread` in POSIX."] + #[allow(async_fn_in_trait)] + fn read_via_stream( + &self, + offset: Filesize, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader>, + ) { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::ReadViaStream(latch::DescriptorReadViaStreamArgs { offset }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.read-via-stream FD={self} OFFSET={offset}"); + let (_, data_reader) = wit_stream::new(); + let (result_writer, result_reader) = + wit_future::new(|| Err(ErrorCode::Other(None))); + result_writer.write(Err(reason)); + (data_reader, result_reader) + } + _ => self.fd.read_via_stream(offset), + } + } + + #[doc = "/ Return a stream for writing to a file, if available."] + #[doc = "/"] + #[doc = "/ May fail with an error-code describing why the file cannot be written."] + #[doc = "/"] + #[doc = "/ It is valid to write past the end of a file; the file is extended to the"] + #[doc = "/ extent of the write, with bytes between the previous end and the start of"] + #[doc = "/ the write set to zero."] + #[doc = "/"] + #[doc = "/ This function returns once either full contents of the stream are"] + #[doc = "/ written or an error is encountered."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `pwrite` in POSIX."] + #[allow(async_fn_in_trait)] + fn write_via_stream( + &self, + data: wit_bindgen::StreamReader, + offset: Filesize, + ) -> wit_bindgen::FutureReader> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::WriteViaStream(latch::DescriptorWriteViaStreamArgs { offset }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.write-via-stream FD={self} OFFSET={offset}"); + let (result_writer, result_reader) = + wit_future::new(|| Err(ErrorCode::Other(None))); + result_writer.write(Err(reason)); + result_reader + } + _ => self.fd.write_via_stream(data, offset), + } + } + + #[doc = "/ Return a stream for appending to a file, if available."] + #[doc = "/"] + #[doc = "/ May fail with an error-code describing why the file cannot be appended."] + #[doc = "/"] + #[doc = "/ This function returns once either full contents of the stream are"] + #[doc = "/ written or an error is encountered."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `write` with `O_APPEND` in POSIX."] + #[allow(async_fn_in_trait)] + fn append_via_stream( + &self, + data: wit_bindgen::StreamReader, + ) -> wit_bindgen::FutureReader> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::AppendViaStream, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.append-via-stream FD={self}"); + let (result_writer, result_reader) = + wit_future::new(|| Err(ErrorCode::Other(None))); + result_writer.write(Err(reason)); + result_reader + } + _ => self.fd.append_via_stream(data), + } + } + + #[doc = "/ Provide file advisory information on a descriptor."] + #[doc = "/"] + #[doc = "/ This is similar to `posix_fadvise` in POSIX."] + #[allow(async_fn_in_trait)] + async fn advise( + &self, + offset: Filesize, + length: Filesize, + advice: Advice, + ) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::Advise(latch::DescriptorAdviseArgs { + offset, + length, + advice, + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.advise FD={self} OFFSET={offset} LENGTH={length} ADVICE={advice}"); + Err(reason) + } + _ => self.fd.advise(offset, length, advice).await, + } + } + + #[doc = "/ Synchronize the data of a file to disk."] + #[doc = "/"] + #[doc = "/ This function succeeds with no effect if the file descriptor is not"] + #[doc = "/ opened for writing."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `fdatasync` in POSIX."] + #[allow(async_fn_in_trait)] + async fn sync_data(&self) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::SyncData, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.sync-data FD={self}"); + Err(reason) + } + _ => self.fd.sync_data().await, + } + } + + #[doc = "/ Get flags associated with a descriptor."] + #[doc = "/"] + #[doc = "/ Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX."] + #[doc = "/"] + #[doc = "/ Note: This returns the value that was the `fs_flags` value returned"] + #[doc = "/ from `fdstat_get` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn get_flags(&self) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::GetFlags, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.get-flags FD={self}"); + Err(reason) + } + _ => self.fd.get_flags().await, + } + } + + #[doc = "/ Get the dynamic type of a descriptor."] + #[doc = "/"] + #[doc = "/ Note: This returns the same value as the `type` field of the `fd-stat`"] + #[doc = "/ returned by `stat`, `stat-at` and similar."] + #[doc = "/"] + #[doc = "/ Note: This returns similar flags to the `st_mode & S_IFMT` value provided"] + #[doc = "/ by `fstat` in POSIX."] + #[doc = "/"] + #[doc = "/ Note: This returns the value that was the `fs_filetype` value returned"] + #[doc = "/ from `fdstat_get` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn get_type(&self) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::GetType, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.get-type FD={self}"); + Err(reason) + } + _ => self.fd.get_type().await, + } + } + + #[doc = "/ Adjust the size of an open file. If this increases the file\'s size, the"] + #[doc = "/ extra bytes are filled with zeros."] + #[doc = "/"] + #[doc = "/ Note: This was called `fd_filestat_set_size` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn set_size(&self, size: Filesize) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::SetSize(latch::DescriptorSetSizeArgs { size }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.set-size FD={self} SIZE={size}"); + Err(reason) + } + _ => self.fd.set_size(size).await, + } + } + + #[doc = "/ Adjust the timestamps of an open file or directory."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `futimens` in POSIX."] + #[doc = "/"] + #[doc = "/ Note: This was called `fd_filestat_set_times` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn set_times( + &self, + data_access_timestamp: NewTimestamp, + data_modification_timestamp: NewTimestamp, + ) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::SetTimes(latch::DescriptorSetTimesArgs { + data_access_timestamp, + data_modification_timestamp, + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.set-times FD={self} ACCESS-TIME={data_access_timestamp:?} MODIFIED-TIME={data_modification_timestamp:?}"); + Err(reason) + } + _ => { + self.fd + .set_times(data_access_timestamp, data_modification_timestamp) + .await + } + } + } + + #[doc = "/ Read directory entries from a directory."] + #[doc = "/"] + #[doc = "/ On filesystems where directories contain entries referring to themselves"] + #[doc = "/ and their parents, often named `.` and `..` respectively, these entries"] + #[doc = "/ are omitted."] + #[doc = "/"] + #[doc = "/ This always returns a new stream which starts at the beginning of the"] + #[doc = "/ directory. Multiple streams may be active on the same directory, and they"] + #[doc = "/ do not interfere with each other."] + #[doc = "/"] + #[doc = "/ This function returns a future, which will resolve to an error code if"] + #[doc = "/ reading full contents of the directory fails."] + #[allow(async_fn_in_trait)] + fn read_directory( + &self, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader>, + ) { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::ReadDirectory, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.read-directory FD={self}"); + let (_, dir_reader) = wit_stream::new(); + let (result_writer, result_reader) = + wit_future::new(|| Err(ErrorCode::Other(None))); + result_writer.write(Err(reason)); + (dir_reader, result_reader) + } + _ => { + // TODO authorize individual directory entries + self.fd.read_directory() + } + } + } + + #[doc = "/ Synchronize the data and metadata of a file to disk."] + #[doc = "/"] + #[doc = "/ This function succeeds with no effect if the file descriptor is not"] + #[doc = "/ opened for writing."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `fsync` in POSIX."] + #[allow(async_fn_in_trait)] + async fn sync(&self) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::Sync, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.sync FD={self}"); + Err(reason) + } + _ => self.fd.sync().await, + } + } + + #[doc = "/ Create a directory."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `mkdirat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::CreateDirectoryAt(latch::DescriptorCreateDirectoryAtArgs { + path: path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.create-directory-at FD={self} PATH={path}"); + Err(reason) + } + _ => self.fd.create_directory_at(path).await, + } + } + + #[doc = "/ Return the attributes of an open file or directory."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `fstat` in POSIX, except that it does not return"] + #[doc = "/ device and inode information. For testing whether two descriptors refer to"] + #[doc = "/ the same underlying filesystem object, use `is-same-object`. To obtain"] + #[doc = "/ additional data that can be used do determine whether a file has been"] + #[doc = "/ modified, use `metadata-hash`."] + #[doc = "/"] + #[doc = "/ Note: This was called `fd_filestat_get` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn stat(&self) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::Stat, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.stat FD={self}"); + Err(reason) + } + _ => self.fd.stat().await, + } + } + + #[doc = "/ Return the attributes of a file or directory."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `fstatat` in POSIX, except that it does not"] + #[doc = "/ return device and inode information. See the `stat` description for a"] + #[doc = "/ discussion of alternatives."] + #[doc = "/"] + #[doc = "/ Note: This was called `path_filestat_get` in earlier versions of WASI."] + #[allow(async_fn_in_trait)] + async fn stat_at( + &self, + path_flags: PathFlags, + path: String, + ) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::StatAt(latch::DescriptorStatAtArgs { + path_flags, + path: path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.stat-at FD={self} PATH-FLAGS={path_flags} PATH={path}"); + Err(reason) + } + _ => self.fd.stat_at(path_flags, path).await, + } + } + + #[doc = "/ Adjust the timestamps of a file or directory."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `utimensat` in POSIX."] + #[doc = "/"] + #[doc = "/ Note: This was called `path_filestat_set_times` in earlier versions of"] + #[doc = "/ WASI."] + #[allow(async_fn_in_trait)] + async fn set_times_at( + &self, + path_flags: PathFlags, + path: String, + data_access_timestamp: NewTimestamp, + data_modification_timestamp: NewTimestamp, + ) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::SetTimesAt(latch::DescriptorSetTimesAtArgs { + path_flags, + path: path.clone(), + data_access_timestamp, + data_modification_timestamp, + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.set-times-at FD={self} PATH-FLAGS={path_flags} PATH={path} ACCESS-TIME={data_access_timestamp:?} MODIFIED-TIME={data_modification_timestamp:?}"); + Err(reason) + } + _ => { + self.fd + .set_times_at( + path_flags, + path, + data_access_timestamp, + data_modification_timestamp, + ) + .await + } + } + } + + #[doc = "/ Create a hard link."] + #[doc = "/"] + #[doc = "/ Fails with `error-code::no-entry` if the old path does not exist,"] + #[doc = "/ with `error-code::exist` if the new path already exists, and"] + #[doc = "/ `error-code::not-permitted` if the old path is not a file."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `linkat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn link_at( + &self, + old_path_flags: PathFlags, + old_path: String, + new_descriptor: DescriptorBorrow<'_>, + new_path: String, + ) -> Result<(), ErrorCode> { + let new_descriptor: &Self = new_descriptor.get(); + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::LinkAt(latch::DescriptorLinkAtArgs { + old_path_flags, + old_path: old_path.clone(), + new_descriptor: &new_descriptor.fd, + new_path: new_path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.link-at FD={self} OLD-PATH={old_path} OLD-PATH-FLAGS={old_path_flags} NEW-PATH={new_path}", + ); + Err(reason) + } + _ => { + self.fd + .link_at(old_path_flags, old_path, &new_descriptor.fd, new_path) + .await + } + } + } + + #[doc = "/ Open a file or directory."] + #[doc = "/"] + #[doc = "/ If `flags` contains `descriptor-flags::mutate-directory`, and the base"] + #[doc = "/ descriptor doesn\'t have `descriptor-flags::mutate-directory` set,"] + #[doc = "/ `open-at` fails with `error-code::read-only`."] + #[doc = "/"] + #[doc = "/ If `flags` contains `write` or `mutate-directory`, or `open-flags`"] + #[doc = "/ contains `truncate` or `create`, and the base descriptor doesn\'t have"] + #[doc = "/ `descriptor-flags::mutate-directory` set, `open-at` fails with"] + #[doc = "/ `error-code::read-only`."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `openat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn open_at( + &self, + path_flags: PathFlags, + path: String, + open_flags: OpenFlags, + flags: DescriptorFlags, + ) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::OpenAt(latch::DescriptorOpenAtArgs { + path_flags, + path: path.clone(), + open_flags, + flags, + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.open-at FD={self} PATH-FLAGS={path_flags} PATH={path} OPEN-FLAGS={open_flags} FLAGS={flags}"); + Err(reason) + } + _ => self + .fd + .open_at(path_flags, path.clone(), open_flags, flags) + .await + .map(|fd| Descriptor::new(GatedFileDescriptor::new(fd, self.path.join(path)))), + } + } + + #[doc = "/ Read the contents of a symbolic link."] + #[doc = "/"] + #[doc = "/ If the contents contain an absolute or rooted path in the underlying"] + #[doc = "/ filesystem, this function fails with `error-code::not-permitted`."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `readlinkat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn readlink_at(&self, path: String) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::ReadlinkAt(latch::DescriptorReadlinkAtArgs { path: path.clone() }), + ))) { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.readlink-at FD={self} PATH={path}", + ); + Err(reason) + } + _ => self.fd.readlink_at(path).await, + } + } + + #[doc = "/ Remove a directory."] + #[doc = "/"] + #[doc = "/ Return `error-code::not-empty` if the directory is not empty."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX."] + #[allow(async_fn_in_trait)] + async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::RemoveDirectoryAt(latch::DescriptorRemoveDirectoryAtArgs { + path: path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.remove-directory-at FD={self} PATH={path}"); + Err(reason) + } + _ => self.fd.remove_directory_at(path).await, + } + } + + #[doc = "/ Rename a filesystem object."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `renameat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn rename_at( + &self, + old_path: String, + new_descriptor: DescriptorBorrow<'_>, + new_path: String, + ) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::RenameAt(latch::DescriptorRenameAtArgs { + old_path: old_path.clone(), + new_descriptor: &new_descriptor.get::().fd, + new_path: new_path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.rename-at FD={self} OLD-PATH={old_path} NEW-PATH={new_path}", + ); + Err(reason) + } + _ => { + let new_descriptor: &Self = new_descriptor.get(); + self.fd + .rename_at(old_path, &new_descriptor.fd, new_path) + .await + } + } + } + + #[doc = "/ Create a symbolic link (also known as a \"symlink\")."] + #[doc = "/"] + #[doc = "/ If `old-path` starts with `/`, the function fails with"] + #[doc = "/ `error-code::not-permitted`."] + #[doc = "/"] + #[doc = "/ Note: This is similar to `symlinkat` in POSIX."] + #[allow(async_fn_in_trait)] + async fn symlink_at(&self, old_path: String, new_path: String) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::SymlinkAt(latch::DescriptorSymlinkAtArgs { + old_path: old_path.clone(), + new_path: new_path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.symlink-at FD={self} OLD-PATH={old_path} NEW-PATH={new_path}", + ); + Err(reason) + } + _ => self.fd.symlink_at(old_path, new_path).await, + } + } + + #[doc = "/ Unlink a filesystem object that is not a directory."] + #[doc = "/"] + #[doc = "/ This is similar to `unlinkat(fd, path, 0)` in POSIX."] + #[doc = "/"] + #[doc = "/ Error returns are as specified by POSIX."] + #[doc = "/"] + #[doc = "/ If the filesystem object is a directory, `error-code::access` or"] + #[doc = "/ `error-code::is-directory` may be returned instead of the"] + #[doc = "/ POSIX-specified `error-code::not-permitted`."] + #[allow(async_fn_in_trait)] + async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::UnlinkFileAt(latch::DescriptorUnlinkFileAtArgs { + path: path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.unlink-file-at FD={self} PATH={path}", + ); + Err(reason) + } + _ => self.fd.unlink_file_at(path).await, + } + } + + #[doc = "/ Test whether two descriptors refer to the same filesystem object."] + #[doc = "/"] + #[doc = "/ In POSIX, this corresponds to testing whether the two descriptors have the"] + #[doc = "/ same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers."] + #[doc = "/ wasi-filesystem does not expose device and inode numbers, so this function"] + #[doc = "/ may be used instead."] + #[allow(async_fn_in_trait)] + async fn is_same_object(&self, other: DescriptorBorrow<'_>) -> bool { + let other: &Self = other.get(); + self.fd.is_same_object(&other.fd).await + } + + #[doc = "/ Return a hash of the metadata associated with a filesystem object referred"] + #[doc = "/ to by a descriptor."] + #[doc = "/"] + #[doc = "/ This returns a hash of the last-modification timestamp and file size, and"] + #[doc = "/ may also include the inode number, device number, birth timestamp, and"] + #[doc = "/ other metadata fields that may change when the file is modified or"] + #[doc = "/ replaced. It may also include a secret value chosen by the"] + #[doc = "/ implementation and not otherwise exposed."] + #[doc = "/"] + #[doc = "/ Implementations are encouraged to provide the following properties:"] + #[doc = "/"] + #[doc = "/ - If the file is not modified or replaced, the computed hash value should"] + #[doc = "/ usually not change."] + #[doc = "/ - If the object is modified or replaced, the computed hash value should"] + #[doc = "/ usually change."] + #[doc = "/ - The inputs to the hash should not be easily computable from the"] + #[doc = "/ computed hash."] + #[doc = "/"] + #[doc = "/ However, none of these is required."] + #[allow(async_fn_in_trait)] + async fn metadata_hash(&self) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::MetadataHash, + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.metadata-hash FD={self}"); + Err(reason) + } + _ => self.fd.metadata_hash().await, + } + } + + #[doc = "/ Return a hash of the metadata associated with a filesystem object referred"] + #[doc = "/ to by a directory descriptor and a relative path."] + #[doc = "/"] + #[doc = "/ This performs the same hash computation as `metadata-hash`."] + #[allow(async_fn_in_trait)] + async fn metadata_hash_at( + &self, + path_flags: PathFlags, + path: String, + ) -> Result { + match authorize(&Operation::Descriptor(( + &self.fd, + self.path.to_string_lossy().into_owned(), + DescriptorOperation::MetadataHashAt(latch::DescriptorMetadataHashAtArgs { + path_flags, + path: path.clone(), + }), + ))) { + Some(Denied(reason)) => { + warn!("Denied REASON={reason} OPERATION=wasi:filesystem/types#descriptor.metadata-hash-at FD={self} PATH={path} PATH-FLAGS={path_flags}"); + Err(reason) + } + _ => self.fd.metadata_hash_at(path_flags, path).await, + } + } +} + +impl Display for GatedFileDescriptor { + fn fmt(&self, d: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + d.write_fmt(format_args!("{}", self.path.display())) + } +} + +impl Display for types::Advice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_string().to_kebab_case()) + } +} + +impl Display for types::DescriptorFlags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let names: Vec = self + .iter_names() + .map(|(name, _flags)| name.to_kebab_case()) + .collect(); + f.write_fmt(format_args!("({})", &names.join("|"))) + } +} + +impl Display for types::OpenFlags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let names: Vec = self + .iter_names() + .map(|(name, _flags)| name.to_kebab_case()) + .collect(); + f.write_fmt(format_args!("({})", &names.join("|"))) + } +} + +impl Display for types::PathFlags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let names: Vec = self + .iter_names() + .map(|(name, _flags)| name.to_kebab_case()) + .collect(); + f.write_fmt(format_args!("({})", &names.join("|"))) + } +} + +impl Display for types::NewTimestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + types::NewTimestamp::NoChange => f.write_str("no-change"), + types::NewTimestamp::Now => f.write_str("now"), + types::NewTimestamp::Timestamp(instant) => { + f.write_fmt(format_args!("timestamp<{instant}>")) + } + } + } +} + +impl Display for types::Instant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let date = DateTime::from_timestamp(self.seconds as i64, self.nanoseconds) + .unwrap() + .format("%Y-%m-%d %H:%M:%S.%3fZ"); + + f.write_fmt(format_args!("{date}")) + } +} + +impl Display for types::DescriptorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + types::DescriptorType::Other(Some(type_)) => { + f.write_fmt(format_args!("other<{type_}>")) + } + _ => f.write_str(&self.to_string().to_kebab_case()), + } + } +} + +wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GatedFilesystem); diff --git a/components/latch-deny-all/Cargo.toml b/components/latch-deny-all/Cargo.toml new file mode 100644 index 0000000..741f659 --- /dev/null +++ b/components/latch-deny-all/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-deny-all" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-deny-all/README.md b/components/latch-deny-all/README.md new file mode 100644 index 0000000..c619098 --- /dev/null +++ b/components/latch-deny-all/README.md @@ -0,0 +1,3 @@ +# `latch-deny-all` + +Filesystem latch that implicitly denies all operations. diff --git a/components/latch-deny-all/src/lib.rs b/components/latch-deny-all/src/lib.rs new file mode 100644 index 0000000..3f2baba --- /dev/null +++ b/components/latch-deny-all/src/lib.rs @@ -0,0 +1,23 @@ +#![no_main] + +use crate::{ + exports::componentized::filesystem::latch::{Decision, Guest as Latch, Operation}, + wasi::filesystem::types::ErrorCode, +}; + +struct DenyAllLatch {} + +impl Latch for DenyAllLatch { + fn authorize(_: Operation) -> Option { + Some(Decision::Denied(ErrorCode::NotPermitted)) + } +} + +wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(DenyAllLatch); diff --git a/components/latch-glob/Cargo.toml b/components/latch-glob/Cargo.toml new file mode 100644 index 0000000..862df7b --- /dev/null +++ b/components/latch-glob/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "latch-glob" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } +path-matchers = "1.0" diff --git a/components/latch-glob/README.md b/components/latch-glob/README.md new file mode 100644 index 0000000..e8cc14b --- /dev/null +++ b/components/latch-glob/README.md @@ -0,0 +1,9 @@ +# `latch-glob` + +Filesystem latch that uses glob patterns to grant or deny operations on a path. + +The patterns are defined in a wasi:config/store. Keys starting with `deny` are parsed as globs with matching path operations being denied. Multiple patterns are allowed by defining unique config keys (e.g. `deny-1`, `deny-2`, etc). Keys starting with `grant` are parsed as globs with matching path operations being granted. + +Operations are evaluated against the pattern with the base path for the operation joined with a relative path, if any. For example, the file descriptor open-at operation will join the descriptor's path with the argument's path, the resulting path is matched to the patterns. + +For operations with multiple paths, each path is evaluated individually. Any path matching a deny pattern will result in a denial. diff --git a/components/latch-glob/src/lib.rs b/components/latch-glob/src/lib.rs new file mode 100644 index 0000000..6917d98 --- /dev/null +++ b/components/latch-glob/src/lib.rs @@ -0,0 +1,200 @@ +#![no_main] + +use std::path::Path; + +use path_matchers::{glob, PathMatcher}; + +use crate::{ + exports::componentized::filesystem::latch::{ + Decision, DescriptorOperation, Guest as Latch, Operation, PreopensOperation, + }, + wasi::filesystem::types::ErrorCode, +}; + +struct GlobLatch {} + +struct Patterns { + initialized: bool, + denies: Option>>, + grants: Option>>, + deny_reason: ErrorCode, +} + +impl Patterns { + fn initialize() { + if unsafe { STATE.initialized } { + return; + } + + let mut denies: Vec> = vec![]; + let mut grants: Vec> = vec![]; + let mut reason = ErrorCode::NotPermitted; + + for (key, value) in wasi::config::store::get_all().expect("config must be available") { + if key.starts_with("deny") { + denies.push(Box::new( + glob(&value).expect("config value must parse as a glob"), + )); + } else if key.starts_with("grant") { + grants.push(Box::new( + glob(&value).expect("config value must parse as a glob"), + )); + } else if key == "reason" { + reason = get_error_code(value); + } + } + + unsafe { + STATE.denies = Some(denies); + STATE.grants = Some(grants); + STATE.deny_reason = reason; + STATE.initialized = true; + }; + } + + #[allow(static_mut_refs)] + fn authorize(path: String, paths: Vec) -> Option { + if paths.len() == 0 { + let path = Path::new(&path); + return unsafe { STATE.authorize_path(path) }; + } + let mut decision = None; + for p in paths { + let path = Path::new(&path).join(p); + match unsafe { STATE.authorize_path(&path) } { + // return denies immediately, buffer grants + Some(Decision::Denied(reason)) => return Some(Decision::Denied(reason)), + Some(Decision::Granted) => decision = Some(Decision::Granted), + None => {} + } + } + decision + } + + fn authorize_path(&self, path: &Path) -> Option { + for deny in self.denies.as_ref().unwrap() { + if deny.matches(path) { + return Some(Decision::Denied(self.deny_reason.clone())); + } + } + for grant in self.grants.as_ref().unwrap() { + if grant.matches(path) { + return Some(Decision::Granted); + } + } + None + } +} + +static mut STATE: Patterns = Patterns { + initialized: false, + denies: None, + grants: None, + deny_reason: ErrorCode::NotPermitted, +}; + +impl Latch for GlobLatch { + fn authorize(operation: Operation) -> Option { + Patterns::initialize(); + + match operation { + Operation::Preopens(preopens_operation) => match preopens_operation { + PreopensOperation::GetDirectoriesItem((_, path)) => { + Patterns::authorize(path, vec![]) + } + }, + Operation::Descriptor((_, path, descriptor_operation)) => match descriptor_operation { + DescriptorOperation::ReadViaStream(_) => Patterns::authorize(path, vec![]), + DescriptorOperation::WriteViaStream(_) => Patterns::authorize(path, vec![]), + DescriptorOperation::AppendViaStream => Patterns::authorize(path, vec![]), + DescriptorOperation::Advise(_) => Patterns::authorize(path, vec![]), + DescriptorOperation::SyncData => Patterns::authorize(path, vec![]), + DescriptorOperation::GetFlags => Patterns::authorize(path, vec![]), + DescriptorOperation::GetType => Patterns::authorize(path, vec![]), + DescriptorOperation::SetSize(_) => Patterns::authorize(path, vec![]), + DescriptorOperation::SetTimes(_) => Patterns::authorize(path, vec![]), + DescriptorOperation::ReadDirectory => Patterns::authorize(path, vec![]), + DescriptorOperation::Sync => Patterns::authorize(path, vec![]), + DescriptorOperation::CreateDirectoryAt(args) => { + Patterns::authorize(path, vec![args.path]) + } + DescriptorOperation::Stat => Patterns::authorize(path, vec![]), + DescriptorOperation::StatAt(args) => Patterns::authorize(path, vec![args.path]), + DescriptorOperation::SetTimesAt(args) => Patterns::authorize(path, vec![args.path]), + DescriptorOperation::LinkAt(args) => { + Patterns::authorize(path, vec![args.old_path, args.new_path]) + } + DescriptorOperation::OpenAt(args) => Patterns::authorize(path, vec![args.path]), + DescriptorOperation::ReadlinkAt(args) => Patterns::authorize(path, vec![args.path]), + DescriptorOperation::RemoveDirectoryAt(args) => { + Patterns::authorize(path, vec![args.path]) + } + DescriptorOperation::RenameAt(args) => { + Patterns::authorize(path, vec![args.old_path, args.new_path]) + } + DescriptorOperation::SymlinkAt(args) => { + Patterns::authorize(path, vec![args.old_path, args.new_path]) + } + DescriptorOperation::UnlinkFileAt(args) => { + Patterns::authorize(path, vec![args.path]) + } + DescriptorOperation::MetadataHash => Patterns::authorize(path, vec![]), + DescriptorOperation::MetadataHashAt(args) => { + Patterns::authorize(path, vec![args.path]) + } + }, + } + } +} + +fn get_error_code(value: String) -> ErrorCode { + match value.as_str() { + "access" => ErrorCode::Access, + "already" => ErrorCode::Already, + "bad-descriptor" => ErrorCode::BadDescriptor, + "busy" => ErrorCode::Busy, + "deadlock" => ErrorCode::Deadlock, + "quota" => ErrorCode::Quota, + "exist" => ErrorCode::Exist, + "file-too-large" => ErrorCode::FileTooLarge, + "illegal-byte-sequence" => ErrorCode::IllegalByteSequence, + "in-progress" => ErrorCode::InProgress, + "interrupted" => ErrorCode::Interrupted, + "invalid" => ErrorCode::Invalid, + "io" => ErrorCode::Io, + "is-directory" => ErrorCode::IsDirectory, + "loop" => ErrorCode::Loop, + "too-many-links" => ErrorCode::TooManyLinks, + "message-size" => ErrorCode::MessageSize, + "name-too-long" => ErrorCode::NameTooLong, + "no-device" => ErrorCode::NoDevice, + "no-entry" => ErrorCode::NoEntry, + "no-lock" => ErrorCode::NoLock, + "insufficient-memory" => ErrorCode::InsufficientMemory, + "insufficient-space" => ErrorCode::InsufficientSpace, + "not-directory" => ErrorCode::NotDirectory, + "not-empty" => ErrorCode::NotEmpty, + "not-recoverable" => ErrorCode::NotRecoverable, + "unsupported" => ErrorCode::Unsupported, + "no-tty" => ErrorCode::NoTty, + "no-such-device" => ErrorCode::NoSuchDevice, + "overflow" => ErrorCode::Overflow, + "not-permitted" => ErrorCode::NotPermitted, + "pipe" => ErrorCode::Pipe, + "read-only" => ErrorCode::ReadOnly, + "invalid-seek" => ErrorCode::InvalidSeek, + "text-file-busy" => ErrorCode::TextFileBusy, + "cross-device" => ErrorCode::CrossDevice, + "other" => ErrorCode::Other(None), + _ => ErrorCode::Other(Some(value)), + } +} + +wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GlobLatch); diff --git a/components/latch-grant-all/Cargo.toml b/components/latch-grant-all/Cargo.toml new file mode 100644 index 0000000..e37c55f --- /dev/null +++ b/components/latch-grant-all/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-grant-all" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-grant-all/README.md b/components/latch-grant-all/README.md new file mode 100644 index 0000000..21165c4 --- /dev/null +++ b/components/latch-grant-all/README.md @@ -0,0 +1,3 @@ +# `latch-grant-all` + +Filesystem latch that implicitly grants all operations. diff --git a/components/latch-grant-all/src/lib.rs b/components/latch-grant-all/src/lib.rs new file mode 100644 index 0000000..318e16c --- /dev/null +++ b/components/latch-grant-all/src/lib.rs @@ -0,0 +1,20 @@ +#![no_main] + +use crate::exports::componentized::filesystem::latch::{Decision, Guest as Latch, Operation}; + +struct GrantAllLatch {} + +impl Latch for GrantAllLatch { + fn authorize(_: Operation) -> Option { + Some(Decision::Granted) + } +} + +wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GrantAllLatch); diff --git a/components/latch-n2/Cargo.toml b/components/latch-n2/Cargo.toml new file mode 100644 index 0000000..4fbab2a --- /dev/null +++ b/components/latch-n2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n2" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n2/README.md b/components/latch-n2/README.md new file mode 100644 index 0000000..d302838 --- /dev/null +++ b/components/latch-n2/README.md @@ -0,0 +1,3 @@ +# `latch-n2` + +Filesystem latch that aggregates two other filesystem latches. diff --git a/components/latch-n2/src/lib.rs b/components/latch-n2/src/lib.rs new file mode 100644 index 0000000..a1366d4 --- /dev/null +++ b/components/latch-n2/src/lib.rs @@ -0,0 +1,18 @@ +#![no_main] + +use latch_n::bindings::componentized::filesystem::{latch as latch0, latch1}; +use latch_n::bindings::exports::componentized::filesystem::latch::{ + Decision, Guest as Latch, Operation, +}; + +struct LatchN2 {} + +impl Latch for LatchN2 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Option { + let authorizers = vec![latch0::authorize, latch1::authorize]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN2 with_types_in latch_n::bindings); diff --git a/components/latch-n3/Cargo.toml b/components/latch-n3/Cargo.toml new file mode 100644 index 0000000..baf0a07 --- /dev/null +++ b/components/latch-n3/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n3" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n3/README.md b/components/latch-n3/README.md new file mode 100644 index 0000000..8ff8624 --- /dev/null +++ b/components/latch-n3/README.md @@ -0,0 +1,3 @@ +# `latch-n3` + +Filesystem latch that aggregates three other filesystem latches. diff --git a/components/latch-n3/src/lib.rs b/components/latch-n3/src/lib.rs new file mode 100644 index 0000000..ff3b838 --- /dev/null +++ b/components/latch-n3/src/lib.rs @@ -0,0 +1,18 @@ +#![no_main] + +use latch_n::bindings::componentized::filesystem::{latch as latch0, latch1, latch2}; +use latch_n::bindings::exports::componentized::filesystem::latch::{ + Decision, Guest as Latch, Operation, +}; + +struct LatchN3 {} + +impl Latch for LatchN3 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Option { + let authorizers = vec![latch0::authorize, latch1::authorize, latch2::authorize]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN3 with_types_in latch_n::bindings); diff --git a/components/latch-n4/Cargo.toml b/components/latch-n4/Cargo.toml new file mode 100644 index 0000000..d6bc670 --- /dev/null +++ b/components/latch-n4/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n4" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n4/README.md b/components/latch-n4/README.md new file mode 100644 index 0000000..f9586c5 --- /dev/null +++ b/components/latch-n4/README.md @@ -0,0 +1,3 @@ +# `latch-n4` + +Filesystem latch that aggregates four other filesystem latches. diff --git a/components/latch-n4/src/lib.rs b/components/latch-n4/src/lib.rs new file mode 100644 index 0000000..5418cc7 --- /dev/null +++ b/components/latch-n4/src/lib.rs @@ -0,0 +1,23 @@ +#![no_main] + +use latch_n::bindings::componentized::filesystem::{latch as latch0, latch1, latch2, latch3}; +use latch_n::bindings::exports::componentized::filesystem::latch::{ + Decision, Guest as Latch, Operation, +}; + +struct LatchN4 {} + +impl Latch for LatchN4 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Option { + let authorizers = vec![ + latch0::authorize, + latch1::authorize, + latch2::authorize, + latch3::authorize, + ]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN4 with_types_in latch_n::bindings); diff --git a/components/latch-n5/Cargo.toml b/components/latch-n5/Cargo.toml new file mode 100644 index 0000000..1140d89 --- /dev/null +++ b/components/latch-n5/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n5" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n5/README.md b/components/latch-n5/README.md new file mode 100644 index 0000000..407bcb2 --- /dev/null +++ b/components/latch-n5/README.md @@ -0,0 +1,3 @@ +# `latch-n5` + +Filesystem latch that aggregates five other filesystem latches. diff --git a/components/latch-n5/src/lib.rs b/components/latch-n5/src/lib.rs new file mode 100644 index 0000000..c257021 --- /dev/null +++ b/components/latch-n5/src/lib.rs @@ -0,0 +1,26 @@ +#![no_main] + +use latch_n::bindings::componentized::filesystem::{ + latch as latch0, latch1, latch2, latch3, latch4, +}; +use latch_n::bindings::exports::componentized::filesystem::latch::{ + Decision, Guest as Latch, Operation, +}; + +struct LatchN5 {} + +impl Latch for LatchN5 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Option { + let authorizers = vec![ + latch0::authorize, + latch1::authorize, + latch2::authorize, + latch3::authorize, + latch4::authorize, + ]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN5 with_types_in latch_n::bindings); diff --git a/components/latch-readonly/Cargo.toml b/components/latch-readonly/Cargo.toml new file mode 100644 index 0000000..8e3f98b --- /dev/null +++ b/components/latch-readonly/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-readonly" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-readonly/README.md b/components/latch-readonly/README.md new file mode 100644 index 0000000..79e5e96 --- /dev/null +++ b/components/latch-readonly/README.md @@ -0,0 +1,3 @@ +# `latch-readonly` + +Filesystem latch that denies operations which would mutate the filesystem. diff --git a/components/latch-readonly/src/lib.rs b/components/latch-readonly/src/lib.rs new file mode 100644 index 0000000..aef5a85 --- /dev/null +++ b/components/latch-readonly/src/lib.rs @@ -0,0 +1,71 @@ +#![no_main] + +use crate::{ + exports::componentized::filesystem::latch::{ + Decision::{self, Denied}, + DescriptorOpenAtArgs, DescriptorOperation, Guest as Latch, Operation, + }, + wasi::filesystem::types::{DescriptorFlags, ErrorCode::ReadOnly, OpenFlags}, +}; + +struct ReadOnlyLatch {} + +impl Latch for ReadOnlyLatch { + fn authorize(operation: Operation) -> Option { + match operation { + Operation::Preopens(_) => None, + Operation::Descriptor((_, _, descriptor_operation)) => match descriptor_operation { + DescriptorOperation::ReadViaStream(_) => None, + DescriptorOperation::WriteViaStream(_) => Some(Denied(ReadOnly)), + DescriptorOperation::AppendViaStream => Some(Denied(ReadOnly)), + DescriptorOperation::Advise(_) => None, + DescriptorOperation::SyncData => Some(Denied(ReadOnly)), + DescriptorOperation::GetFlags => None, + DescriptorOperation::GetType => None, + DescriptorOperation::SetSize(_) => Some(Denied(ReadOnly)), + DescriptorOperation::SetTimes(_) => Some(Denied(ReadOnly)), + DescriptorOperation::ReadDirectory => None, + DescriptorOperation::Sync => Some(Denied(ReadOnly)), + DescriptorOperation::CreateDirectoryAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::Stat => None, + DescriptorOperation::StatAt(_) => None, + DescriptorOperation::SetTimesAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::LinkAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::OpenAt(DescriptorOpenAtArgs { + open_flags, flags, .. + }) => { + if open_flags.intersects( + OpenFlags::CREATE + .union(OpenFlags::EXCLUSIVE) + .union(OpenFlags::TRUNCATE), + ) || flags.intersects( + DescriptorFlags::WRITE + .union(DescriptorFlags::FILE_INTEGRITY_SYNC) + .union(DescriptorFlags::DATA_INTEGRITY_SYNC) + .union(DescriptorFlags::REQUESTED_WRITE_SYNC), + ) { + Some(Denied(ReadOnly)) + } else { + None + } + } + DescriptorOperation::ReadlinkAt(_) => None, + DescriptorOperation::RemoveDirectoryAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::RenameAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::SymlinkAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::UnlinkFileAt(_) => Some(Denied(ReadOnly)), + DescriptorOperation::MetadataHash => None, + DescriptorOperation::MetadataHashAt(_) => None, + }, + } + } +} + +wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(ReadOnlyLatch); diff --git a/components/tracing/src/lib.rs b/components/tracing/src/lib.rs index 56d4aef..dd9b714 100644 --- a/components/tracing/src/lib.rs +++ b/components/tracing/src/lib.rs @@ -507,13 +507,8 @@ impl exports::wasi::filesystem::types::GuestDescriptor for TracingDescriptor { } impl Display for TracingDescriptor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str( - &self - .path - .to_str() - .expect("path contains invalid unicode characters"), - ) + fn fmt(&self, d: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + d.write_fmt(format_args!("{}", self.path.display())) } } diff --git a/crates/latch-n/Cargo.toml b/crates/latch-n/Cargo.toml new file mode 100644 index 0000000..89c2b9e --- /dev/null +++ b/crates/latch-n/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "latch-n" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/crates/latch-n/src/lib.rs b/crates/latch-n/src/lib.rs new file mode 100644 index 0000000..547abcd --- /dev/null +++ b/crates/latch-n/src/lib.rs @@ -0,0 +1,34 @@ +#![no_main] + +use crate::bindings::exports::componentized::filesystem::latch::{Decision, Operation}; + +pub fn authorize( + operation: Operation, + authorizers: Vec) -> Option>, +) -> Option { + for authorize in authorizers { + match authorize(&operation) { + None => {} + Some(Decision::Granted) => return Some(Decision::Granted), + Some(Decision::Denied(error_code)) => return Some(Decision::Denied(error_code)), + } + } + None +} + +pub mod bindings { + wit_bindgen::generate!({ + path: "../../wit", + world: "filesystem-latch-n", + pub_export_macro: true, + merge_structurally_equal_types: true, + generate_all + }); +} + +#[macro_export] +macro_rules! export { + ($($t:tt)*) => { + $crate::bindings::export!($($t)*); + }; +} diff --git a/lib/tests/.gitignore b/lib/tests/.gitignore new file mode 100644 index 0000000..902f891 --- /dev/null +++ b/lib/tests/.gitignore @@ -0,0 +1,2 @@ +*.md +*.wasm diff --git a/tests/filesystem-cli/Cargo.toml b/tests/filesystem-cli/Cargo.toml new file mode 100644 index 0000000..6b2ecad --- /dev/null +++ b/tests/filesystem-cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "filesystem-cli" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[dependencies] +clap = { version = "4.6.1", features = ["derive"] } diff --git a/tests/filesystem-cli/src/main.rs b/tests/filesystem-cli/src/main.rs new file mode 100644 index 0000000..1eda915 --- /dev/null +++ b/tests/filesystem-cli/src/main.rs @@ -0,0 +1,100 @@ +use std::{ + fs::{self, File, OpenOptions}, + io, + path::PathBuf, +}; + +use clap::{Parser, Subcommand}; + +/// componentized filesystem CLI +#[derive(Debug, Parser)] // requires `derive` feature +#[command(name = "filesystem")] +#[command(about = "componentized filesystem CLI", long_about = None)] +struct FilesystemCli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand, Clone)] +enum Commands { + /// List items in a directory + List { + #[arg()] + path: PathBuf, + }, + + /// Read a file + #[command(arg_required_else_help = true)] + Read { + #[arg()] + path: PathBuf, + }, + + /// Write a file + #[command(arg_required_else_help = true)] + Write { + #[arg()] + path: PathBuf, + }, + + /// Append to a file + #[command(arg_required_else_help = true)] + Append { + #[arg()] + path: PathBuf, + }, + + /// Move a file + #[command(arg_required_else_help = true)] + Move { + #[arg()] + from: PathBuf, + + #[arg()] + to: PathBuf, + }, + + /// Remove a file + #[command(arg_required_else_help = true)] + Remove { + #[arg()] + path: PathBuf, + }, +} + +fn main() -> Result<(), std::io::Error> { + match FilesystemCli::parse().command { + Commands::List { path } => { + for entry in fs::read_dir(path)? { + let entry = entry?; + println!("{:?}", entry.file_name()); + } + Ok(()) + } + Commands::Read { path } => { + let mut from = OpenOptions::new().read(true).open(path)?; + let mut to = io::stdout(); + io::copy(&mut from, &mut to)?; + Ok(()) + } + Commands::Write { path } => { + let mut from = io::stdin(); + let mut to = File::create(path)?; + io::copy(&mut from, &mut to)?; + Ok(()) + } + Commands::Append { path } => { + let mut from = io::stdin(); + let mut to = OpenOptions::new().create(true).append(true).open(path)?; + io::copy(&mut from, &mut to)?; + Ok(()) + } + Commands::Move { from, to } => fs::rename(from, to), + Commands::Remove { path } => { + if path.is_dir() { + return fs::remove_dir_all(path); + } + fs::remove_file(path) + } + } +} diff --git a/wit/latch.wit b/wit/latch.wit new file mode 100644 index 0000000..ac56f9c --- /dev/null +++ b/wit/latch.wit @@ -0,0 +1,164 @@ + +interface latch { + use wasi:filesystem/types@0.3.0.{advice, descriptor, descriptor-flags, error-code, filesize, open-flags, new-timestamp, path-flags}; + + record descriptor-get-directory-args { + path: string, + } + + record descriptor-read-via-stream-args { + offset: filesize, + } + + record descriptor-write-via-stream-args { + offset: filesize, + } + + record descriptor-advise-args { + offset: filesize, + length: filesize, + advice: advice, + } + + record descriptor-set-size-args { + size: filesize, + } + + record descriptor-set-times-args { + data-access-timestamp: new-timestamp, + data-modification-timestamp: new-timestamp, + } + + record descriptor-create-directory-at-args { + path: string, + } + + record descriptor-stat-at-args { + path-flags: path-flags, + path: string, + } + + record descriptor-set-times-at-args { + path-flags: path-flags, + path: string, + data-access-timestamp: new-timestamp, + data-modification-timestamp: new-timestamp, + } + + record descriptor-link-at-args { + old-path-flags: path-flags, + old-path: string, + new-descriptor: borrow, + new-path: string, + } + + record descriptor-open-at-args { + path-flags: path-flags, + path: string, + open-flags: open-flags, + %flags: descriptor-flags, + } + + record descriptor-readlink-at-args { + path: string, + } + + record descriptor-remove-directory-at-args { + path: string, + } + + record descriptor-rename-at-args { + old-path: string, + new-descriptor: borrow, + new-path: string, + } + + record descriptor-symlink-at-args { + old-path: string, + new-path: string, + } + + record descriptor-unlink-file-at-args { + path: string, + } + + + record descriptor-metadata-hash-at-args { + path-flags: path-flags, + path: string, + } + + variant preopens-operation { + get-directories-item(tuple, string>), + } + + variant descriptor-operation { + read-via-stream(descriptor-read-via-stream-args), + write-via-stream(descriptor-write-via-stream-args), + append-via-stream, + advise(descriptor-advise-args), + sync-data, + get-flags, + get-type, + set-size(descriptor-set-size-args), + set-times(descriptor-set-times-args), + read-directory, + sync, + create-directory-at(descriptor-create-directory-at-args), + stat, + stat-at(descriptor-stat-at-args), + set-times-at( descriptor-set-times-at-args), + link-at(descriptor-link-at-args), + open-at(descriptor-open-at-args), + readlink-at(descriptor-readlink-at-args), + remove-directory-at(descriptor-remove-directory-at-args), + rename-at(descriptor-rename-at-args), + symlink-at(descriptor-symlink-at-args), + unlink-file-at(descriptor-unlink-file-at-args), + metadata-hash, + metadata-hash-at(descriptor-metadata-hash-at-args), + } + + variant operation { + preopens(preopens-operation), + descriptor(tuple, string, descriptor-operation>), + } + + variant decision { + granted, + denied(error-code), + } + + authorize: func(operation: operation) -> option; +} + +interface latch0 { + use latch.{operation, decision}; + + authorize: func(operation: operation) -> option; +} + + +interface latch1 { + use latch.{operation, decision}; + + authorize: func(operation: operation) -> option; +} + +interface latch2 { + use latch.{operation, decision}; + + authorize: func(operation: operation) -> option; +} + +interface latch3 { + use latch.{operation, decision}; + + authorize: func(operation: operation) -> option; +} + +interface latch4 { + use latch.{operation, decision}; + + authorize: func(operation: operation) -> option; +} diff --git a/wit/worlds.wit b/wit/worlds.wit index d527e48..5d6ea15 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,6 +1,7 @@ package componentized:filesystem; world filesystem { + import latch; import wasi:config/store@0.2.0-rc.1; import wasi:logging/logging@0.1.0-draft; import wasi:filesystem/preopens@0.3.0; @@ -8,3 +9,19 @@ world filesystem { import wasi:filesystem/types@0.3.0; export wasi:filesystem/types@0.3.0; } + +world filesystem-latch { + import wasi:config/store@0.2.0-rc.1; + export latch; +} + +world filesystem-latch-n { + export latch; + import latch; + import latch1; + import latch2; + import latch3; + import latch4; + import wasi:filesystem/preopens@0.3.0; + import wasi:filesystem/types@0.3.0; +}