diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3900c29149..c381ee0d7c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master + with: + submodules: true - uses: ./.github/actions/install-rust - run: cargo fmt --all -- --check @@ -41,6 +43,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master + with: + submodules: true - uses: ./.github/actions/install-rust - run: cargo doc --no-deps -p wasmtime - run: cargo doc --no-deps -p wasmtime-api @@ -52,6 +56,7 @@ jobs: - run: cargo doc --no-deps -p wasmtime-runtime - run: cargo doc --no-deps -p wasmtime-wasi - run: cargo doc --no-deps -p wasmtime-wast + - run: cargo doc --no-deps -p wasi-common - uses: actions/upload-artifact@v1 with: name: doc-api @@ -90,10 +95,14 @@ jobs: with: toolchain: ${{ matrix.rust }} + # Install wasm32-wasi target in order to build wasi-common's integration + # tests + - run: rustup target add wasm32-wasi + - run: cargo fetch # Build and test all features except for lightbeam - - run: cargo test --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py -- --nocapture + - run: cargo test --features wasi-common/wasm_tests --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py -- --nocapture env: RUST_BACKTRACE: 1 @@ -119,6 +128,8 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@master + with: + submodules: true - uses: ./.github/actions/install-rust with: toolchain: nightly-2019-08-15 @@ -196,6 +207,10 @@ jobs: submodules: true - uses: ./.github/actions/install-rust - uses: ./.github/actions/binary-compatible-builds + + # Install wasm32-wasi target in order to build wasi-common's integration + # tests + - run: rustup target add wasm32-wasi # Build `wasmtime` and executables - run: $CENTOS cargo build --release --bin wasmtime --bin wasm2obj @@ -204,7 +219,7 @@ jobs: - run: $CENTOS cargo build --release --features wasm-c-api --manifest-path wasmtime-api/Cargo.toml shell: bash # Test what we just built - - run: $CENTOS cargo test --release --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py --exclude wasmtime-api + - run: $CENTOS cargo test --features wasi-common/wasm_tests --release --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py --exclude wasmtime-api shell: bash env: RUST_BACKTRACE: 1 diff --git a/.gitmodules b/.gitmodules index 65787f6699..e64d540ed0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "wasmtime-api/c-examples/wasm-c-api"] path = wasmtime-api/c-examples/wasm-c-api url = https://github.com/WebAssembly/wasm-c-api +[submodule "wasi-common/WASI"] + path = wasi-common/WASI + url = https://github.com/WebAssembly/WASI diff --git a/Cargo.toml b/Cargo.toml index d4111d7472..60972c775c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ wasmtime-obj = { path = "wasmtime-obj" } wasmtime-wast = { path = "wasmtime-wast" } wasmtime-wasi = { path = "wasmtime-wasi" } wasmtime-wasi-c = { path = "wasmtime-wasi-c", optional = true } -wasi-common = { git = "https://github.com/CraneStation/wasi-common", rev = "2fe3530"} +wasi-common = { path = "wasi-common" } docopt = "1.0.1" serde = { "version" = "1.0.94", features = ["derive"] } faerie = "0.12.0" @@ -48,6 +48,8 @@ members = [ "misc/wasmtime-py", ] +exclude = ["wasi-common/wasi-misc-tests"] + [features] # Enable all supported architectures by default. default = ["cranelift-codegen/all-arch"] diff --git a/wasi-common/Cargo.toml b/wasi-common/Cargo.toml new file mode 100644 index 0000000000..4da67c2683 --- /dev/null +++ b/wasi-common/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "wasi-common" +version = "0.5.0" +authors = ["The Wasmtime Project Developers"] +edition = "2018" +license = "Apache-2.0 WITH LLVM-exception" +description = "WASI implementation in Rust" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/CraneStation/wasmtime" +readme = "README.md" + +[features] +# this feature requires wasm32-wasi target installed, and it enables wasm32 +# integration tests when run with `cargo test --features wasm_tests` +wasm_tests = [] + +[dependencies] +wasi-common-cbindgen = { path = "wasi-common-cbindgen", version = "0.5.0" } +failure = "0.1" +libc = "0.2" +rand = "0.7" +cfg-if = "0.1.9" +log = "0.4" +filetime = "0.2.7" +lazy_static = "1.4.0" +num = { version = "0.2.0", default-features = false } +wig = { path = "wig", version = "0.1.0" } + +[target.'cfg(unix)'.dependencies] +nix = "0.15" + +[target.'cfg(windows)'.dependencies] +winx = { path = "winx", version = "0.5.0" } +winapi = "0.3" +cpu-time = "1.0" + +[dev-dependencies] +wasmtime-runtime = { path = "../wasmtime-runtime" } +wasmtime-environ = { path = "../wasmtime-environ" } +wasmtime-jit = { path = "../wasmtime-jit" } +wasmtime-wasi = { path = "../wasmtime-wasi" } +wasmtime-api = { path = "../wasmtime-api" } +cranelift-codegen = "0.49" +target-lexicon = "0.8.1" +pretty_env_logger = "0.3.0" +tempfile = "3.1.0" + +[build-dependencies] +cfg-if = "0.1.9" + +[lib] +name = "wasi_common" +crate-type = ["rlib", "staticlib", "cdylib"] + diff --git a/wasi-common/LICENSE b/wasi-common/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/wasi-common/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/wasi-common/LICENSE.cloudabi-utils b/wasi-common/LICENSE.cloudabi-utils new file mode 100644 index 0000000000..04c6f48a27 --- /dev/null +++ b/wasi-common/LICENSE.cloudabi-utils @@ -0,0 +1,24 @@ +All code is distributed under the following license: + + Copyright (c) 2015 Nuxi, https://nuxi.nl/ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. diff --git a/wasi-common/README.md b/wasi-common/README.md new file mode 100644 index 0000000000..0a9c39311f --- /dev/null +++ b/wasi-common/README.md @@ -0,0 +1,63 @@ +# wasi-common + +The `wasi-common` crate will ultimately serve as a library providing a common implementation of +WASI hostcalls for re-use in any WASI (and potentially non-WASI) runtimes +such as [Wasmtime] and [Lucet]. + +The library is an adaption of [lucet-wasi] crate from the [Lucet] project, and it is +currently based on [40ae1df][lucet-wasi-tracker] git revision. + +Please note that the library requires Rust compiler version at least 1.37.0. + +[Wasmtime]: https://github.com/CraneStation/wasmtime +[Lucet]: https://github.com/fastly/lucet +[lucet-wasi]: https://github.com/fastly/lucet/tree/master/lucet-wasi +[lucet-wasi-tracker]: https://github.com/fastly/lucet/commit/40ae1df64536250a2b6ab67e7f167d22f4aa7f94 + +## Supported syscalls + +### *nix +In our *nix implementation, we currently support the entire [WASI API] +with the exception of socket hostcalls: +- `sock_recv` +- `sock_send` +- `sock_shutdown` + +We expect these to be implemented when network access is standardised. + +We also currently do not support the `proc_raise` hostcall, as it is expected to +be dropped entirely from WASI. + +[WASI API]: https://github.com/CraneStation/wasmtime/blob/master/docs/WASI-api.md + +### Windows +In our Windows implementation, we currently support the minimal subset of [WASI API] +which allows for running the very basic "Hello world!" style WASM apps. More coming shortly, +so stay tuned! + +## Development hints +When testing the crate, you may want to enable and run full wasm32 integration testsuite. This +requires `wasm32-wasi` target installed which can be done as follows using [rustup] + +``` +rustup target add wasm32-wasi +``` + +[rustup]: https://rustup.rs + +Next initiate submodules containing the integration testsuite + +``` +git submodule update --init +``` + +Now, you should be able to run the integration testsuite by enabling the `wasm_tests` feature + +``` +cargo test --features wasm_tests +``` + +## Third-Party Code +Significant parts of our hostcall implementations are derived from the C implementations in +`cloudabi-utils`. See [LICENSE.cloudabi-utils](LICENSE.cloudabi-utils) for license information. + diff --git a/wasi-common/WASI b/wasi-common/WASI new file mode 160000 index 0000000000..7a5f477fe4 --- /dev/null +++ b/wasi-common/WASI @@ -0,0 +1 @@ +Subproject commit 7a5f477fe411c2887d0689184d4aadbfc4892c0f diff --git a/wasi-common/build.rs b/wasi-common/build.rs new file mode 100644 index 0000000000..1a34fe59e0 --- /dev/null +++ b/wasi-common/build.rs @@ -0,0 +1,219 @@ +//! Build program to generate a program which runs all the testsuites. +//! +//! By generating a separate `#[test]` test for each file, we allow cargo test +//! to automatically run the files in parallel. +//! +//! Idea adapted from: https://github.com/CraneStation/wasmtime/blob/master/build.rs +//! Thanks @sunfishcode + +fn main() { + #[cfg(feature = "wasm_tests")] + wasm_tests::build_and_generate_tests(); +} + +#[cfg(feature = "wasm_tests")] +mod wasm_tests { + use std::env; + use std::fs::{read_dir, DirEntry, File}; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + use std::process::{Command, Stdio}; + + pub(crate) fn build_and_generate_tests() { + // Validate if any of test sources are present and if they changed + // This should always work since there is no submodule to init anymore + let bin_tests = std::fs::read_dir("wasi-misc-tests/src/bin").unwrap(); + for test in bin_tests { + if let Ok(test_file) = test { + let test_file_path = test_file + .path() + .into_os_string() + .into_string() + .expect("test file path"); + println!("cargo:rerun-if-changed={}", test_file_path); + } + } + // Build tests to OUT_DIR (target/*/build/wasi-common-*/out/wasm32-wasi/release/*.wasm) + let out_dir = PathBuf::from( + env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set"), + ); + let mut out = File::create(out_dir.join("wasi_misc_tests.rs")) + .expect("error generating test source file"); + build_tests("wasi-misc-tests", &out_dir).expect("building tests"); + test_directory(&mut out, "wasi-misc-tests", &out_dir).expect("generating tests"); + } + + fn build_tests(testsuite: &str, out_dir: &Path) -> io::Result<()> { + let mut cmd = Command::new("cargo"); + cmd.args(&[ + "build", + "--release", + "--target=wasm32-wasi", + "--target-dir", + out_dir.to_str().unwrap(), + ]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .current_dir(testsuite); + let output = cmd.output()?; + + let status = output.status; + if !status.success() { + panic!( + "Building tests failed: exit code: {}", + status.code().unwrap() + ); + } + + Ok(()) + } + + fn test_directory(out: &mut File, testsuite: &str, out_dir: &Path) -> io::Result<()> { + let mut dir_entries: Vec<_> = read_dir(out_dir.join("wasm32-wasi/release")) + .expect("reading testsuite directory") + .map(|r| r.expect("reading testsuite directory entry")) + .filter(|dir_entry| { + let p = dir_entry.path(); + if let Some(ext) = p.extension() { + // Only look at wast files. + if ext == "wasm" { + // Ignore files starting with `.`, which could be editor temporary files + if let Some(stem) = p.file_stem() { + if let Some(stemstr) = stem.to_str() { + if !stemstr.starts_with('.') { + return true; + } + } + } + } + } + false + }) + .collect(); + + dir_entries.sort_by_key(|dir| dir.path()); + + writeln!( + out, + "mod {} {{", + Path::new(testsuite) + .file_stem() + .expect("testsuite filename should have a stem") + .to_str() + .expect("testsuite filename should be representable as a string") + .replace("-", "_") + )?; + writeln!(out, " use super::{{runtime, utils, setup_log}};")?; + for dir_entry in dir_entries { + write_testsuite_tests(out, dir_entry, testsuite)?; + } + writeln!(out, "}}")?; + Ok(()) + } + + fn write_testsuite_tests( + out: &mut File, + dir_entry: DirEntry, + testsuite: &str, + ) -> io::Result<()> { + let path = dir_entry.path(); + let stemstr = path + .file_stem() + .expect("file_stem") + .to_str() + .expect("to_str"); + + writeln!(out, " #[test]")?; + if ignore(testsuite, stemstr) { + writeln!(out, " #[ignore]")?; + } + writeln!( + out, + " fn {}() -> Result<(), String> {{", + avoid_keywords(&stemstr.replace("-", "_")) + )?; + writeln!(out, " setup_log();")?; + write!(out, " let path = std::path::Path::new(\"")?; + // Write out the string with escape_debug to prevent special characters such + // as backslash from being reinterpreted. + for c in path.display().to_string().chars() { + write!(out, "{}", c.escape_debug())?; + } + writeln!(out, "\");")?; + writeln!(out, " let data = utils::read_wasm(path)?;")?; + writeln!( + out, + " let bin_name = utils::extract_exec_name_from_path(path)?;" + )?; + let workspace = if no_preopens(testsuite, stemstr) { + "None" + } else { + writeln!( + out, + " let workspace = utils::prepare_workspace(&bin_name)?;" + )?; + "Some(workspace.path())" + }; + writeln!( + out, + " runtime::instantiate(&data, &bin_name, {})", + workspace + )?; + writeln!(out, " }}")?; + writeln!(out)?; + Ok(()) + } + + /// Rename tests which have the same name as Rust keywords. + fn avoid_keywords(name: &str) -> &str { + match name { + "if" => "if_", + "loop" => "loop_", + "type" => "type_", + "const" => "const_", + "return" => "return_", + other => other, + } + } + + cfg_if::cfg_if! { + if #[cfg(not(windows))] { + /// Ignore tests that aren't supported yet. + fn ignore(_testsuite: &str, _name: &str) -> bool { + false + } + } else { + /// Ignore tests that aren't supported yet. + fn ignore(testsuite: &str, name: &str) -> bool { + if testsuite == "wasi-misc-tests" { + match name { + "readlink_no_buffer" => true, + "dangling_symlink" => true, + "symlink_loop" => true, + "truncation_rights" => true, + "path_rename_trailing_slashes" => true, + "fd_readdir" => true, + "poll_oneoff" => true, + _ => false, + } + } else { + unreachable!() + } + } + } + } + + /// Mark tests which do not require preopens + fn no_preopens(testsuite: &str, name: &str) -> bool { + if testsuite == "wasi-misc-tests" { + match name { + "big_random_buf" => true, + "clock_time_get" => true, + "sched_yield" => true, + _ => false, + } + } else { + unreachable!() + } + } +} diff --git a/wasi-common/src/ctx.rs b/wasi-common/src/ctx.rs new file mode 100644 index 0000000000..15994e4c4d --- /dev/null +++ b/wasi-common/src/ctx.rs @@ -0,0 +1,335 @@ +use crate::fdentry::FdEntry; +use crate::{wasi, Error, Result}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::env; +use std::ffi::{CString, OsString}; +use std::fs::File; +use std::path::{Path, PathBuf}; + +enum PendingFdEntry { + Thunk(fn() -> Result), + File(File), +} + +impl std::fmt::Debug for PendingFdEntry { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PendingFdEntry::Thunk(f) => write!( + fmt, + "PendingFdEntry::Thunk({:p})", + f as *const fn() -> Result + ), + PendingFdEntry::File(f) => write!(fmt, "PendingFdEntry::File({:?})", f), + } + } +} + +#[derive(Debug, Eq, Hash, PartialEq)] +enum PendingCString { + Bytes(Vec), + OsString(OsString), +} + +impl From> for PendingCString { + fn from(bytes: Vec) -> Self { + Self::Bytes(bytes) + } +} + +impl From for PendingCString { + fn from(s: OsString) -> Self { + Self::OsString(s) + } +} + +impl PendingCString { + fn into_string(self) -> Result { + match self { + PendingCString::Bytes(v) => String::from_utf8(v).map_err(|_| Error::EILSEQ), + PendingCString::OsString(s) => s.into_string().map_err(|_| Error::EILSEQ), + } + } + + /// Create a `CString` containing valid UTF-8, or fail with `Error::EILSEQ`. + fn into_utf8_cstring(self) -> Result { + self.into_string() + .and_then(|s| CString::new(s).map_err(|_| Error::EILSEQ)) + } +} + +/// A builder allowing customizable construction of `WasiCtx` instances. +pub struct WasiCtxBuilder { + fds: HashMap, + preopens: Vec<(PathBuf, File)>, + args: Vec, + env: HashMap, +} + +impl WasiCtxBuilder { + /// Builder for a new `WasiCtx`. + pub fn new() -> Self { + let mut builder = Self { + fds: HashMap::new(), + preopens: Vec::new(), + args: vec![], + env: HashMap::new(), + }; + + builder.fds.insert(0, PendingFdEntry::Thunk(FdEntry::null)); + builder.fds.insert(1, PendingFdEntry::Thunk(FdEntry::null)); + builder.fds.insert(2, PendingFdEntry::Thunk(FdEntry::null)); + + builder + } + + /// Add arguments to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn args>(mut self, args: impl IntoIterator) -> Self { + self.args = args + .into_iter() + .map(|arg| arg.as_ref().to_vec().into()) + .collect(); + self + } + + /// Add an argument to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn arg>(mut self, arg: S) -> Self { + self.args.push(arg.as_ref().to_vec().into()); + self + } + + /// Inherit the command-line arguments from the host process. + /// + /// If any arguments from the host process contain invalid UTF-8, `WasiCtxBuilder::build()` will + /// fail with `Error::EILSEQ`. + pub fn inherit_args(mut self) -> Self { + self.args = env::args_os().map(PendingCString::OsString).collect(); + self + } + + /// Inherit the stdin, stdout, and stderr streams from the host process. + pub fn inherit_stdio(mut self) -> Self { + self.fds + .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); + self.fds + .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); + self.fds + .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); + self + } + + /// Inherit the environment variables from the host process. + /// + /// If any environment variables from the host process contain invalid Unicode (UTF-16 for + /// Windows, UTF-8 for other platforms), `WasiCtxBuilder::build()` will fail with + /// `Error::EILSEQ`. + pub fn inherit_env(mut self) -> Self { + self.env = std::env::vars_os() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + self + } + + /// Add an entry to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn env>(mut self, k: S, v: S) -> Self { + self.env + .insert(k.as_ref().to_vec().into(), v.as_ref().to_vec().into()); + self + } + + /// Add entries to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn envs, T: Borrow<(S, S)>>( + mut self, + envs: impl IntoIterator, + ) -> Self { + self.env = envs + .into_iter() + .map(|t| { + let (k, v) = t.borrow(); + (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) + }) + .collect(); + self + } + + /// Provide a File to use as stdin + pub fn stdin(mut self, file: File) -> Self { + self.fds.insert(0, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stdout + pub fn stdout(mut self, file: File) -> Self { + self.fds.insert(1, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stderr + pub fn stderr(mut self, file: File) -> Self { + self.fds.insert(2, PendingFdEntry::File(file)); + self + } + + /// Add a preopened directory. + pub fn preopened_dir>(mut self, dir: File, guest_path: P) -> Self { + self.preopens.push((guest_path.as_ref().to_owned(), dir)); + self + } + + /// Build a `WasiCtx`, consuming this `WasiCtxBuilder`. + /// + /// If any of the arguments or environment variables in this builder cannot be converted into + /// `CString`s, either due to NUL bytes or Unicode conversions, this returns `Error::EILSEQ`. + pub fn build(self) -> Result { + // Process arguments and environment variables into `CString`s, failing quickly if they + // contain any NUL bytes, or if conversion from `OsString` fails. + let args = self + .args + .into_iter() + .map(|arg| arg.into_utf8_cstring()) + .collect::>>()?; + + let env = self + .env + .into_iter() + .map(|(k, v)| { + k.into_string().and_then(|mut pair| { + v.into_string().and_then(|v| { + pair.push('='); + pair.push_str(v.as_str()); + // We have valid UTF-8, but the keys and values have not yet been checked + // for NULs, so we do a final check here. + CString::new(pair).map_err(|_| Error::EILSEQ) + }) + }) + }) + .collect::>>()?; + + let mut fds: HashMap = HashMap::new(); + // Populate the non-preopen fds. + for (fd, pending) in self.fds { + log::debug!("WasiCtx inserting ({:?}, {:?})", fd, pending); + match pending { + PendingFdEntry::Thunk(f) => { + fds.insert(fd, f()?); + } + PendingFdEntry::File(f) => { + fds.insert(fd, FdEntry::from(f)?); + } + } + } + // Then add the preopen fds. Startup code in the guest starts looking at fd 3 for preopens, + // so we start from there. This variable is initially 2, though, because the loop + // immediately does the increment and check for overflow. + let mut preopen_fd: wasi::__wasi_fd_t = 2; + for (guest_path, dir) in self.preopens { + // We do the increment at the beginning of the loop body, so that we don't overflow + // unnecessarily if we have exactly the maximum number of file descriptors. + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + + if !dir.metadata()?.is_dir() { + return Err(Error::EBADF); + } + + // We don't currently allow setting file descriptors other than 0-2, but this will avoid + // collisions if we restore that functionality in the future. + while fds.contains_key(&preopen_fd) { + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + } + let mut fe = FdEntry::from(dir)?; + fe.preopen_path = Some(guest_path); + log::debug!("WasiCtx inserting ({:?}, {:?})", preopen_fd, fe); + fds.insert(preopen_fd, fe); + log::debug!("WasiCtx fds = {:?}", fds); + } + + Ok(WasiCtx { args, env, fds }) + } +} + +#[derive(Debug)] +pub struct WasiCtx { + fds: HashMap, + pub(crate) args: Vec, + pub(crate) env: Vec, +} + +impl WasiCtx { + /// Make a new `WasiCtx` with some default settings. + /// + /// - File descriptors 0, 1, and 2 inherit stdin, stdout, and stderr from the host process. + /// + /// - Environment variables are inherited from the host process. + /// + /// To override these behaviors, use `WasiCtxBuilder`. + pub fn new>(args: impl IntoIterator) -> Result { + WasiCtxBuilder::new() + .args(args) + .inherit_stdio() + .inherit_env() + .build() + } + + /// Check if `WasiCtx` contains the specified raw WASI `fd`. + pub(crate) unsafe fn contains_fd_entry(&self, fd: wasi::__wasi_fd_t) -> bool { + self.fds.contains_key(&fd) + } + + /// Get an immutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry(&self, fd: wasi::__wasi_fd_t) -> Result<&FdEntry> { + self.fds.get(&fd).ok_or(Error::EBADF) + } + + /// Get a mutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry_mut( + &mut self, + fd: wasi::__wasi_fd_t, + ) -> Result<&mut FdEntry> { + self.fds.get_mut(&fd).ok_or(Error::EBADF) + } + + /// Insert the specified `FdEntry` into the `WasiCtx` object. + /// + /// The `FdEntry` will automatically get another free raw WASI `fd` assigned. Note that + /// the two subsequent free raw WASI `fd`s do not have to be stored contiguously. + pub(crate) fn insert_fd_entry(&mut self, fe: FdEntry) -> Result { + // Never insert where stdio handles are expected to be. + let mut fd = 3; + while self.fds.contains_key(&fd) { + if let Some(next_fd) = fd.checked_add(1) { + fd = next_fd; + } else { + return Err(Error::EMFILE); + } + } + self.fds.insert(fd, fe); + Ok(fd) + } + + /// Insert the specified `FdEntry` with the specified raw WASI `fd` key into the `WasiCtx` + /// object. + pub(crate) fn insert_fd_entry_at( + &mut self, + fd: wasi::__wasi_fd_t, + fe: FdEntry, + ) -> Option { + self.fds.insert(fd, fe) + } + + /// Remove `FdEntry` corresponding to the specified raw WASI `fd` from the `WasiCtx` object. + pub(crate) fn remove_fd_entry(&mut self, fd: wasi::__wasi_fd_t) -> Result { + self.fds.remove(&fd).ok_or(Error::EBADF) + } +} diff --git a/wasi-common/src/error.rs b/wasi-common/src/error.rs new file mode 100644 index 0000000000..7152844131 --- /dev/null +++ b/wasi-common/src/error.rs @@ -0,0 +1,280 @@ +// Due to https://github.com/rust-lang/rust/issues/64247 +#![allow(clippy::use_self)] +use crate::wasi; +use failure::Fail; +use std::convert::Infallible; +use std::fmt; +use std::num::TryFromIntError; +use std::str; + +#[derive(Clone, Copy, Debug, Fail, Eq, PartialEq)] +#[repr(u16)] +pub enum WasiError { + ESUCCESS = wasi::__WASI_ESUCCESS, + E2BIG = wasi::__WASI_E2BIG, + EACCES = wasi::__WASI_EACCES, + EADDRINUSE = wasi::__WASI_EADDRINUSE, + EADDRNOTAVAIL = wasi::__WASI_EADDRNOTAVAIL, + EAFNOSUPPORT = wasi::__WASI_EAFNOSUPPORT, + EAGAIN = wasi::__WASI_EAGAIN, + EALREADY = wasi::__WASI_EALREADY, + EBADF = wasi::__WASI_EBADF, + EBADMSG = wasi::__WASI_EBADMSG, + EBUSY = wasi::__WASI_EBUSY, + ECANCELED = wasi::__WASI_ECANCELED, + ECHILD = wasi::__WASI_ECHILD, + ECONNABORTED = wasi::__WASI_ECONNABORTED, + ECONNREFUSED = wasi::__WASI_ECONNREFUSED, + ECONNRESET = wasi::__WASI_ECONNRESET, + EDEADLK = wasi::__WASI_EDEADLK, + EDESTADDRREQ = wasi::__WASI_EDESTADDRREQ, + EDOM = wasi::__WASI_EDOM, + EDQUOT = wasi::__WASI_EDQUOT, + EEXIST = wasi::__WASI_EEXIST, + EFAULT = wasi::__WASI_EFAULT, + EFBIG = wasi::__WASI_EFBIG, + EHOSTUNREACH = wasi::__WASI_EHOSTUNREACH, + EIDRM = wasi::__WASI_EIDRM, + EILSEQ = wasi::__WASI_EILSEQ, + EINPROGRESS = wasi::__WASI_EINPROGRESS, + EINTR = wasi::__WASI_EINTR, + EINVAL = wasi::__WASI_EINVAL, + EIO = wasi::__WASI_EIO, + EISCONN = wasi::__WASI_EISCONN, + EISDIR = wasi::__WASI_EISDIR, + ELOOP = wasi::__WASI_ELOOP, + EMFILE = wasi::__WASI_EMFILE, + EMLINK = wasi::__WASI_EMLINK, + EMSGSIZE = wasi::__WASI_EMSGSIZE, + EMULTIHOP = wasi::__WASI_EMULTIHOP, + ENAMETOOLONG = wasi::__WASI_ENAMETOOLONG, + ENETDOWN = wasi::__WASI_ENETDOWN, + ENETRESET = wasi::__WASI_ENETRESET, + ENETUNREACH = wasi::__WASI_ENETUNREACH, + ENFILE = wasi::__WASI_ENFILE, + ENOBUFS = wasi::__WASI_ENOBUFS, + ENODEV = wasi::__WASI_ENODEV, + ENOENT = wasi::__WASI_ENOENT, + ENOEXEC = wasi::__WASI_ENOEXEC, + ENOLCK = wasi::__WASI_ENOLCK, + ENOLINK = wasi::__WASI_ENOLINK, + ENOMEM = wasi::__WASI_ENOMEM, + ENOMSG = wasi::__WASI_ENOMSG, + ENOPROTOOPT = wasi::__WASI_ENOPROTOOPT, + ENOSPC = wasi::__WASI_ENOSPC, + ENOSYS = wasi::__WASI_ENOSYS, + ENOTCONN = wasi::__WASI_ENOTCONN, + ENOTDIR = wasi::__WASI_ENOTDIR, + ENOTEMPTY = wasi::__WASI_ENOTEMPTY, + ENOTRECOVERABLE = wasi::__WASI_ENOTRECOVERABLE, + ENOTSOCK = wasi::__WASI_ENOTSOCK, + ENOTSUP = wasi::__WASI_ENOTSUP, + ENOTTY = wasi::__WASI_ENOTTY, + ENXIO = wasi::__WASI_ENXIO, + EOVERFLOW = wasi::__WASI_EOVERFLOW, + EOWNERDEAD = wasi::__WASI_EOWNERDEAD, + EPERM = wasi::__WASI_EPERM, + EPIPE = wasi::__WASI_EPIPE, + EPROTO = wasi::__WASI_EPROTO, + EPROTONOSUPPORT = wasi::__WASI_EPROTONOSUPPORT, + EPROTOTYPE = wasi::__WASI_EPROTOTYPE, + ERANGE = wasi::__WASI_ERANGE, + EROFS = wasi::__WASI_EROFS, + ESPIPE = wasi::__WASI_ESPIPE, + ESRCH = wasi::__WASI_ESRCH, + ESTALE = wasi::__WASI_ESTALE, + ETIMEDOUT = wasi::__WASI_ETIMEDOUT, + ETXTBSY = wasi::__WASI_ETXTBSY, + EXDEV = wasi::__WASI_EXDEV, + ENOTCAPABLE = wasi::__WASI_ENOTCAPABLE, +} + +impl WasiError { + pub fn as_raw_errno(self) -> wasi::__wasi_errno_t { + self as wasi::__wasi_errno_t + } +} + +impl fmt::Display for WasiError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + _ => write!(f, "{:?}", self), + } + } +} + +#[derive(Debug, Fail)] +pub enum Error { + Wasi(WasiError), + Io(std::io::Error), + #[cfg(unix)] + Nix(nix::Error), + #[cfg(windows)] + Win(winx::winerror::WinError), +} + +impl From for Error { + fn from(err: WasiError) -> Self { + Self::Wasi(err) + } +} + +#[cfg(unix)] +impl From for Error { + fn from(err: nix::Error) -> Self { + Self::Nix(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +impl From for Error { + fn from(_: TryFromIntError) -> Self { + Self::Wasi(WasiError::EOVERFLOW) + } +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +impl From for Error { + fn from(_: str::Utf8Error) -> Self { + Self::Wasi(WasiError::EILSEQ) + } +} + +#[cfg(windows)] +impl From for Error { + fn from(err: winx::winerror::WinError) -> Self { + Self::Win(err) + } +} + +impl Error { + pub(crate) fn as_wasi_errno(&self) -> wasi::__wasi_errno_t { + match self { + Self::Wasi(no) => no.as_raw_errno(), + Self::Io(e) => errno_from_ioerror(e.to_owned()), + #[cfg(unix)] + Self::Nix(err) => err + .as_errno() + .map_or_else( + || { + log::debug!("Unknown nix errno: {}", err); + Self::ENOSYS + }, + crate::sys::host_impl::errno_from_nix, + ) + .as_wasi_errno(), + #[cfg(windows)] + Self::Win(err) => crate::sys::host_impl::errno_from_win(*err), + } + } + pub const ESUCCESS: Self = Error::Wasi(WasiError::ESUCCESS); + pub const E2BIG: Self = Error::Wasi(WasiError::E2BIG); + pub const EACCES: Self = Error::Wasi(WasiError::EACCES); + pub const EADDRINUSE: Self = Error::Wasi(WasiError::EADDRINUSE); + pub const EADDRNOTAVAIL: Self = Error::Wasi(WasiError::EADDRNOTAVAIL); + pub const EAFNOSUPPORT: Self = Error::Wasi(WasiError::EAFNOSUPPORT); + pub const EAGAIN: Self = Error::Wasi(WasiError::EAGAIN); + pub const EALREADY: Self = Error::Wasi(WasiError::EALREADY); + pub const EBADF: Self = Error::Wasi(WasiError::EBADF); + pub const EBADMSG: Self = Error::Wasi(WasiError::EBADMSG); + pub const EBUSY: Self = Error::Wasi(WasiError::EBUSY); + pub const ECANCELED: Self = Error::Wasi(WasiError::ECANCELED); + pub const ECHILD: Self = Error::Wasi(WasiError::ECHILD); + pub const ECONNABORTED: Self = Error::Wasi(WasiError::ECONNABORTED); + pub const ECONNREFUSED: Self = Error::Wasi(WasiError::ECONNREFUSED); + pub const ECONNRESET: Self = Error::Wasi(WasiError::ECONNRESET); + pub const EDEADLK: Self = Error::Wasi(WasiError::EDEADLK); + pub const EDESTADDRREQ: Self = Error::Wasi(WasiError::EDESTADDRREQ); + pub const EDOM: Self = Error::Wasi(WasiError::EDOM); + pub const EDQUOT: Self = Error::Wasi(WasiError::EDQUOT); + pub const EEXIST: Self = Error::Wasi(WasiError::EEXIST); + pub const EFAULT: Self = Error::Wasi(WasiError::EFAULT); + pub const EFBIG: Self = Error::Wasi(WasiError::EFBIG); + pub const EHOSTUNREACH: Self = Error::Wasi(WasiError::EHOSTUNREACH); + pub const EIDRM: Self = Error::Wasi(WasiError::EIDRM); + pub const EILSEQ: Self = Error::Wasi(WasiError::EILSEQ); + pub const EINPROGRESS: Self = Error::Wasi(WasiError::EINPROGRESS); + pub const EINTR: Self = Error::Wasi(WasiError::EINTR); + pub const EINVAL: Self = Error::Wasi(WasiError::EINVAL); + pub const EIO: Self = Error::Wasi(WasiError::EIO); + pub const EISCONN: Self = Error::Wasi(WasiError::EISCONN); + pub const EISDIR: Self = Error::Wasi(WasiError::EISDIR); + pub const ELOOP: Self = Error::Wasi(WasiError::ELOOP); + pub const EMFILE: Self = Error::Wasi(WasiError::EMFILE); + pub const EMLINK: Self = Error::Wasi(WasiError::EMLINK); + pub const EMSGSIZE: Self = Error::Wasi(WasiError::EMSGSIZE); + pub const EMULTIHOP: Self = Error::Wasi(WasiError::EMULTIHOP); + pub const ENAMETOOLONG: Self = Error::Wasi(WasiError::ENAMETOOLONG); + pub const ENETDOWN: Self = Error::Wasi(WasiError::ENETDOWN); + pub const ENETRESET: Self = Error::Wasi(WasiError::ENETRESET); + pub const ENETUNREACH: Self = Error::Wasi(WasiError::ENETUNREACH); + pub const ENFILE: Self = Error::Wasi(WasiError::ENFILE); + pub const ENOBUFS: Self = Error::Wasi(WasiError::ENOBUFS); + pub const ENODEV: Self = Error::Wasi(WasiError::ENODEV); + pub const ENOENT: Self = Error::Wasi(WasiError::ENOENT); + pub const ENOEXEC: Self = Error::Wasi(WasiError::ENOEXEC); + pub const ENOLCK: Self = Error::Wasi(WasiError::ENOLCK); + pub const ENOLINK: Self = Error::Wasi(WasiError::ENOLINK); + pub const ENOMEM: Self = Error::Wasi(WasiError::ENOMEM); + pub const ENOMSG: Self = Error::Wasi(WasiError::ENOMSG); + pub const ENOPROTOOPT: Self = Error::Wasi(WasiError::ENOPROTOOPT); + pub const ENOSPC: Self = Error::Wasi(WasiError::ENOSPC); + pub const ENOSYS: Self = Error::Wasi(WasiError::ENOSYS); + pub const ENOTCONN: Self = Error::Wasi(WasiError::ENOTCONN); + pub const ENOTDIR: Self = Error::Wasi(WasiError::ENOTDIR); + pub const ENOTEMPTY: Self = Error::Wasi(WasiError::ENOTEMPTY); + pub const ENOTRECOVERABLE: Self = Error::Wasi(WasiError::ENOTRECOVERABLE); + pub const ENOTSOCK: Self = Error::Wasi(WasiError::ENOTSOCK); + pub const ENOTSUP: Self = Error::Wasi(WasiError::ENOTSUP); + pub const ENOTTY: Self = Error::Wasi(WasiError::ENOTTY); + pub const ENXIO: Self = Error::Wasi(WasiError::ENXIO); + pub const EOVERFLOW: Self = Error::Wasi(WasiError::EOVERFLOW); + pub const EOWNERDEAD: Self = Error::Wasi(WasiError::EOWNERDEAD); + pub const EPERM: Self = Error::Wasi(WasiError::EPERM); + pub const EPIPE: Self = Error::Wasi(WasiError::EPIPE); + pub const EPROTO: Self = Error::Wasi(WasiError::EPROTO); + pub const EPROTONOSUPPORT: Self = Error::Wasi(WasiError::EPROTONOSUPPORT); + pub const EPROTOTYPE: Self = Error::Wasi(WasiError::EPROTOTYPE); + pub const ERANGE: Self = Error::Wasi(WasiError::ERANGE); + pub const EROFS: Self = Error::Wasi(WasiError::EROFS); + pub const ESPIPE: Self = Error::Wasi(WasiError::ESPIPE); + pub const ESRCH: Self = Error::Wasi(WasiError::ESRCH); + pub const ESTALE: Self = Error::Wasi(WasiError::ESTALE); + pub const ETIMEDOUT: Self = Error::Wasi(WasiError::ETIMEDOUT); + pub const ETXTBSY: Self = Error::Wasi(WasiError::ETXTBSY); + pub const EXDEV: Self = Error::Wasi(WasiError::EXDEV); + pub const ENOTCAPABLE: Self = Error::Wasi(WasiError::ENOTCAPABLE); +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Io(e) => e.fmt(f), + Self::Wasi(e) => e.fmt(f), + #[cfg(unix)] + Self::Nix(e) => e.fmt(f), + #[cfg(windows)] + Self::Win(e) => e.fmt(f), + } + } +} + +fn errno_from_ioerror(e: &std::io::Error) -> wasi::__wasi_errno_t { + match e.raw_os_error() { + Some(code) => crate::sys::errno_from_host(code), + None => { + log::debug!("Inconvertible OS error: {}", e); + wasi::__WASI_EIO + } + } +} diff --git a/wasi-common/src/fdentry.rs b/wasi-common/src/fdentry.rs new file mode 100644 index 0000000000..948a8ee459 --- /dev/null +++ b/wasi-common/src/fdentry.rs @@ -0,0 +1,186 @@ +use crate::sys::dev_null; +use crate::sys::fdentry_impl::{determine_type_and_access_rights, OsFile}; +use crate::{wasi, Error, Result}; +use std::path::PathBuf; +use std::{fs, io}; + +#[derive(Debug)] +pub(crate) enum Descriptor { + OsFile(OsFile), + Stdin, + Stdout, + Stderr, +} + +impl Descriptor { + pub(crate) fn as_file(&self) -> Result<&OsFile> { + match self { + Self::OsFile(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + pub(crate) fn as_file_mut(&mut self) -> Result<&mut OsFile> { + match self { + Self::OsFile(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + pub(crate) fn is_file(&self) -> bool { + match self { + Self::OsFile(_) => true, + _ => false, + } + } + + #[allow(unused)] + pub(crate) fn is_stdin(&self) -> bool { + match self { + Self::Stdin => true, + _ => false, + } + } + + #[allow(unused)] + pub(crate) fn is_stdout(&self) -> bool { + match self { + Self::Stdout => true, + _ => false, + } + } + + #[allow(unused)] + pub(crate) fn is_stderr(&self) -> bool { + match self { + Self::Stderr => true, + _ => false, + } + } +} + +/// An abstraction struct serving as a wrapper for a host `Descriptor` object which requires +/// certain base rights `rights_base` and inheriting rights `rights_inheriting` in order to be +/// accessed correctly. +/// +/// Here, the `descriptor` field stores the host `Descriptor` object (such as a file descriptor, or +/// stdin handle), and accessing it can only be done via the provided `FdEntry::as_descriptor` and +/// `FdEntry::as_descriptor_mut` methods which require a set of base and inheriting rights to be +/// specified, verifying whether the stored `Descriptor` object is valid for the rights specified. +#[derive(Debug)] +pub(crate) struct FdEntry { + pub(crate) file_type: wasi::__wasi_filetype_t, + descriptor: Descriptor, + pub(crate) rights_base: wasi::__wasi_rights_t, + pub(crate) rights_inheriting: wasi::__wasi_rights_t, + pub(crate) preopen_path: Option, + // TODO: directories +} + +impl FdEntry { + pub(crate) fn from(file: fs::File) -> Result { + unsafe { determine_type_and_access_rights(&file) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::OsFile(OsFile::from(file)), + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate(file: &fs::File) -> Result { + Self::from(file.try_clone()?) + } + + pub(crate) fn duplicate_stdin() -> Result { + unsafe { determine_type_and_access_rights(&io::stdin()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdin, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stdout() -> Result { + unsafe { determine_type_and_access_rights(&io::stdout()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdout, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stderr() -> Result { + unsafe { determine_type_and_access_rights(&io::stderr()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stderr, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn null() -> Result { + Self::from(dev_null()?) + } + + /// Convert this `FdEntry` into a host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&self.descriptor) + } + + /// Convert this `FdEntry` into a mutable host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor_mut( + &mut self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&mut Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&mut self.descriptor) + } + + /// Check if this `FdEntry` object satisfies the specified base rights `rights_base`, and + /// inheriting rights `rights_inheriting`; i.e., if rights attached to this `FdEntry` object + /// are a superset. + /// + /// Upon unsuccessful check, `Error::ENOTCAPABLE` is returned. + fn validate_rights( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<()> { + if !self.rights_base & rights_base != 0 || !self.rights_inheriting & rights_inheriting != 0 + { + Err(Error::ENOTCAPABLE) + } else { + Ok(()) + } + } +} diff --git a/wasi-common/src/fs/dir.rs b/wasi-common/src/fs/dir.rs new file mode 100644 index 0000000000..b670ad7ead --- /dev/null +++ b/wasi-common/src/fs/dir.rs @@ -0,0 +1,216 @@ +use crate::fs::{error::wasi_errno_to_io_error, File, OpenOptions, ReadDir}; +use crate::{host, hostcalls, wasi, WasiCtx}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +use std::{io, path::Path}; + +/// A reference to an open directory on the filesystem. +/// +/// TODO: Implement `Dir`-using versions of `std::fs`'s free functions: +/// `copy`, `create_dir`, `create_dir_all`, `hard_link`, `metadata`, +/// `read_link`, `read_to_string`, `remove_dir`, `remove_dir_all`, +/// `remove_file`, `rename`, `set_permissions`, `symlink_metadata`, and +/// `write`. +/// +/// Unlike `std::fs`, this API has no `canonicalize`, because absolute paths +/// don't interoperate well with the capability-oriented security model. +pub struct Dir<'ctx> { + ctx: &'ctx mut WasiCtx, + fd: wasi::__wasi_fd_t, +} + +impl<'ctx> Dir<'ctx> { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + pub unsafe fn from_raw_wasi_fd(ctx: &'ctx mut WasiCtx, fd: wasi::__wasi_fd_t) -> Self { + Self { ctx, fd } + } + + /// Attempts to open a file in read-only mode. + /// + /// This corresponds to [`std::fs::File::open`], but only accesses paths + /// relative to and within `self`. + /// + /// TODO: Not yet implemented. Refactor the hostcalls functions to split out the + /// encoding/decoding parts from the underlying functionality, so that we can call + /// into the underlying functionality directly. + /// + /// [`std::fs::File::open`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.open + pub fn open_file>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: Refactor the hostcalls functions to split out the encoding/decoding + // parts from the underlying functionality, so that we can call into the + // underlying functionality directly. + // + // TODO: Set the requested rights to be readonly. + // + // TODO: Handle paths for non-Unix platforms which don't have `as_bytes()` + // on `OsStrExt`. + unimplemented!("Dir::open_file"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUP_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + path.as_os_str().len(), + 0, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { File::from_raw_wasi_fd(ctx, fd) }) + } + + /// Opens a file at `path` with the options specified by `self`. + /// + /// This corresponds to [`std::fs::OpenOptions::open`]. + /// + /// Instead of being a method on `OpenOptions`, this is a method on `Dir`, + /// and it only accesses functions relative to and within `self`. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::OpenOptions::open`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.open + pub fn open_file_with>( + &mut self, + path: P, + options: &OpenOptions, + ) -> io::Result { + unimplemented!("Dir::open_file_with"); + } + + /// Attempts to open a directory. + /// + /// TODO: Not yet implemented. See the comment in `open_file`. + pub fn open_dir>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: See the comment in `open_file`. + unimplemented!("Dir::open_dir"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUP_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + wasi::__WASI_O_DIRECTORY, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { Dir::from_raw_wasi_fd(ctx, fd) }) + } + + /// Opens a file in write-only mode. + /// + /// This corresponds to [`std::fs::File::create`], but only accesses paths + /// relative to and within `self`. + /// + /// TODO: Not yet implemented. See the comment in `open_file`. + /// + /// [`std::fs::File::create`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.create + pub fn create_file>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: See the comments in `open_file`. + // + // TODO: Set the requested rights to be read+write. + unimplemented!("Dir::create_file"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUP_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + path.as_os_str().len(), + wasi::__WASI_O_CREAT | wasi::__WASI_O_TRUNC, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { File::from_raw_wasi_fd(ctx, fd) }) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`std::fs::read_dir`], but reads the directory + /// represented by `self`. + /// + /// TODO: Not yet implemented. We may need to wait until we have the ability + /// to duplicate file descriptors before we can implement read safely. For + /// now, use `into_read` instead. + /// + /// [`std::fs::read_dir`]: https://doc.rust-lang.org/std/fs/fn.read_dir.html + pub fn read(&mut self) -> io::Result { + unimplemented!("Dir::read") + } + + /// Consumes self and returns an iterator over the entries within a directory + /// in the manner of `read`. + pub fn into_read(self) -> ReadDir { + unsafe { ReadDir::from_raw_wasi_fd(self.fd) } + } + + /// Read the entire contents of a file into a bytes vector. + /// + /// This corresponds to [`std::fs::read`], but only accesses paths + /// relative to and within `self`. + /// + /// [`std::fs::read`]: https://doc.rust-lang.org/std/fs/fn.read.html + pub fn read_file>(&mut self, path: P) -> io::Result> { + use io::Read; + let mut file = self.open_file(path)?; + let mut bytes = Vec::with_capacity(initial_buffer_size(&file)); + file.read_to_end(&mut bytes)?; + Ok(bytes) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`std::fs::read_dir`], but only accesses paths + /// relative to and within `self`. + /// + /// [`std::fs::read_dir`]: https://doc.rust-lang.org/std/fs/fn.read_dir.html + pub fn read_dir>(&mut self, path: P) -> io::Result { + self.open_dir(path)?.read() + } +} + +impl<'ctx> Drop for Dir<'ctx> { + fn drop(&mut self) { + // Note that errors are ignored when closing a file descriptor. The + // reason for this is that if an error occurs we don't actually know if + // the file descriptor was closed or not, and if we retried (for + // something like EINTR), we might close another valid file descriptor + // opened after we closed ours. + let _ = unsafe { hostcalls::fd_close(self.ctx, self.fd) }; + } +} + +/// Indicates how large a buffer to pre-allocate before reading the entire file. +/// +/// Derived from the function of the same name in libstd. +fn initial_buffer_size(file: &File) -> usize { + // Allocate one extra byte so the buffer doesn't need to grow before the + // final `read` call at the end of the file. Don't worry about `usize` + // overflow because reading will fail regardless in that case. + file.metadata().map(|m| m.len() as usize + 1).unwrap_or(0) +} + +// TODO: impl Debug for Dir diff --git a/wasi-common/src/fs/dir_builder.rs b/wasi-common/src/fs/dir_builder.rs new file mode 100644 index 0000000000..aaf44b7345 --- /dev/null +++ b/wasi-common/src/fs/dir_builder.rs @@ -0,0 +1,49 @@ +use std::{io, path::Path}; + +/// A builder used to create directories in various manners. +/// +/// This corresponds to [`std::fs::DirBuilder`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::DirBuilder`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html +pub struct DirBuilder {} + +impl DirBuilder { + /// Creates a new set of options with default mode/security settings for all platforms and also non-recursive. + /// + /// This corresponds to [`std::fs::DirBuilder::new`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::new`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.new + pub fn new() -> Self { + unimplemented!("DirBuilder::new"); + } + + /// Indicates that directories should be created recursively, creating all parent directories. + /// + /// This corresponds to [`std::fs::DirBuilder::recursive`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::recursive`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.recursive + pub fn recursive(&mut self, recursive: bool) -> &mut Self { + unimplemented!("DirBuilder::recursive"); + } + + /// Creates the specified directory with the options configured in this builder. + /// + /// This corresponds to [`std::fs::DirBuilder::create`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::create`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.create + pub fn create>(&self, path: P) -> io::Result<()> { + unimplemented!("DirBuilder::create"); + } +} + +// TODO: functions from DirBuilderExt? + +// TODO: impl Debug for DirBuilder diff --git a/wasi-common/src/fs/dir_entry.rs b/wasi-common/src/fs/dir_entry.rs new file mode 100644 index 0000000000..ff926f1c4c --- /dev/null +++ b/wasi-common/src/fs/dir_entry.rs @@ -0,0 +1,53 @@ +use crate::fs::{FileType, Metadata}; +use std::{ffi, io}; + +/// Entries returned by the ReadDir iterator. +/// +/// This corresponds to [`std::fs::DirEntry`]. +/// +/// Unlike `std::fs::DirEntry`, this API has no `DirEntry::path`, because +/// absolute paths don't interoperate well with the capability-oriented +/// security model. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::DirEntry`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html +pub struct DirEntry {} + +impl DirEntry { + /// Returns the metadata for the file that this entry points at. + /// + /// This corresponds to [`std::fs::DirEntry::metadata`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::metadata`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.metadata + pub fn metadata(&self) -> io::Result { + unimplemented!("DirEntry::metadata"); + } + + /// Returns the file type for the file that this entry points at. + /// + /// This to [`std::fs::DirEntry::file_type`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::file_type`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_type + pub fn file_type(&self) -> io::Result { + unimplemented!("DirEntry::file_type"); + } + + /// Returns the bare file name of this directory entry without any other leading path component. + /// + /// This corresponds to [`std::fs::DirEntry::file_name`], though it returns + /// `String` rather than `OsString`. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::file_name`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_name + pub fn file_name(&self) -> String { + unimplemented!("DirEntry::file_name"); + } +} + +// TODO: impl Debug for DirEntry diff --git a/wasi-common/src/fs/error.rs b/wasi-common/src/fs/error.rs new file mode 100644 index 0000000000..a887c93668 --- /dev/null +++ b/wasi-common/src/fs/error.rs @@ -0,0 +1,265 @@ +use crate::wasi; +use std::io; + +/// Translate a WASI errno code into an `io::Result<()>`. +/// +/// TODO: Would it be better to have our own version of `io::Error` (and +/// `io::Result`), rather than trying to shoehorn WASI errors into the +/// libstd version? +pub(crate) fn wasi_errno_to_io_error(errno: wasi::__wasi_errno_t) -> io::Result<()> { + #[cfg(unix)] + let raw_os_error = match errno { + wasi::__WASI_ESUCCESS => return Ok(()), + wasi::__WASI_EIO => libc::EIO, + wasi::__WASI_EPERM => libc::EPERM, + wasi::__WASI_EINVAL => libc::EINVAL, + wasi::__WASI_EPIPE => libc::EPIPE, + wasi::__WASI_ENOTCONN => libc::ENOTCONN, + wasi::__WASI_E2BIG => libc::E2BIG, + wasi::__WASI_EACCES => libc::EACCES, + wasi::__WASI_EADDRINUSE => libc::EADDRINUSE, + wasi::__WASI_EADDRNOTAVAIL => libc::EADDRNOTAVAIL, + wasi::__WASI_EAFNOSUPPORT => libc::EAFNOSUPPORT, + wasi::__WASI_EAGAIN => libc::EAGAIN, + wasi::__WASI_EALREADY => libc::EALREADY, + wasi::__WASI_EBADF => libc::EBADF, + wasi::__WASI_EBADMSG => libc::EBADMSG, + wasi::__WASI_EBUSY => libc::EBUSY, + wasi::__WASI_ECANCELED => libc::ECANCELED, + wasi::__WASI_ECHILD => libc::ECHILD, + wasi::__WASI_ECONNABORTED => libc::ECONNABORTED, + wasi::__WASI_ECONNREFUSED => libc::ECONNREFUSED, + wasi::__WASI_ECONNRESET => libc::ECONNRESET, + wasi::__WASI_EDEADLK => libc::EDEADLK, + wasi::__WASI_EDESTADDRREQ => libc::EDESTADDRREQ, + wasi::__WASI_EDOM => libc::EDOM, + wasi::__WASI_EDQUOT => libc::EDQUOT, + wasi::__WASI_EEXIST => libc::EEXIST, + wasi::__WASI_EFAULT => libc::EFAULT, + wasi::__WASI_EFBIG => libc::EFBIG, + wasi::__WASI_EHOSTUNREACH => libc::EHOSTUNREACH, + wasi::__WASI_EIDRM => libc::EIDRM, + wasi::__WASI_EILSEQ => libc::EILSEQ, + wasi::__WASI_EINPROGRESS => libc::EINPROGRESS, + wasi::__WASI_EINTR => libc::EINTR, + wasi::__WASI_EISCONN => libc::EISCONN, + wasi::__WASI_EISDIR => libc::EISDIR, + wasi::__WASI_ELOOP => libc::ELOOP, + wasi::__WASI_EMFILE => libc::EMFILE, + wasi::__WASI_EMLINK => libc::EMLINK, + wasi::__WASI_EMSGSIZE => libc::EMSGSIZE, + wasi::__WASI_EMULTIHOP => libc::EMULTIHOP, + wasi::__WASI_ENAMETOOLONG => libc::ENAMETOOLONG, + wasi::__WASI_ENETDOWN => libc::ENETDOWN, + wasi::__WASI_ENETRESET => libc::ENETRESET, + wasi::__WASI_ENETUNREACH => libc::ENETUNREACH, + wasi::__WASI_ENFILE => libc::ENFILE, + wasi::__WASI_ENOBUFS => libc::ENOBUFS, + wasi::__WASI_ENODEV => libc::ENODEV, + wasi::__WASI_ENOENT => libc::ENOENT, + wasi::__WASI_ENOEXEC => libc::ENOEXEC, + wasi::__WASI_ENOLCK => libc::ENOLCK, + wasi::__WASI_ENOLINK => libc::ENOLINK, + wasi::__WASI_ENOMEM => libc::ENOMEM, + wasi::__WASI_ENOMSG => libc::ENOMSG, + wasi::__WASI_ENOPROTOOPT => libc::ENOPROTOOPT, + wasi::__WASI_ENOSPC => libc::ENOSPC, + wasi::__WASI_ENOSYS => libc::ENOSYS, + wasi::__WASI_ENOTDIR => libc::ENOTDIR, + wasi::__WASI_ENOTEMPTY => libc::ENOTEMPTY, + wasi::__WASI_ENOTRECOVERABLE => libc::ENOTRECOVERABLE, + wasi::__WASI_ENOTSOCK => libc::ENOTSOCK, + wasi::__WASI_ENOTSUP => libc::ENOTSUP, + wasi::__WASI_ENOTTY => libc::ENOTTY, + wasi::__WASI_ENXIO => libc::ENXIO, + wasi::__WASI_EOVERFLOW => libc::EOVERFLOW, + wasi::__WASI_EOWNERDEAD => libc::EOWNERDEAD, + wasi::__WASI_EPROTO => libc::EPROTO, + wasi::__WASI_EPROTONOSUPPORT => libc::EPROTONOSUPPORT, + wasi::__WASI_EPROTOTYPE => libc::EPROTOTYPE, + wasi::__WASI_ERANGE => libc::ERANGE, + wasi::__WASI_EROFS => libc::EROFS, + wasi::__WASI_ESPIPE => libc::ESPIPE, + wasi::__WASI_ESRCH => libc::ESRCH, + wasi::__WASI_ESTALE => libc::ESTALE, + wasi::__WASI_ETIMEDOUT => libc::ETIMEDOUT, + wasi::__WASI_ETXTBSY => libc::ETXTBSY, + wasi::__WASI_EXDEV => libc::EXDEV, + #[cfg(target_os = "wasi")] + wasi::__WASI_ENOTCAPABLE => libc::ENOTCAPABLE, + #[cfg(not(target_os = "wasi"))] + wasi::__WASI_ENOTCAPABLE => libc::EIO, + _ => panic!("unexpected wasi errno value"), + }; + + #[cfg(windows)] + use winapi::shared::winerror::*; + + #[cfg(windows)] + let raw_os_error = match errno { + wasi::__WASI_ESUCCESS => return Ok(()), + wasi::__WASI_EINVAL => WSAEINVAL, + wasi::__WASI_EPIPE => ERROR_BROKEN_PIPE, + wasi::__WASI_ENOTCONN => WSAENOTCONN, + wasi::__WASI_EPERM | wasi::__WASI_EACCES => ERROR_ACCESS_DENIED, + wasi::__WASI_EADDRINUSE => WSAEADDRINUSE, + wasi::__WASI_EADDRNOTAVAIL => WSAEADDRNOTAVAIL, + wasi::__WASI_EAGAIN => WSAEWOULDBLOCK, + wasi::__WASI_ECONNABORTED => WSAECONNABORTED, + wasi::__WASI_ECONNREFUSED => WSAECONNREFUSED, + wasi::__WASI_ECONNRESET => WSAECONNRESET, + wasi::__WASI_EEXIST => ERROR_ALREADY_EXISTS, + wasi::__WASI_ENOENT => ERROR_FILE_NOT_FOUND, + wasi::__WASI_ETIMEDOUT => WSAETIMEDOUT, + wasi::__WASI_EAFNOSUPPORT => WSAEAFNOSUPPORT, + wasi::__WASI_EALREADY => WSAEALREADY, + wasi::__WASI_EBADF => WSAEBADF, + wasi::__WASI_EDESTADDRREQ => WSAEDESTADDRREQ, + wasi::__WASI_EDQUOT => WSAEDQUOT, + wasi::__WASI_EFAULT => WSAEFAULT, + wasi::__WASI_EHOSTUNREACH => WSAEHOSTUNREACH, + wasi::__WASI_EINPROGRESS => WSAEINPROGRESS, + wasi::__WASI_EINTR => WSAEINTR, + wasi::__WASI_EISCONN => WSAEISCONN, + wasi::__WASI_ELOOP => WSAELOOP, + wasi::__WASI_EMFILE => WSAEMFILE, + wasi::__WASI_EMSGSIZE => WSAEMSGSIZE, + wasi::__WASI_ENAMETOOLONG => WSAENAMETOOLONG, + wasi::__WASI_ENETDOWN => WSAENETDOWN, + wasi::__WASI_ENETRESET => WSAENETRESET, + wasi::__WASI_ENETUNREACH => WSAENETUNREACH, + wasi::__WASI_ENOBUFS => WSAENOBUFS, + wasi::__WASI_ENOPROTOOPT => WSAENOPROTOOPT, + wasi::__WASI_ENOTEMPTY => WSAENOTEMPTY, + wasi::__WASI_ENOTSOCK => WSAENOTSOCK, + wasi::__WASI_EPROTONOSUPPORT => WSAEPROTONOSUPPORT, + wasi::__WASI_EPROTOTYPE => WSAEPROTOTYPE, + wasi::__WASI_ESTALE => WSAESTALE, + wasi::__WASI_EIO + | wasi::__WASI_EISDIR + | wasi::__WASI_E2BIG + | wasi::__WASI_EBADMSG + | wasi::__WASI_EBUSY + | wasi::__WASI_ECANCELED + | wasi::__WASI_ECHILD + | wasi::__WASI_EDEADLK + | wasi::__WASI_EDOM + | wasi::__WASI_EFBIG + | wasi::__WASI_EIDRM + | wasi::__WASI_EILSEQ + | wasi::__WASI_EMLINK + | wasi::__WASI_EMULTIHOP + | wasi::__WASI_ENFILE + | wasi::__WASI_ENODEV + | wasi::__WASI_ENOEXEC + | wasi::__WASI_ENOLCK + | wasi::__WASI_ENOLINK + | wasi::__WASI_ENOMEM + | wasi::__WASI_ENOMSG + | wasi::__WASI_ENOSPC + | wasi::__WASI_ENOSYS + | wasi::__WASI_ENOTDIR + | wasi::__WASI_ENOTRECOVERABLE + | wasi::__WASI_ENOTSUP + | wasi::__WASI_ENOTTY + | wasi::__WASI_ENXIO + | wasi::__WASI_EOVERFLOW + | wasi::__WASI_EOWNERDEAD + | wasi::__WASI_EPROTO + | wasi::__WASI_ERANGE + | wasi::__WASI_EROFS + | wasi::__WASI_ESPIPE + | wasi::__WASI_ESRCH + | wasi::__WASI_ETXTBSY + | wasi::__WASI_EXDEV + | wasi::__WASI_ENOTCAPABLE => { + return Err(io::Error::new(io::ErrorKind::Other, error_str(errno))) + } + _ => panic!("unrecognized WASI errno value"), + } as i32; + + Err(io::Error::from_raw_os_error(raw_os_error)) +} + +#[cfg(windows)] +fn error_str(errno: wasi::__wasi_errno_t) -> &'static str { + match errno { + wasi::__WASI_E2BIG => "Argument list too long", + wasi::__WASI_EACCES => "Permission denied", + wasi::__WASI_EADDRINUSE => "Address in use", + wasi::__WASI_EADDRNOTAVAIL => "Address not available", + wasi::__WASI_EAFNOSUPPORT => "Address family not supported by protocol", + wasi::__WASI_EAGAIN => "Resource temporarily unavailable", + wasi::__WASI_EALREADY => "Operation already in progress", + wasi::__WASI_EBADF => "Bad file descriptor", + wasi::__WASI_EBADMSG => "Bad message", + wasi::__WASI_EBUSY => "Resource busy", + wasi::__WASI_ECANCELED => "Operation canceled", + wasi::__WASI_ECHILD => "No child process", + wasi::__WASI_ECONNABORTED => "Connection aborted", + wasi::__WASI_ECONNREFUSED => "Connection refused", + wasi::__WASI_ECONNRESET => "Connection reset by peer", + wasi::__WASI_EDEADLK => "Resource deadlock would occur", + wasi::__WASI_EDESTADDRREQ => "Destination address required", + wasi::__WASI_EDOM => "Domain error", + wasi::__WASI_EDQUOT => "Quota exceeded", + wasi::__WASI_EEXIST => "File exists", + wasi::__WASI_EFAULT => "Bad address", + wasi::__WASI_EFBIG => "File too large", + wasi::__WASI_EHOSTUNREACH => "Host is unreachable", + wasi::__WASI_EIDRM => "Identifier removed", + wasi::__WASI_EILSEQ => "Illegal byte sequence", + wasi::__WASI_EINPROGRESS => "Operation in progress", + wasi::__WASI_EINTR => "Interrupted system call", + wasi::__WASI_EINVAL => "Invalid argument", + wasi::__WASI_EIO => "Remote I/O error", + wasi::__WASI_EISCONN => "Socket is connected", + wasi::__WASI_EISDIR => "Is a directory", + wasi::__WASI_ELOOP => "Symbolic link loop", + wasi::__WASI_EMFILE => "No file descriptors available", + wasi::__WASI_EMLINK => "Too many links", + wasi::__WASI_EMSGSIZE => "Message too large", + wasi::__WASI_EMULTIHOP => "Multihop attempted", + wasi::__WASI_ENAMETOOLONG => "Filename too long", + wasi::__WASI_ENETDOWN => "Network is down", + wasi::__WASI_ENETRESET => "Connection reset by network", + wasi::__WASI_ENETUNREACH => "Network unreachable", + wasi::__WASI_ENFILE => "Too many open files in system", + wasi::__WASI_ENOBUFS => "No buffer space available", + wasi::__WASI_ENODEV => "No such device", + wasi::__WASI_ENOENT => "No such file or directory", + wasi::__WASI_ENOEXEC => "Exec format error", + wasi::__WASI_ENOLCK => "No locks available", + wasi::__WASI_ENOLINK => "Link has been severed", + wasi::__WASI_ENOMEM => "Out of memory", + wasi::__WASI_ENOMSG => "No message of desired type", + wasi::__WASI_ENOPROTOOPT => "Protocol not available", + wasi::__WASI_ENOSPC => "No space left on device", + wasi::__WASI_ENOSYS => "Function not implemented", + wasi::__WASI_ENOTCONN => "Socket not connected", + wasi::__WASI_ENOTDIR => "Not a directory", + wasi::__WASI_ENOTEMPTY => "Directory not empty", + wasi::__WASI_ENOTRECOVERABLE => "State not recoverable", + wasi::__WASI_ENOTSOCK => "Not a socket", + wasi::__WASI_ENOTSUP => "Not supported", + wasi::__WASI_ENOTTY => "Not a tty", + wasi::__WASI_ENXIO => "No such device or address", + wasi::__WASI_EOVERFLOW => "Value too large for data type", + wasi::__WASI_EOWNERDEAD => "Previous owner died", + wasi::__WASI_EPERM => "Operation not permitted", + wasi::__WASI_EPIPE => "Broken pipe", + wasi::__WASI_EPROTO => "Protocol error", + wasi::__WASI_EPROTONOSUPPORT => "Protocol not supported", + wasi::__WASI_EPROTOTYPE => "Protocol wrong type for socket", + wasi::__WASI_ERANGE => "Result not representable", + wasi::__WASI_EROFS => "Read-only file system", + wasi::__WASI_ESPIPE => "Invalid seek", + wasi::__WASI_ESRCH => "No such process", + wasi::__WASI_ESTALE => "Stale file handle", + wasi::__WASI_ETIMEDOUT => "Operation timed out", + wasi::__WASI_ETXTBSY => "Text file busy", + wasi::__WASI_EXDEV => "Cross-device link", + wasi::__WASI_ENOTCAPABLE => "Capabilities insufficient", + _ => panic!("unrecognized WASI errno value"), + } +} diff --git a/wasi-common/src/fs/file.rs b/wasi-common/src/fs/file.rs new file mode 100644 index 0000000000..86090b2c3a --- /dev/null +++ b/wasi-common/src/fs/file.rs @@ -0,0 +1,107 @@ +use crate::fs::{error::wasi_errno_to_io_error, Metadata}; +use crate::{host, hostcalls, wasi, WasiCtx}; +use std::io; + +/// A reference to an open file on the filesystem. +/// +/// This corresponds to [`std::fs::File`]. +/// +/// Note that this `File` has no `open` or `create` methods. To open or create +/// a file, you must first obtain a [`Dir`] containing the file, and then call +/// [`Dir::open_file`] or [`Dir::create_file`]. +/// +/// [`std::fs::File`]: https://doc.rust-lang.org/std/fs/struct.File.html +/// [`Dir`]: struct.Dir.html +/// [`Dir::open_file`]: struct.Dir.html#method.open_file +/// [`Dir::create_file`]: struct.Dir.html#method.create_file +pub struct File<'ctx> { + ctx: &'ctx mut WasiCtx, + fd: wasi::__wasi_fd_t, +} + +impl<'ctx> File<'ctx> { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + /// + /// This corresponds to [`std::fs::File::from_raw_fd`]. + /// + /// [`std::fs::File::from_raw_fd`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.from_raw_fd + pub unsafe fn from_raw_wasi_fd(ctx: &'ctx mut WasiCtx, fd: wasi::__wasi_fd_t) -> Self { + Self { ctx, fd } + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// This corresponds to [`std::fs::File::sync_all`]. + /// + /// [`std::fs::File::sync_all`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all + pub fn sync_all(&self) -> io::Result<()> { + wasi_errno_to_io_error(unsafe { hostcalls::fd_sync(self.ctx, self.fd) }) + } + + /// This function is similar to `sync_all`, except that it may not synchronize + /// file metadata to the filesystem. + /// + /// This corresponds to [`std::fs::File::sync_data`]. + /// + /// [`std::fs::File::sync_data`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_data + pub fn sync_data(&self) -> io::Result<()> { + wasi_errno_to_io_error(unsafe { hostcalls::fd_datasync(self.ctx, self.fd) }) + } + + /// Truncates or extends the underlying file, updating the size of this file + /// to become size. + /// + /// This corresponds to [`std::fs::File::set_len`]. + /// + /// [`std::fs::File::set_len`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.set_len + pub fn set_len(&self, size: u64) -> io::Result<()> { + wasi_errno_to_io_error(unsafe { hostcalls::fd_filestat_set_size(self.ctx, self.fd, size) }) + } + + /// Queries metadata about the underlying file. + /// + /// This corresponds to [`std::fs::File::metadata`]. + /// + /// [`std::fs::File::metadata`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.metadata + pub fn metadata(&self) -> io::Result { + Ok(Metadata {}) + } +} + +impl<'ctx> Drop for File<'ctx> { + fn drop(&mut self) { + // Note that errors are ignored when closing a file descriptor. The + // reason for this is that if an error occurs we don't actually know if + // the file descriptor was closed or not, and if we retried (for + // something like EINTR), we might close another valid file descriptor + // opened after we closed ours. + let _ = unsafe { hostcalls::fd_close(self.ctx, self.fd) }; + } +} + +impl<'ctx> io::Read for File<'ctx> { + /// TODO: Not yet implemented. See the comment in `Dir::open_file`. + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let iov = [host::__wasi_iovec_t { + buf: buf.as_mut_ptr() as *mut u8, + buf_len: buf.len(), + }]; + let mut nread = 0; + + // TODO: See the comment in `Dir::open_file`. + unimplemented!("File::read"); + /* + wasi_errno_to_io_error(unsafe { + hostcalls::fd_read(self.ctx, self.fd, &iov, 1, &mut nread) + })?; + */ + + Ok(nread) + } +} + +// TODO: traits to implement: Write, Seek + +// TODO: functions from FileExt? + +// TODO: impl Debug for File diff --git a/wasi-common/src/fs/file_type.rs b/wasi-common/src/fs/file_type.rs new file mode 100644 index 0000000000..a97f53a8e1 --- /dev/null +++ b/wasi-common/src/fs/file_type.rs @@ -0,0 +1,49 @@ +/// A structure representing a type of file with accessors for each file type. +/// It is returned by `Metadata::file_type` method. +/// +/// This corresponds to [`std::fs::FileType`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::FileType`]: https://doc.rust-lang.org/std/fs/struct.FileType.html +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct FileType {} + +impl FileType { + /// Tests whether this file type represents a directory. + /// + /// This corresponds to [`std::fs::FileType::is_dir`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_dir`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_dir + pub fn is_dir(&self) -> bool { + unimplemented!("FileType::is_dir"); + } + + /// Tests whether this file type represents a regular file. + /// + /// This corresponds to [`std::fs::FileType::is_file`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_file`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_file + pub fn is_file(&self) -> bool { + unimplemented!("FileType::is_file"); + } + + /// Tests whether this file type represents a symbolic link. + /// + /// This corresponds to [`std::fs::FileType::is_symlink`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_symlink`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_symlink + pub fn is_symlink(&self) -> bool { + unimplemented!("FileType::is_symlink"); + } +} + +// TODO: functions from FileTypeExt? + +// TODO: impl Debug for FileType diff --git a/wasi-common/src/fs/metadata.rs b/wasi-common/src/fs/metadata.rs new file mode 100644 index 0000000000..e55afb73a8 --- /dev/null +++ b/wasi-common/src/fs/metadata.rs @@ -0,0 +1,106 @@ +use crate::fs::{FileType, Permissions}; +use std::{io, time::SystemTime}; + +/// Metadata information about a file. +/// +/// This corresponds to [`std::fs::Metadata`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::Metadata`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html +#[derive(Clone)] +pub struct Metadata {} + +impl Metadata { + /// Returns the file type for this metadata. + /// + /// This corresponds to [`std::fs::Metadata::file_type`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::file_type`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.file_type + pub fn file_type(&self) -> FileType { + unimplemented!("Metadata::file_type"); + } + + /// Returns true if this metadata is for a directory. + /// + /// This corresponds to [`std::fs::Metadata::is_dir`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::is_dir`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.is_dir + pub fn is_dir(&self) -> bool { + unimplemented!("Metadata::is_dir"); + } + + /// Returns true if this metadata is for a regular file. + /// + /// This corresponds to [`std::fs::Metadata::is_file`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::is_file`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.is_file + pub fn is_file(&self) -> bool { + unimplemented!("Metadata::is_file"); + } + + /// Returns the size of the file, in bytes, this metadata is for. + /// + /// This corresponds to [`std::fs::Metadata::len`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::len`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.len + pub fn len(&self) -> u64 { + unimplemented!("Metadata::len"); + } + + /// Returns the permissions of the file this metadata is for. + /// + /// This corresponds to [`std::fs::Metadata::permissions`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::permissions`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.permissions + pub fn permissions(&self) -> Permissions { + unimplemented!("Metadata::permissions"); + } + + /// Returns the last modification time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::modified`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::modified`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.modified + pub fn modified(&self) -> io::Result { + unimplemented!("Metadata::modified"); + } + + /// Returns the last access time of this metadata. + /// + /// This corresponds to [`std::fs::Metadata::accessed`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::accessed`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.accessed + pub fn accessed(&self) -> io::Result { + unimplemented!("Metadata::accessed"); + } + + /// Returns the creation time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::created`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::created`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.created + pub fn created(&self) -> io::Result { + unimplemented!("Metadata::created"); + } +} + +// TODO: Functions from MetadataExt? + +// TODO: impl Debug for Metadata diff --git a/wasi-common/src/fs/mod.rs b/wasi-common/src/fs/mod.rs new file mode 100644 index 0000000000..d76f83d700 --- /dev/null +++ b/wasi-common/src/fs/mod.rs @@ -0,0 +1,51 @@ +//! A very experimental module modeled providing a high-level and safe +//! filesystem interface, modeled after `std::fs`, implemented on top of +//! WASI functions. +//! +//! Most functions in this API are not yet implemented! +//! +//! This corresponds to [`std::fs`]. +//! +//! Instead of [`std::fs`'s free functions] which operate on paths, this +//! crate has methods on [`Dir`] which operate on paths which must be +//! relative to and within the directory. +//! +//! Since all functions which expose raw file descriptors are `unsafe`, +//! I/O handles in this API are unforgeable (unsafe code notwithstanding). +//! This combined with WASI's lack of absolute paths provides a natural +//! capability-oriented interface. +//! +//! [`std::fs`]: https://doc.rust-lang.org/std/fs/index.html +//! [`std::fs`'s free functions]: https://doc.rust-lang.org/std/fs/index.html#functions +//! [`DIR`]: struct.Dir.html + +// TODO: When more things are implemented, remove these. +#![allow( + unused_imports, + unreachable_code, + unused_variables, + unused_mut, + unused_unsafe, + dead_code +)] + +mod dir; +mod dir_builder; +mod dir_entry; +mod error; +mod file; +mod file_type; +mod metadata; +mod open_options; +mod permissions; +mod readdir; + +pub use dir::*; +pub use dir_builder::*; +pub use dir_entry::*; +pub use file::*; +pub use file_type::*; +pub use metadata::*; +pub use open_options::*; +pub use permissions::*; +pub use readdir::*; diff --git a/wasi-common/src/fs/open_options.rs b/wasi-common/src/fs/open_options.rs new file mode 100644 index 0000000000..0de1a43841 --- /dev/null +++ b/wasi-common/src/fs/open_options.rs @@ -0,0 +1,99 @@ +/// Options and flags which can be used to configure how a file is opened. +/// +/// This corresponds to [`std::fs::OpenOptions`]. +/// +/// Note that this `OpenOptions` has no `open` method. To open a file with +/// an `OptionOptions`, you must first obtain a [`Dir`] containing the file, and +/// then call [`Dir::open_file_with`]. +/// +/// [`std::fs::OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html +/// [`Dir`]: struct.Dir.html +/// [`Dir::open_file_with`]: struct.Dir.html#method.open_file_with +pub struct OpenOptions { + pub(crate) read: bool, + pub(crate) write: bool, + pub(crate) append: bool, + pub(crate) truncate: bool, + pub(crate) create: bool, + pub(crate) create_new: bool, +} + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// This corresponds to [`std::fs::OpenOptions::new`]. + /// + /// [`std::fs::OpenOptions::new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.new + pub fn new() -> Self { + Self { + read: false, + write: false, + append: false, + truncate: false, + create: false, + create_new: false, + } + } + + /// Sets the option for read access. + /// + /// This corresponds to [`std::fs::OpenOptions::read`]. + /// + /// [`std::fs::OpenOptions::read`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.read + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + /// Sets the option for write access. + /// + /// This corresponds to [`std::fs::OpenOptions::write`]. + /// + /// [`std::fs::OpenOptions::write`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.write + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + + /// Sets the option for the append mode. + /// + /// This corresponds to [`std::fs::OpenOptions::append`]. + /// + /// [`std::fs::OpenOptions::append`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.append + pub fn append(&mut self, append: bool) -> &mut Self { + self.append = append; + self + } + + /// Sets the option for truncating a previous file. + /// + /// This corresponds to [`std::fs::OpenOptions::truncate`]. + /// + /// [`std::fs::OpenOptions::truncate`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.truncate + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + /// Sets the option to create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create`]. + /// + /// [`std::fs::OpenOptions::create`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + /// Sets the option to always create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create_new`]. + /// + /// [`std::fs::OpenOptions::create_new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } +} + +// TODO: Functions from OpenOptionsExt? diff --git a/wasi-common/src/fs/permissions.rs b/wasi-common/src/fs/permissions.rs new file mode 100644 index 0000000000..498a1898ff --- /dev/null +++ b/wasi-common/src/fs/permissions.rs @@ -0,0 +1,37 @@ +/// Representation of the various permissions on a file. +/// +/// This corresponds to [`std::fs::Permissions`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::Permissions`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html +#[derive(Eq, PartialEq, Clone)] +pub struct Permissions {} + +impl Permissions { + /// Returns true if these permissions describe a readonly (unwritable) file. + /// + /// This corresponds to [`std::fs::Permissions::readonly`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Permissions::readonly`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly + pub fn readonly(&self) -> bool { + unimplemented!("Permissions::readonly"); + } + + /// Modifies the readonly flag for this set of permissions. + /// + /// This corresponds to [`std::fs::Permissions::set_readonly`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Permissions::set_readonly`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.set_readonly + pub fn set_readonly(&mut self, readonly: bool) { + unimplemented!("Permissions::set_readonly"); + } +} + +// TODO: functions from PermissionsExt? + +// TODO: impl Debug for Permissions diff --git a/wasi-common/src/fs/readdir.rs b/wasi-common/src/fs/readdir.rs new file mode 100644 index 0000000000..4a5ce7196c --- /dev/null +++ b/wasi-common/src/fs/readdir.rs @@ -0,0 +1,32 @@ +use crate::fs::DirEntry; +use crate::{hostcalls, wasi}; + +/// Iterator over the entries in a directory. +/// +/// This corresponds to [`std::fs::ReadDir`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::ReadDir`]: https://doc.rust-lang.org/std/fs/struct.ReadDir.html +pub struct ReadDir { + fd: wasi::__wasi_fd_t, +} + +impl ReadDir { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + pub unsafe fn from_raw_wasi_fd(fd: wasi::__wasi_fd_t) -> Self { + Self { fd } + } +} + +/// TODO: Not yet implemented. +impl Iterator for ReadDir { + type Item = DirEntry; + + /// TODO: Not yet implemented. + fn next(&mut self) -> Option { + unimplemented!("ReadDir::next"); + } +} + +// TODO: impl Debug for ReadDir diff --git a/wasi-common/src/helpers.rs b/wasi-common/src/helpers.rs new file mode 100644 index 0000000000..9762a7f4f9 --- /dev/null +++ b/wasi-common/src/helpers.rs @@ -0,0 +1,20 @@ +use crate::{Error, Result}; +use std::convert::TryInto; +use std::str; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub(crate) fn systemtime_to_timestamp(st: SystemTime) -> Result { + st.duration_since(UNIX_EPOCH) + .map_err(|_| Error::EINVAL)? // date earlier than UNIX_EPOCH + .as_nanos() + .try_into() + .map_err(Into::into) // u128 doesn't fit into u64 +} + +/// Creates not-owned WASI path from byte slice. +/// +/// NB WASI spec requires bytes to be valid UTF-8. Otherwise, +/// `__WASI_EILSEQ` error is returned. +pub(crate) fn path_from_slice<'a>(s: &'a [u8]) -> Result<&'a str> { + str::from_utf8(s).map_err(|_| Error::EILSEQ) +} diff --git a/wasi-common/src/host.rs b/wasi-common/src/host.rs new file mode 100644 index 0000000000..d2378e1a48 --- /dev/null +++ b/wasi-common/src/host.rs @@ -0,0 +1,109 @@ +//! WASI host types. These are types that contain raw pointers and `usize` +//! values, and so are platform-specific. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::wasi::*; +use std::{io, slice}; +use wig::witx_host_types; + +witx_host_types!("unstable" "wasi_unstable_preview0"); + +pub(crate) unsafe fn ciovec_to_host(ciovec: &__wasi_ciovec_t) -> io::IoSlice { + let slice = slice::from_raw_parts(ciovec.buf as *const u8, ciovec.buf_len); + io::IoSlice::new(slice) +} + +pub(crate) unsafe fn iovec_to_host_mut(iovec: &mut __wasi_iovec_t) -> io::IoSliceMut { + let slice = slice::from_raw_parts_mut(iovec.buf as *mut u8, iovec.buf_len); + io::IoSliceMut::new(slice) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn bindgen_test_layout___wasi_prestat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_t>(), + 16usize, + concat!("Size of: ", stringify!(__wasi_prestat_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_prestat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_t>())).pr_type as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_t), + "::", + stringify!(pr_type) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_t>())).u as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_t), + "::", + stringify!(u) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_prestat_t___wasi_prestat_u___wasi_prestat_u_dir_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_dir>(), + 8usize, + concat!("Size of: ", stringify!(__wasi_prestat_dir)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_dir>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_prestat_dir)) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_prestat_dir>())).pr_name_len as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_dir), + "::", + stringify!(pr_name_len) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_prestat_t___wasi_prestat_u() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_u>(), + 8usize, + concat!("Size of: ", stringify!(__wasi_prestat_u)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_u>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_prestat_u)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_u>())).dir as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_u), + "::", + stringify!(dir) + ) + ); + } +} diff --git a/wasi-common/src/hostcalls/fs.rs b/wasi-common/src/hostcalls/fs.rs new file mode 100644 index 0000000000..fd60c59574 --- /dev/null +++ b/wasi-common/src/hostcalls/fs.rs @@ -0,0 +1,256 @@ +#![allow(non_camel_case_types)] +use crate::ctx::WasiCtx; +use crate::{wasi, wasi32}; + +hostcalls! { + pub unsafe fn fd_close(wasi_ctx: &mut WasiCtx, fd: wasi::__wasi_fd_t,) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_datasync(wasi_ctx: &WasiCtx, fd: wasi::__wasi_fd_t,) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_pread( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nread: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_pwrite( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nwritten: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_read( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nread: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_renumber( + wasi_ctx: &mut WasiCtx, + from: wasi::__wasi_fd_t, + to: wasi::__wasi_fd_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_seek( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filedelta_t, + whence: wasi::__wasi_whence_t, + newoffset: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_tell( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + newoffset: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_fdstat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fdstat_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_fdstat_set_flags( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + fdflags: wasi::__wasi_fdflags_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_fdstat_set_rights( + wasi_ctx: &mut WasiCtx, + fd: wasi::__wasi_fd_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_sync(wasi_ctx: &WasiCtx, fd: wasi::__wasi_fd_t,) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_write( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nwritten: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_advise( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + advice: wasi::__wasi_advice_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_allocate( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_create_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_link( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_flags: wasi::__wasi_lookupflags_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_open( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + oflags: wasi::__wasi_oflags_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, + fs_flags: wasi::__wasi_fdflags_t, + fd_out_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_readdir( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + buf: wasi32::uintptr_t, + buf_len: wasi32::size_t, + cookie: wasi::__wasi_dircookie_t, + buf_used: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_readlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, + buf_used: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_rename( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + filestat_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_filestat_set_times( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_filestat_set_size( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + st_size: wasi::__wasi_filesize_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + filestat_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_filestat_set_times( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_symlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_unlink_file( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn path_remove_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_prestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + prestat_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn fd_prestat_dir_name( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; +} diff --git a/wasi-common/src/hostcalls/misc.rs b/wasi-common/src/hostcalls/misc.rs new file mode 100644 index 0000000000..eb866f44c6 --- /dev/null +++ b/wasi-common/src/hostcalls/misc.rs @@ -0,0 +1,83 @@ +#![allow(non_camel_case_types)] +use crate::ctx::WasiCtx; +use crate::{wasi, wasi32}; +use log::trace; + +use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +pub unsafe fn proc_exit(rval: wasi::__wasi_exitcode_t) { + trace!("proc_exit(rval={:?})", rval); + // TODO: Rather than call std::process::exit here, we should trigger a + // stack unwind similar to a trap. + std::process::exit(rval as i32); +} + +#[wasi_common_cbindgen] +pub unsafe fn proc_raise( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sig: wasi::__wasi_signal_t, +) -> wasi::__wasi_errno_t { + unimplemented!("proc_raise") +} + +hostcalls! { + pub unsafe fn args_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argv_ptr: wasi32::uintptr_t, + argv_buf: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn args_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argc_ptr: wasi32::uintptr_t, + argv_buf_size_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn environ_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_ptr: wasi32::uintptr_t, + environ_buf: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn environ_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_count_ptr: wasi32::uintptr_t, + environ_size_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn random_get( + memory: &mut [u8], + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn clock_res_get( + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + resolution_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn clock_time_get( + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + precision: wasi::__wasi_timestamp_t, + time_ptr: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn poll_oneoff( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + input: wasi32::uintptr_t, + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + nevents: wasi32::uintptr_t, + ) -> wasi::__wasi_errno_t; + + pub unsafe fn sched_yield() -> wasi::__wasi_errno_t; +} diff --git a/wasi-common/src/hostcalls/mod.rs b/wasi-common/src/hostcalls/mod.rs new file mode 100644 index 0000000000..5832ce7dba --- /dev/null +++ b/wasi-common/src/hostcalls/mod.rs @@ -0,0 +1,7 @@ +mod fs; +mod misc; +mod sock; + +pub use self::fs::*; +pub use self::misc::*; +pub use self::sock::*; diff --git a/wasi-common/src/hostcalls/sock.rs b/wasi-common/src/hostcalls/sock.rs new file mode 100644 index 0000000000..f3021b7991 --- /dev/null +++ b/wasi-common/src/hostcalls/sock.rs @@ -0,0 +1,43 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +#![allow(unused)] +use crate::ctx::WasiCtx; +use crate::{wasi, wasi32}; +use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +pub unsafe fn sock_recv( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + sock: wasi::__wasi_fd_t, + ri_data: wasi32::uintptr_t, + ri_data_len: wasi32::size_t, + ri_flags: wasi::__wasi_riflags_t, + ro_datalen: wasi32::uintptr_t, + ro_flags: wasi32::uintptr_t, +) -> wasi::__wasi_errno_t { + unimplemented!("sock_recv") +} + +#[wasi_common_cbindgen] +pub unsafe fn sock_send( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + sock: wasi::__wasi_fd_t, + si_data: wasi32::uintptr_t, + si_data_len: wasi32::size_t, + si_flags: wasi::__wasi_siflags_t, + so_datalen: wasi32::uintptr_t, +) -> wasi::__wasi_errno_t { + unimplemented!("sock_send") +} + +#[wasi_common_cbindgen] +pub unsafe fn sock_shutdown( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + sock: wasi::__wasi_fd_t, + how: wasi::__wasi_sdflags_t, +) -> wasi::__wasi_errno_t { + unimplemented!("sock_shutdown") +} diff --git a/wasi-common/src/hostcalls_impl/fs.rs b/wasi-common/src/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..ae2485be21 --- /dev/null +++ b/wasi-common/src/hostcalls_impl/fs.rs @@ -0,0 +1,1091 @@ +#![allow(non_camel_case_types)] +use super::fs_helpers::path_get; +use crate::ctx::WasiCtx; +use crate::fdentry::{Descriptor, FdEntry}; +use crate::helpers::*; +use crate::memory::*; +use crate::sys::fdentry_impl::determine_type_rights; +use crate::sys::hostcalls_impl::fs_helpers::path_open_rights; +use crate::sys::{host_impl, hostcalls_impl}; +use crate::{helpers, host, wasi, wasi32, Error, Result}; +use filetime::{set_file_handle_times, FileTime}; +use log::trace; +use std::convert::TryInto; +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::mem; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub(crate) unsafe fn fd_close(wasi_ctx: &mut WasiCtx, fd: wasi::__wasi_fd_t) -> Result<()> { + trace!("fd_close(fd={:?})", fd); + + if let Ok(fe) = wasi_ctx.get_fd_entry(fd) { + // can't close preopened files + if fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + } + + wasi_ctx.remove_fd_entry(fd)?; + Ok(()) +} + +pub(crate) unsafe fn fd_datasync(wasi_ctx: &WasiCtx, fd: wasi::__wasi_fd_t) -> Result<()> { + trace!("fd_datasync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_DATASYNC, 0)? + .as_file()?; + + fd.sync_data().map_err(Into::into) +} + +pub(crate) unsafe fn fd_pread( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pread(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nread + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_READ, 0)? + .as_file()?; + + let iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = vec![0; buf_size]; + let host_nread = hostcalls_impl::fd_pread(fd, &mut buf, offset)?; + let mut buf_offset = 0; + let mut left = host_nread; + for iov in &iovs { + if left == 0 { + break; + } + let vec_len = std::cmp::min(iov.buf_len, left); + std::slice::from_raw_parts_mut(iov.buf as *mut u8, vec_len) + .copy_from_slice(&buf[buf_offset..buf_offset + vec_len]); + buf_offset += vec_len; + left -= vec_len; + } + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_pwrite( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pwrite(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nwritten + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_WRITE, 0)? + .as_file()?; + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = Vec::with_capacity(buf_size); + for iov in &iovs { + buf.extend_from_slice(std::slice::from_raw_parts( + iov.buf as *const u8, + iov.buf_len, + )); + } + let host_nwritten = hostcalls_impl::fd_pwrite(fd, &buf, offset)?; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_read( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_read(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + nread + ); + + let mut iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + let mut iovs: Vec = iovs + .iter_mut() + .map(|vec| host::iovec_to_host_mut(vec)) + .collect(); + + let maybe_host_nread = match wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHT_FD_READ, 0)? + { + Descriptor::OsFile(file) => file.read_vectored(&mut iovs), + Descriptor::Stdin => io::stdin().lock().read_vectored(&mut iovs), + _ => return Err(Error::EBADF), + }; + + let host_nread = maybe_host_nread?; + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_renumber( + wasi_ctx: &mut WasiCtx, + from: wasi::__wasi_fd_t, + to: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_renumber(from={:?}, to={:?})", from, to); + + if !wasi_ctx.contains_fd_entry(from) || !wasi_ctx.contains_fd_entry(to) { + return Err(Error::EBADF); + } + + let from_fe = wasi_ctx.get_fd_entry(from)?; + let to_fe = wasi_ctx.get_fd_entry(to)?; + + // Don't allow renumbering over a pre-opened resource. + // TODO: Eventually, we do want to permit this, once libpreopen in + // userspace is capable of removing entries from its tables as well. + if from_fe.preopen_path.is_some() || to_fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + + // check if stdio fds + // TODO should we renumber stdio fds? + if !from_fe.as_descriptor(0, 0)?.is_file() || !to_fe.as_descriptor(0, 0)?.is_file() { + return Err(Error::EBADF); + } + + let fe_from_dup = from_fe + .as_descriptor(0, 0)? + .as_file() + .and_then(|file| FdEntry::duplicate(file))?; + + wasi_ctx.insert_fd_entry_at(to, fe_from_dup); + wasi_ctx.remove_fd_entry(from)?; + + Ok(()) +} + +pub(crate) unsafe fn fd_seek( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filedelta_t, + whence: wasi::__wasi_whence_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_seek(fd={:?}, offset={:?}, whence={}, newoffset={:#x?})", + fd, + offset, + wasi::whence_to_str(whence), + newoffset + ); + + let rights = if offset == 0 && whence == wasi::__WASI_WHENCE_CUR { + wasi::__WASI_RIGHT_FD_TELL + } else { + wasi::__WASI_RIGHT_FD_SEEK | wasi::__WASI_RIGHT_FD_TELL + }; + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(rights, 0)? + .as_file_mut()?; + + let pos = match whence { + wasi::__WASI_WHENCE_CUR => SeekFrom::Current(offset), + wasi::__WASI_WHENCE_END => SeekFrom::End(offset), + wasi::__WASI_WHENCE_SET => SeekFrom::Start(offset as u64), + _ => return Err(Error::EINVAL), + }; + let host_newoffset = fd.seek(pos)?; + + trace!(" | *newoffset={:?}", host_newoffset); + + enc_filesize_byref(memory, newoffset, host_newoffset) +} + +pub(crate) unsafe fn fd_tell( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!("fd_tell(fd={:?}, newoffset={:#x?})", fd, newoffset); + + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHT_FD_TELL, 0)? + .as_file_mut()?; + + let host_offset = fd.seek(SeekFrom::Current(0))?; + + trace!(" | *newoffset={:?}", host_offset); + + enc_filesize_byref(memory, newoffset, host_offset) +} + +pub(crate) unsafe fn fd_fdstat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fdstat_ptr: wasi32::uintptr_t, // *mut wasi::__wasi_fdstat_t +) -> Result<()> { + trace!("fd_fdstat_get(fd={:?}, fdstat_ptr={:#x?})", fd, fdstat_ptr); + + let mut fdstat = dec_fdstat_byref(memory, fdstat_ptr)?; + let wasi_fd = wasi_ctx.get_fd_entry(fd)?.as_descriptor(0, 0)?.as_file()?; + + let fs_flags = hostcalls_impl::fd_fdstat_get(wasi_fd)?; + + let fe = wasi_ctx.get_fd_entry(fd)?; + fdstat.fs_filetype = fe.file_type; + fdstat.fs_rights_base = fe.rights_base; + fdstat.fs_rights_inheriting = fe.rights_inheriting; + fdstat.fs_flags = fs_flags; + + trace!(" | *buf={:?}", fdstat); + + enc_fdstat_byref(memory, fdstat_ptr, fdstat) +} + +pub(crate) unsafe fn fd_fdstat_set_flags( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result<()> { + trace!("fd_fdstat_set_flags(fd={:?}, fdflags={:#x?})", fd, fdflags); + + let fd = wasi_ctx.get_fd_entry(fd)?.as_descriptor(0, 0)?.as_file()?; + + hostcalls_impl::fd_fdstat_set_flags(fd, fdflags) +} + +pub(crate) unsafe fn fd_fdstat_set_rights( + wasi_ctx: &mut WasiCtx, + fd: wasi::__wasi_fd_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, +) -> Result<()> { + trace!( + "fd_fdstat_set_rights(fd={:?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?})", + fd, + fs_rights_base, + fs_rights_inheriting + ); + + let fe = wasi_ctx.get_fd_entry_mut(fd)?; + if fe.rights_base & fs_rights_base != fs_rights_base + || fe.rights_inheriting & fs_rights_inheriting != fs_rights_inheriting + { + return Err(Error::ENOTCAPABLE); + } + fe.rights_base = fs_rights_base; + fe.rights_inheriting = fs_rights_inheriting; + + Ok(()) +} + +pub(crate) unsafe fn fd_sync(wasi_ctx: &WasiCtx, fd: wasi::__wasi_fd_t) -> Result<()> { + trace!("fd_sync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_SYNC, 0)? + .as_file()?; + fd.sync_all().map_err(Into::into) +} + +pub(crate) unsafe fn fd_write( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_write(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + nwritten + ); + + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + let iovs: Vec = iovs.iter().map(|vec| host::ciovec_to_host(vec)).collect(); + + // perform unbuffered writes + let host_nwritten = match wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHT_FD_WRITE, 0)? + { + Descriptor::OsFile(file) => file.write_vectored(&iovs)?, + Descriptor::Stdin => return Err(Error::EBADF), + Descriptor::Stdout => { + // lock for the duration of the scope + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + let nwritten = stdout.write_vectored(&iovs)?; + stdout.flush()?; + nwritten + } + Descriptor::Stderr => io::stderr().lock().write_vectored(&iovs)?, + }; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_advise( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + advice: wasi::__wasi_advice_t, +) -> Result<()> { + trace!( + "fd_advise(fd={:?}, offset={}, len={}, advice={:?})", + fd, + offset, + len, + advice + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_ADVISE, 0)? + .as_file()?; + + hostcalls_impl::fd_advise(fd, advice, offset, len) +} + +pub(crate) unsafe fn fd_allocate( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_allocate(fd={:?}, offset={}, len={})", fd, offset, len); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_ALLOCATE, 0)? + .as_file()?; + + let metadata = fd.metadata()?; + + let current_size = metadata.len(); + let wanted_size = offset.checked_add(len).ok_or(Error::E2BIG)?; + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if wanted_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + + if wanted_size > current_size { + fd.set_len(wanted_size).map_err(Into::into) + } else { + Ok(()) + } +} + +pub(crate) unsafe fn path_create_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_create_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len, + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let rights = wasi::__WASI_RIGHT_PATH_OPEN | wasi::__WASI_RIGHT_PATH_CREATE_DIRECTORY; + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, rights, 0, 0, path, false)?; + + hostcalls_impl::path_create_directory(resolved) +} + +pub(crate) unsafe fn path_link( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_flags: wasi::__wasi_lookupflags_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_link(old_dirfd={:?}, old_flags={:?}, old_path_ptr={:#x?}, old_path_len={}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_dirfd, + old_flags, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHT_PATH_LINK_SOURCE, + 0, + 0, + old_path, + false, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHT_PATH_LINK_TARGET, + 0, + 0, + new_path, + false, + )?; + + hostcalls_impl::path_link(resolved_old, resolved_new) +} + +pub(crate) unsafe fn path_open( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + oflags: wasi::__wasi_oflags_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, + fs_flags: wasi::__wasi_fdflags_t, + fd_out_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_open(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={:?}, oflags={:#x?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?}, fs_flags={:#x?}, fd_out_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + oflags, + fs_rights_base, + fs_rights_inheriting, + fs_flags, + fd_out_ptr + ); + + // pre-encode fd_out_ptr to -1 in case of error in opening a path + enc_fd_byref(memory, fd_out_ptr, wasi::__wasi_fd_t::max_value())?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let (needed_base, needed_inheriting) = + path_open_rights(fs_rights_base, fs_rights_inheriting, oflags, fs_flags); + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + needed_base, + needed_inheriting, + dirflags, + path, + oflags & wasi::__WASI_O_CREAT != 0, + )?; + + // which open mode do we need? + let read = fs_rights_base & (wasi::__WASI_RIGHT_FD_READ | wasi::__WASI_RIGHT_FD_READDIR) != 0; + let write = fs_rights_base + & (wasi::__WASI_RIGHT_FD_DATASYNC + | wasi::__WASI_RIGHT_FD_WRITE + | wasi::__WASI_RIGHT_FD_ALLOCATE + | wasi::__WASI_RIGHT_FD_FILESTAT_SET_SIZE) + != 0; + + let fd = hostcalls_impl::path_open(resolved, read, write, oflags, fs_flags)?; + + // Determine the type of the new file descriptor and which rights contradict with this type + let (_ty, max_base, max_inheriting) = determine_type_rights(&fd)?; + let mut fe = FdEntry::from(fd)?; + fe.rights_base &= max_base; + fe.rights_inheriting &= max_inheriting; + let guest_fd = wasi_ctx.insert_fd_entry(fe)?; + + trace!(" | *fd={:?}", guest_fd); + + enc_fd_byref(memory, fd_out_ptr, guest_fd) +} + +pub(crate) unsafe fn fd_readdir( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + buf: wasi32::uintptr_t, + buf_len: wasi32::size_t, + cookie: wasi::__wasi_dircookie_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_readdir(fd={:?}, buf={:#x?}, buf_len={}, cookie={:#x?}, buf_used={:#x?})", + fd, + buf, + buf_len, + cookie, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let file = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHT_FD_READDIR, 0)? + .as_file_mut()?; + let host_buf = dec_slice_of_mut_u8(memory, buf, buf_len)?; + + trace!(" | (buf,buf_len)={:?}", host_buf); + + let host_bufused = hostcalls_impl::fd_readdir(file, host_buf, cookie)?; + + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} + +pub(crate) unsafe fn path_readlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_readlink(dirfd={:?}, path_ptr={:#x?}, path_len={:?}, buf_ptr={:#x?}, buf_len={}, buf_used={:#x?})", + dirfd, + path_ptr, + path_len, + buf_ptr, + buf_len, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", &path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHT_PATH_READLINK, 0, 0, &path, false)?; + + let mut buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + let host_bufused = hostcalls_impl::path_readlink(resolved, &mut buf)?; + + trace!(" | (buf_ptr,*buf_used)={:?}", buf); + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} + +pub(crate) unsafe fn path_rename( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_rename(old_dirfd={:?}, old_path_ptr={:#x?}, old_path_len={:?}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={:?})", + old_dirfd, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHT_PATH_RENAME_SOURCE, + 0, + 0, + old_path, + true, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHT_PATH_RENAME_TARGET, + 0, + 0, + new_path, + true, + )?; + + log::debug!("path_rename resolved_old={:?}", resolved_old); + log::debug!("path_rename resolved_new={:?}", resolved_new); + + hostcalls_impl::path_rename(resolved_old, resolved_new) +} + +pub(crate) unsafe fn fd_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_filestat_get(fd={:?}, filestat_ptr={:#x?})", + fd, + filestat_ptr + ); + + let fd = wasi_ctx.get_fd_entry(fd)?.as_descriptor(0, 0)?.as_file()?; + + let host_filestat = hostcalls_impl::fd_filestat_get_impl(fd)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn fd_filestat_set_times( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "fd_filestat_set_times(fd={:?}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + fd, + st_atim, + st_mtim, + fst_flags + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_FILESTAT_SET_TIMES, 0)? + .as_file()?; + + fd_filestat_set_times_impl(fd, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn fd_filestat_set_times_impl( + fd: &File, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + let set_atim = fst_flags & wasi::__WASI_FILESTAT_SET_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FILESTAT_SET_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FILESTAT_SET_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FILESTAT_SET_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + let atim = if set_atim { + let time = UNIX_EPOCH + Duration::from_nanos(st_atim); + Some(FileTime::from_system_time(time)) + } else if set_atim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + + let mtim = if set_mtim { + let time = UNIX_EPOCH + Duration::from_nanos(st_mtim); + Some(FileTime::from_system_time(time)) + } else if set_mtim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + set_file_handle_times(fd, atim, mtim).map_err(Into::into) +} + +pub(crate) unsafe fn fd_filestat_set_size( + wasi_ctx: &WasiCtx, + fd: wasi::__wasi_fd_t, + st_size: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_filestat_set_size(fd={:?}, st_size={})", fd, st_size); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHT_FD_FILESTAT_SET_SIZE, 0)? + .as_file()?; + + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if st_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + fd.set_len(st_size).map_err(Into::into) +} + +pub(crate) unsafe fn path_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_filestat_get(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, filestat_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + filestat_ptr + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHT_PATH_FILESTAT_GET, + 0, + dirflags, + path, + false, + )?; + let host_filestat = hostcalls_impl::path_filestat_get(resolved, dirflags)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn path_filestat_set_times( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "path_filestat_set_times(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + st_atim, st_mtim, + fst_flags + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHT_PATH_FILESTAT_SET_TIMES, + 0, + dirflags, + path, + false, + )?; + + hostcalls_impl::path_filestat_set_times(resolved, dirflags, st_atim, st_mtim, fst_flags) +} + +pub(crate) unsafe fn path_symlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_symlink(old_path_ptr={:#x?}, old_path_len={}, dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_path_ptr, + old_path_len, + dirfd, + new_path_ptr, + new_path_len + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved_new = path_get(fe, wasi::__WASI_RIGHT_PATH_SYMLINK, 0, 0, new_path, true)?; + + hostcalls_impl::path_symlink(old_path, resolved_new) +} + +pub(crate) unsafe fn path_unlink_file( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_unlink_file(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHT_PATH_UNLINK_FILE, 0, 0, path, false)?; + + hostcalls_impl::path_unlink_file(resolved) +} + +pub(crate) unsafe fn path_remove_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_remove_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHT_PATH_REMOVE_DIRECTORY, + 0, + 0, + path, + true, + )?; + + log::debug!("path_remove_directory resolved={:?}", resolved); + + hostcalls_impl::path_remove_directory(resolved) +} + +pub(crate) unsafe fn fd_prestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + prestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_prestat_get(fd={:?}, prestat_ptr={:#x?})", + fd, + prestat_ptr + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + enc_prestat_byref( + memory, + prestat_ptr, + host::__wasi_prestat_t { + pr_type: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u { + dir: host::__wasi_prestat_dir { + pr_name_len: path.len(), + }, + }, + }, + ) +} + +pub(crate) unsafe fn fd_prestat_dir_name( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "fd_prestat_dir_name(fd={:?}, path_ptr={:#x?}, path_len={})", + fd, + path_ptr, + path_len + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + if path.len() > dec_usize(path_len) { + return Err(Error::ENAMETOOLONG); + } + + trace!(" | (path_ptr,path_len)='{}'", path); + + enc_slice_of_u8(memory, path.as_bytes(), path_ptr) +} + +#[allow(dead_code)] // trouble with sockets +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub(crate) enum FileType { + Unknown = wasi::__WASI_FILETYPE_UNKNOWN, + BlockDevice = wasi::__WASI_FILETYPE_BLOCK_DEVICE, + CharacterDevice = wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + Directory = wasi::__WASI_FILETYPE_DIRECTORY, + RegularFile = wasi::__WASI_FILETYPE_REGULAR_FILE, + SocketDgram = wasi::__WASI_FILETYPE_SOCKET_DGRAM, + SocketStream = wasi::__WASI_FILETYPE_SOCKET_STREAM, + Symlink = wasi::__WASI_FILETYPE_SYMBOLIC_LINK, +} + +impl FileType { + pub(crate) fn to_wasi(&self) -> wasi::__wasi_filetype_t { + *self as wasi::__wasi_filetype_t + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Dirent { + pub name: String, + pub ftype: FileType, + pub ino: u64, + pub cookie: wasi::__wasi_dircookie_t, +} + +impl Dirent { + #![allow(unused)] // temporarily, until BSD catches up with this change + /// Serialize the directory entry to the format define by `__wasi_fd_readdir`, + /// so that the serialized entries can be concatenated by the implementation. + pub fn to_wasi_raw(&self) -> Result> { + use std::slice; + + let name = self.name.as_bytes(); + let namlen = name.len(); + let dirent_size = mem::size_of::(); + let offset = dirent_size.checked_add(namlen).ok_or(Error::EOVERFLOW)?; + + let mut raw = Vec::::with_capacity(offset); + raw.resize(offset, 0); + + let sys_dirent = raw.as_mut_ptr() as *mut wasi::__wasi_dirent_t; + unsafe { + *sys_dirent = wasi::__wasi_dirent_t { + d_namlen: namlen.try_into()?, + d_ino: self.ino, + d_next: self.cookie, + d_type: self.ftype.to_wasi(), + }; + } + + let sys_name = unsafe { sys_dirent.offset(1) as *mut u8 }; + let sys_name = unsafe { slice::from_raw_parts_mut(sys_name, namlen) }; + sys_name.copy_from_slice(&name); + + Ok(raw) + } +} diff --git a/wasi-common/src/hostcalls_impl/fs_helpers.rs b/wasi-common/src/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..7aaca0930b --- /dev/null +++ b/wasi-common/src/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,213 @@ +#![allow(non_camel_case_types)] +use crate::sys::host_impl; +use crate::sys::hostcalls_impl::fs_helpers::*; +use crate::{fdentry::FdEntry, wasi, Error, Result}; +use std::fs::File; +use std::path::{Component, Path}; + +#[derive(Debug)] +pub(crate) struct PathGet { + dirfd: File, + path: String, +} + +impl PathGet { + pub(crate) fn dirfd(&self) -> &File { + &self.dirfd + } + + pub(crate) fn path(&self) -> &str { + &self.path + } +} + +/// Normalizes a path to ensure that the target path is located under the directory provided. +/// +/// This is a workaround for not having Capsicum support in the OS. +pub(crate) fn path_get( + fe: &FdEntry, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + dirflags: wasi::__wasi_lookupflags_t, + path: &str, + needs_final_component: bool, +) -> Result { + const MAX_SYMLINK_EXPANSIONS: usize = 128; + + if path.contains('\0') { + // if contains NUL, return EILSEQ + return Err(Error::EILSEQ); + } + + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + // if `dirfd` doesn't refer to a directory, return `ENOTDIR`. + return Err(Error::ENOTDIR); + } + + let dirfd = fe + .as_descriptor(rights_base, rights_inheriting)? + .as_file()? + .try_clone()?; + + // Stack of directory file descriptors. Index 0 always corresponds with the directory provided + // to this function. Entering a directory causes a file descriptor to be pushed, while handling + // ".." entries causes an entry to be popped. Index 0 cannot be popped, as this would imply + // escaping the base directory. + let mut dir_stack = vec![dirfd]; + + // Stack of paths left to process. This is initially the `path` argument to this function, but + // any symlinks we encounter are processed by pushing them on the stack. + let mut path_stack = vec![path.to_owned()]; + + // Track the number of symlinks we've expanded, so we can return `ELOOP` after too many. + let mut symlink_expansions = 0; + + // TODO: rewrite this using a custom posix path type, with a component iterator that respects + // trailing slashes. This version does way too much allocation, and is way too fiddly. + loop { + match path_stack.pop() { + Some(cur_path) => { + log::debug!("path_get cur_path = {:?}", cur_path); + + let ends_with_slash = cur_path.ends_with('/'); + let mut components = Path::new(&cur_path).components(); + let head = match components.next() { + None => return Err(Error::ENOENT), + Some(p) => p, + }; + let tail = components.as_path(); + + if tail.components().next().is_some() { + let mut tail = host_impl::path_from_host(tail.as_os_str())?; + if ends_with_slash { + tail.push('/'); + } + path_stack.push(tail); + } + + log::debug!("path_get path_stack = {:?}", path_stack); + + match head { + Component::Prefix(_) | Component::RootDir => { + // path is absolute! + return Err(Error::ENOTCAPABLE); + } + Component::CurDir => { + // "." so skip + } + Component::ParentDir => { + // ".." so pop a dir + let _ = dir_stack.pop().ok_or(Error::ENOTCAPABLE)?; + + // we're not allowed to pop past the original directory + if dir_stack.is_empty() { + return Err(Error::ENOTCAPABLE); + } + } + Component::Normal(head) => { + let mut head = host_impl::path_from_host(head)?; + if ends_with_slash { + // preserve trailing slash + head.push('/'); + } + + if !path_stack.is_empty() || (ends_with_slash && !needs_final_component) { + match openat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(new_dir) => { + dir_stack.push(new_dir); + } + Err(e) => { + match e.as_wasi_errno() { + wasi::__WASI_ELOOP + | wasi::__WASI_EMLINK + | wasi::__WASI_ENOTDIR => + // Check to see if it was a symlink. Linux indicates + // this with ENOTDIR because of the O_DIRECTORY flag. + { + // attempt symlink expansion + let mut link_path = readlinkat( + dir_stack.last().ok_or(Error::ENOTCAPABLE)?, + &head, + )?; + + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + } + _ => { + return Err(e); + } + } + } + } + + continue; + } else if ends_with_slash + || (dirflags & wasi::__WASI_LOOKUP_SYMLINK_FOLLOW) != 0 + { + // if there's a trailing slash, or if `LOOKUP_SYMLINK_FOLLOW` is set, attempt + // symlink expansion + match readlinkat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(mut link_path) => { + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + continue; + } + Err(e) => { + if e.as_wasi_errno() != wasi::__WASI_EINVAL + && e.as_wasi_errno() != wasi::__WASI_ENOENT + // this handles the cases when trying to link to + // a destination that already exists, and the target + // path contains a slash + && e.as_wasi_errno() != wasi::__WASI_ENOTDIR + { + return Err(e); + } + } + } + } + + // not a symlink, so we're done; + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: head, + }); + } + } + } + None => { + // no further components to process. means we've hit a case like "." or "a/..", or if the + // input path has trailing slashes and `needs_final_component` is not set + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: String::from("."), + }); + } + } + } +} diff --git a/wasi-common/src/hostcalls_impl/misc.rs b/wasi-common/src/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..5ad1f73b0e --- /dev/null +++ b/wasi-common/src/hostcalls_impl/misc.rs @@ -0,0 +1,317 @@ +#![allow(non_camel_case_types)] +use crate::ctx::WasiCtx; +use crate::fdentry::Descriptor; +use crate::memory::*; +use crate::sys::hostcalls_impl; +use crate::{wasi, wasi32, Error, Result}; +use log::trace; +use std::convert::TryFrom; + +pub(crate) fn args_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argv_ptr: wasi32::uintptr_t, + argv_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_get(argv_ptr={:#x?}, argv_buf={:#x?})", + argv_ptr, + argv_buf, + ); + + let mut argv_buf_offset = 0; + let mut argv = vec![]; + + for arg in &wasi_ctx.args { + let arg_bytes = arg.as_bytes_with_nul(); + let arg_ptr = argv_buf + argv_buf_offset; + + enc_slice_of_u8(memory, arg_bytes, arg_ptr)?; + + argv.push(arg_ptr); + + let len = wasi32::uintptr_t::try_from(arg_bytes.len())?; + argv_buf_offset = argv_buf_offset.checked_add(len).ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, argv.as_slice(), argv_ptr) +} + +pub(crate) fn args_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argc_ptr: wasi32::uintptr_t, + argv_buf_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_sizes_get(argc_ptr={:#x?}, argv_buf_size_ptr={:#x?})", + argc_ptr, + argv_buf_size_ptr, + ); + + let argc = wasi_ctx.args.len(); + let argv_size = wasi_ctx + .args + .iter() + .map(|arg| arg.as_bytes_with_nul().len()) + .sum(); + + trace!(" | *argc_ptr={:?}", argc); + + enc_usize_byref(memory, argc_ptr, argc)?; + + trace!(" | *argv_buf_size_ptr={:?}", argv_size); + + enc_usize_byref(memory, argv_buf_size_ptr, argv_size) +} + +pub(crate) fn environ_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_ptr: wasi32::uintptr_t, + environ_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_get(environ_ptr={:#x?}, environ_buf={:#x?})", + environ_ptr, + environ_buf, + ); + + let mut environ_buf_offset = 0; + let mut environ = vec![]; + + for pair in &wasi_ctx.env { + let env_bytes = pair.as_bytes_with_nul(); + let env_ptr = environ_buf + environ_buf_offset; + + enc_slice_of_u8(memory, env_bytes, env_ptr)?; + + environ.push(env_ptr); + + let len = wasi32::uintptr_t::try_from(env_bytes.len())?; + environ_buf_offset = environ_buf_offset + .checked_add(len) + .ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, environ.as_slice(), environ_ptr) +} + +pub(crate) fn environ_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_count_ptr: wasi32::uintptr_t, + environ_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_sizes_get(environ_count_ptr={:#x?}, environ_size_ptr={:#x?})", + environ_count_ptr, + environ_size_ptr, + ); + + let environ_count = wasi_ctx.env.len(); + let environ_size = wasi_ctx + .env + .iter() + .try_fold(0, |acc: u32, pair| { + acc.checked_add(pair.as_bytes_with_nul().len() as u32) + }) + .ok_or(Error::EOVERFLOW)?; + + trace!(" | *environ_count_ptr={:?}", environ_count); + + enc_usize_byref(memory, environ_count_ptr, environ_count)?; + + trace!(" | *environ_size_ptr={:?}", environ_size); + + enc_usize_byref(memory, environ_size_ptr, environ_size as usize) +} + +pub(crate) fn random_get( + memory: &mut [u8], + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, +) -> Result<()> { + use rand::{thread_rng, RngCore}; + + trace!("random_get(buf_ptr={:#x?}, buf_len={:?})", buf_ptr, buf_len); + + let buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + thread_rng().fill_bytes(buf); + + Ok(()) +} + +pub(crate) fn clock_res_get( + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + resolution_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_res_get(clock_id={:?}, resolution_ptr={:#x?})", + clock_id, + resolution_ptr, + ); + + let resolution = hostcalls_impl::clock_res_get(clock_id)?; + + trace!(" | *resolution_ptr={:?}", resolution); + + enc_timestamp_byref(memory, resolution_ptr, resolution) +} + +pub(crate) fn clock_time_get( + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + precision: wasi::__wasi_timestamp_t, + time_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_time_get(clock_id={:?}, precision={:?}, time_ptr={:#x?})", + clock_id, + precision, + time_ptr, + ); + + let time = hostcalls_impl::clock_time_get(clock_id)?; + + trace!(" | *time_ptr={:?}", time); + + enc_timestamp_byref(memory, time_ptr, time) +} + +pub(crate) fn sched_yield() -> Result<()> { + trace!("sched_yield()"); + + std::thread::yield_now(); + + Ok(()) +} + +pub(crate) fn poll_oneoff( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + input: wasi32::uintptr_t, + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + nevents: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "poll_oneoff(input={:#x?}, output={:#x?}, nsubscriptions={}, nevents={:#x?})", + input, + output, + nsubscriptions, + nevents, + ); + + if u64::from(nsubscriptions) > wasi::__wasi_filesize_t::max_value() { + return Err(Error::EINVAL); + } + + enc_int_byref(memory, nevents, 0)?; + + let subscriptions = dec_subscriptions(memory, input, nsubscriptions)?; + let mut events = Vec::new(); + + let mut timeout: Option = None; + let mut fd_events = Vec::new(); + for subscription in subscriptions { + match subscription.r#type { + wasi::__WASI_EVENTTYPE_CLOCK => { + let clock = unsafe { subscription.u.clock }; + let delay = wasi_clock_to_relative_ns_delay(clock)?; + + log::debug!("poll_oneoff event.u.clock = {:?}", clock); + log::debug!("poll_oneoff delay = {:?}ns", delay); + + let current = ClockEventData { + delay, + userdata: subscription.userdata, + }; + let timeout = timeout.get_or_insert(current); + if current.delay < timeout.delay { + *timeout = current; + } + } + r#type + if r#type == wasi::__WASI_EVENTTYPE_FD_READ + || r#type == wasi::__WASI_EVENTTYPE_FD_WRITE => + { + let wasi_fd = unsafe { subscription.u.fd_readwrite.file_descriptor }; + let rights = if r#type == wasi::__WASI_EVENTTYPE_FD_READ { + wasi::__WASI_RIGHT_FD_READ + } else { + wasi::__WASI_RIGHT_FD_WRITE + }; + + match unsafe { + wasi_ctx + .get_fd_entry(wasi_fd) + .and_then(|fe| fe.as_descriptor(rights, 0)) + } { + Ok(descriptor) => fd_events.push(FdEventData { + descriptor, + r#type: subscription.r#type, + userdata: subscription.userdata, + }), + Err(err) => { + let event = wasi::__wasi_event_t { + userdata: subscription.userdata, + r#type, + error: err.as_wasi_errno(), + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }, + }; + events.push(event); + } + }; + } + _ => unreachable!(), + } + } + + log::debug!("poll_oneoff timeout = {:?}", timeout); + log::debug!("poll_oneoff fd_events = {:?}", fd_events); + + hostcalls_impl::poll_oneoff(timeout, fd_events, &mut events)?; + + let events_count = u32::try_from(events.len()).map_err(|_| Error::EOVERFLOW)?; + + enc_events(memory, output, nsubscriptions, events)?; + + trace!(" | *nevents={:?}", events_count); + + enc_int_byref(memory, nevents, events_count) +} + +fn wasi_clock_to_relative_ns_delay(wasi_clock: wasi::__wasi_subscription_clock_t) -> Result { + use std::time::SystemTime; + + if wasi_clock.flags != wasi::__WASI_SUBSCRIPTION_CLOCK_ABSTIME { + return Ok(u128::from(wasi_clock.timeout)); + } + let now: u128 = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| Error::ENOTCAPABLE)? + .as_nanos(); + let deadline = u128::from(wasi_clock.timeout); + Ok(deadline.saturating_sub(now)) +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ClockEventData { + pub(crate) delay: u128, // delay is expressed in nanoseconds + pub(crate) userdata: wasi::__wasi_userdata_t, +} + +#[derive(Debug)] +pub(crate) struct FdEventData<'a> { + pub(crate) descriptor: &'a Descriptor, + pub(crate) r#type: wasi::__wasi_eventtype_t, + pub(crate) userdata: wasi::__wasi_userdata_t, +} diff --git a/wasi-common/src/hostcalls_impl/mod.rs b/wasi-common/src/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..f74be814b0 --- /dev/null +++ b/wasi-common/src/hostcalls_impl/mod.rs @@ -0,0 +1,7 @@ +mod fs; +mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::fs_helpers::PathGet; +pub(crate) use self::misc::*; diff --git a/wasi-common/src/lib.rs b/wasi-common/src/lib.rs new file mode 100644 index 0000000000..245966028d --- /dev/null +++ b/wasi-common/src/lib.rs @@ -0,0 +1,42 @@ +#![deny( + // missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod ctx; +mod error; +mod fdentry; +mod helpers; +mod hostcalls_impl; +mod sys; +#[macro_use] +mod macros; +pub mod fs; +mod host; +pub mod hostcalls; +mod memory; +pub mod wasi; +pub mod wasi32; + +pub use ctx::{WasiCtx, WasiCtxBuilder}; +pub use sys::preopen_dir; + +pub type Error = error::Error; +pub(crate) type Result = std::result::Result; diff --git a/wasi-common/src/macros.rs b/wasi-common/src/macros.rs new file mode 100644 index 0000000000..56476d3d4e --- /dev/null +++ b/wasi-common/src/macros.rs @@ -0,0 +1,13 @@ +macro_rules! hostcalls { + ($(pub unsafe fn $name:ident($($arg:ident: $ty:ty,)*) -> $ret:ty;)*) => ($( + #[wasi_common_cbindgen::wasi_common_cbindgen] + pub unsafe fn $name($($arg: $ty,)*) -> $ret { + let ret = match crate::hostcalls_impl::$name($($arg,)*) { + Ok(()) => crate::wasi::__WASI_ESUCCESS, + Err(e) => e.as_wasi_errno(), + }; + + ret + } + )*) +} diff --git a/wasi-common/src/memory.rs b/wasi-common/src/memory.rs new file mode 100644 index 0000000000..9f1173a8fb --- /dev/null +++ b/wasi-common/src/memory.rs @@ -0,0 +1,480 @@ +//! Functions to store and load data to and from wasm linear memory, +//! transforming them from and to host data types. +//! +//! Endianness concerns are completely encapsulated in this file, so +//! that users outside this file holding a `wasi::*` value never need +//! to consider what endianness it's in. Inside this file, +//! wasm linear-memory-ordered values are called "raw" values, and +//! are not held for long durations. + +#![allow(unused)] +use crate::{host, wasi, wasi32, Error, Result}; +use num::PrimInt; +use std::convert::TryFrom; +use std::mem::{align_of, size_of}; +use std::{ptr, slice}; + +fn dec_ptr(memory: &[u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*const u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_ptr()) +} + +fn dec_ptr_mut(memory: &mut [u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*mut u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get_mut(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_mut_ptr()) +} + +fn dec_ptr_to<'memory, T>(memory: &'memory [u8], ptr: wasi32::uintptr_t) -> Result<&'memory T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr(memory, ptr, size_of::()).map(|p| unsafe { &*(p as *const T) }) +} + +fn dec_ptr_to_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr_mut(memory, ptr, size_of::()).map(|p| unsafe { &mut *(p as *mut T) }) +} + +/// This function does not perform endianness conversions! +fn dec_raw_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result { + dec_ptr_to::(memory, ptr).map(|p| unsafe { ptr::read(p) }) +} + +/// This function does not perform endianness conversions! +fn enc_raw_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> { + dec_ptr_to_mut::(memory, ptr).map(|p| unsafe { ptr::write(p, t) }) +} + +pub(crate) fn dec_int_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result +where + T: PrimInt, +{ + dec_raw_byref::(memory, ptr).map(|i| PrimInt::from_le(i)) +} + +pub(crate) fn enc_int_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> +where + T: PrimInt, +{ + enc_raw_byref::(memory, ptr, PrimInt::to_le(t)) +} + +fn check_slice_of(ptr: wasi32::uintptr_t, len: wasi32::size_t) -> Result<(usize, usize)> { + // check alignment, and that length doesn't overflow + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + let len = dec_usize(len); + let len_bytes = if let Some(len) = size_of::().checked_mul(len) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + Ok((len, len_bytes)) +} + +fn dec_raw_slice_of<'memory, T>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr(memory, ptr, len_bytes)? as *const T; + Ok(unsafe { slice::from_raw_parts(ptr, len) }) +} + +fn dec_raw_slice_of_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + Ok(unsafe { slice::from_raw_parts_mut(ptr, len) }) +} + +fn raw_slice_for_enc<'memory, T>( + memory: &'memory mut [u8], + slice: &[T], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut [T]> { + // check alignment + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + // check that length doesn't overflow + let len_bytes = if let Some(len) = size_of::().checked_mul(slice.len()) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + // get the pointer into guest memory + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + + Ok(unsafe { slice::from_raw_parts_mut(ptr, slice.len()) }) +} + +pub(crate) fn dec_slice_of_u8<'memory>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [u8]> { + dec_raw_slice_of::(memory, ptr, len) +} + +pub(crate) fn dec_slice_of_mut_u8<'memory>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [u8]> { + dec_raw_slice_of_mut::(memory, ptr, len) +} + +pub(crate) fn enc_slice_of_u8( + memory: &mut [u8], + slice: &[u8], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let output = raw_slice_for_enc::(memory, slice, ptr)?; + + output.copy_from_slice(slice); + + Ok(()) +} + +pub(crate) fn enc_slice_of_wasi32_uintptr( + memory: &mut [u8], + slice: &[wasi32::uintptr_t], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let mut output_iter = raw_slice_for_enc::(memory, slice, ptr)?.into_iter(); + + for p in slice { + *output_iter.next().unwrap() = PrimInt::to_le(*p); + } + + Ok(()) +} + +macro_rules! dec_enc_scalar { + ( $ty:ident, $dec_byref:ident, $enc_byref:ident) => { + pub(crate) fn $dec_byref(memory: &mut [u8], ptr: wasi32::uintptr_t) -> Result { + dec_int_byref::(memory, ptr) + } + + pub(crate) fn $enc_byref( + memory: &mut [u8], + ptr: wasi32::uintptr_t, + x: wasi::$ty, + ) -> Result<()> { + enc_int_byref::(memory, ptr, x) + } + }; +} + +pub(crate) fn dec_ciovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_ciovec_t { + buf: dec_ptr(memory, buf, len)? as *const u8, + buf_len: len, + }) + }) + .collect() +} + +pub(crate) fn dec_iovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_iovec_t { + buf: dec_ptr(memory, buf, len)? as *mut u8, + buf_len: len, + }) + }) + .collect() +} + +dec_enc_scalar!(__wasi_clockid_t, dec_clockid_byref, enc_clockid_byref); +dec_enc_scalar!(__wasi_errno_t, dec_errno_byref, enc_errno_byref); +dec_enc_scalar!(__wasi_exitcode_t, dec_exitcode_byref, enc_exitcode_byref); +dec_enc_scalar!(__wasi_fd_t, dec_fd_byref, enc_fd_byref); +dec_enc_scalar!(__wasi_fdflags_t, dec_fdflags_byref, enc_fdflags_byref); +dec_enc_scalar!(__wasi_device_t, dev_device_byref, enc_device_byref); +dec_enc_scalar!(__wasi_inode_t, dev_inode_byref, enc_inode_byref); +dec_enc_scalar!(__wasi_linkcount_t, dev_linkcount_byref, enc_linkcount_byref); + +pub(crate) fn dec_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, filestat_ptr)?; + + Ok(wasi::__wasi_filestat_t { + st_dev: PrimInt::from_le(raw.st_dev), + st_ino: PrimInt::from_le(raw.st_ino), + st_filetype: PrimInt::from_le(raw.st_filetype), + st_nlink: PrimInt::from_le(raw.st_nlink), + st_size: PrimInt::from_le(raw.st_size), + st_atim: PrimInt::from_le(raw.st_atim), + st_mtim: PrimInt::from_le(raw.st_mtim), + st_ctim: PrimInt::from_le(raw.st_ctim), + }) +} + +pub(crate) fn enc_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, + filestat: wasi::__wasi_filestat_t, +) -> Result<()> { + let raw = wasi::__wasi_filestat_t { + st_dev: PrimInt::to_le(filestat.st_dev), + st_ino: PrimInt::to_le(filestat.st_ino), + st_filetype: PrimInt::to_le(filestat.st_filetype), + st_nlink: PrimInt::to_le(filestat.st_nlink), + st_size: PrimInt::to_le(filestat.st_size), + st_atim: PrimInt::to_le(filestat.st_atim), + st_mtim: PrimInt::to_le(filestat.st_mtim), + st_ctim: PrimInt::to_le(filestat.st_ctim), + }; + + enc_raw_byref::(memory, filestat_ptr, raw) +} + +pub(crate) fn dec_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, fdstat_ptr)?; + + Ok(wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::from_le(raw.fs_filetype), + fs_flags: PrimInt::from_le(raw.fs_flags), + fs_rights_base: PrimInt::from_le(raw.fs_rights_base), + fs_rights_inheriting: PrimInt::from_le(raw.fs_rights_inheriting), + }) +} + +pub(crate) fn enc_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, + fdstat: wasi::__wasi_fdstat_t, +) -> Result<()> { + let raw = wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::to_le(fdstat.fs_filetype), + fs_flags: PrimInt::to_le(fdstat.fs_flags), + fs_rights_base: PrimInt::to_le(fdstat.fs_rights_base), + fs_rights_inheriting: PrimInt::to_le(fdstat.fs_rights_inheriting), + }; + + enc_raw_byref::(memory, fdstat_ptr, raw) +} + +dec_enc_scalar!(__wasi_filedelta_t, dec_filedelta_byref, enc_filedelta_byref); +dec_enc_scalar!(__wasi_filesize_t, dec_filesize_byref, enc_filesize_byref); +dec_enc_scalar!(__wasi_filetype_t, dec_filetype_byref, enc_filetype_byref); + +dec_enc_scalar!( + __wasi_lookupflags_t, + dec_lookupflags_byref, + enc_lookupflags_byref +); + +dec_enc_scalar!(__wasi_oflags_t, dec_oflags_byref, enc_oflags_byref); + +pub(crate) fn dec_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, prestat_ptr)?; + + match PrimInt::from_le(raw.pr_type) { + wasi::__WASI_PREOPENTYPE_DIR => Ok(host::__wasi_prestat_t { + pr_type: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u { + dir: host::__wasi_prestat_dir { + pr_name_len: dec_usize(PrimInt::from_le(unsafe { raw.u.dir.pr_name_len })), + }, + }, + }), + _ => Err(Error::EINVAL), + } +} + +pub(crate) fn enc_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, + prestat: host::__wasi_prestat_t, +) -> Result<()> { + let raw = match prestat.pr_type { + wasi::__WASI_PREOPENTYPE_DIR => Ok(wasi32::__wasi_prestat_t { + pr_type: PrimInt::to_le(wasi::__WASI_PREOPENTYPE_DIR), + u: wasi32::__wasi_prestat_u { + dir: wasi32::__wasi_prestat_dir { + pr_name_len: enc_usize(unsafe { prestat.u.dir.pr_name_len }), + }, + }, + }), + _ => Err(Error::EINVAL), + }?; + + enc_raw_byref::(memory, prestat_ptr, raw) +} + +dec_enc_scalar!(__wasi_rights_t, dec_rights_byref, enc_rights_byref); +dec_enc_scalar!(__wasi_timestamp_t, dec_timestamp_byref, enc_timestamp_byref); + +pub(crate) fn dec_usize(size: wasi32::size_t) -> usize { + usize::try_from(size).unwrap() +} + +pub(crate) fn enc_usize(size: usize) -> wasi32::size_t { + wasi32::size_t::try_from(size).unwrap() +} + +pub(crate) fn enc_usize_byref( + memory: &mut [u8], + usize_ptr: wasi32::uintptr_t, + host_usize: usize, +) -> Result<()> { + enc_int_byref::(memory, usize_ptr, enc_usize(host_usize)) +} + +dec_enc_scalar!(__wasi_whence_t, dec_whence_byref, enc_whence_byref); + +dec_enc_scalar!( + __wasi_subclockflags_t, + dec_subclockflags_byref, + enc_subclockflags_byref +); + +dec_enc_scalar!( + __wasi_eventrwflags_t, + dec_eventrwflags_byref, + enc_eventrwflags_byref +); + +dec_enc_scalar!(__wasi_eventtype_t, dec_eventtype_byref, enc_eventtype_byref); +dec_enc_scalar!(__wasi_userdata_t, dec_userdata_byref, enc_userdata_byref); + +pub(crate) fn dec_subscriptions( + memory: &mut [u8], + input: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, +) -> Result> { + let raw_input_slice = + dec_raw_slice_of::(memory, input, nsubscriptions)?; + + raw_input_slice + .into_iter() + .map(|raw_subscription| { + let userdata = PrimInt::from_le(raw_subscription.userdata); + let r#type = PrimInt::from_le(raw_subscription.r#type); + let raw_u = raw_subscription.u; + let u = match r#type { + wasi::__WASI_EVENTTYPE_CLOCK => wasi::__wasi_subscription_u { + clock: unsafe { + wasi::__wasi_subscription_clock_t { + identifier: PrimInt::from_le(raw_u.clock.identifier), + clock_id: PrimInt::from_le(raw_u.clock.clock_id), + timeout: PrimInt::from_le(raw_u.clock.timeout), + precision: PrimInt::from_le(raw_u.clock.precision), + flags: PrimInt::from_le(raw_u.clock.flags), + } + }, + }, + wasi::__WASI_EVENTTYPE_FD_READ | wasi::__WASI_EVENTTYPE_FD_WRITE => { + wasi::__wasi_subscription_u { + fd_readwrite: wasi::__wasi_subscription_fd_readwrite_t { + file_descriptor: PrimInt::from_le(unsafe { + raw_u.fd_readwrite.file_descriptor + }), + }, + } + } + _ => return Err(Error::EINVAL), + }; + Ok(wasi::__wasi_subscription_t { + userdata, + r#type, + u, + }) + }) + .collect::>>() +} + +pub(crate) fn enc_events( + memory: &mut [u8], + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + events: Vec, +) -> Result<()> { + let mut raw_output_iter = + dec_raw_slice_of_mut::(memory, output, nsubscriptions)?.into_iter(); + + for event in events.iter() { + *raw_output_iter + .next() + .expect("the number of events cannot exceed the number of subscriptions") = { + let fd_readwrite = unsafe { event.u.fd_readwrite }; + wasi::__wasi_event_t { + userdata: PrimInt::to_le(event.userdata), + r#type: PrimInt::to_le(event.r#type), + error: PrimInt::to_le(event.error), + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: PrimInt::to_le(fd_readwrite.nbytes), + flags: PrimInt::to_le(fd_readwrite.flags), + }, + }, + } + }; + } + + Ok(()) +} + +dec_enc_scalar!(__wasi_advice_t, dec_advice_byref, enc_advice_byref); +dec_enc_scalar!(__wasi_fstflags_t, dec_fstflags_byref, enc_fstflags_byref); +dec_enc_scalar!(__wasi_dircookie_t, dec_dircookie_byref, enc_dircookie_byref); diff --git a/wasi-common/src/sys/mod.rs b/wasi-common/src/sys/mod.rs new file mode 100644 index 0000000000..972726bd56 --- /dev/null +++ b/wasi-common/src/sys/mod.rs @@ -0,0 +1,24 @@ +use crate::wasi; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(unix)] { + mod unix; + pub(crate) use self::unix::*; + pub use self::unix::preopen_dir; + + pub(crate) fn errno_from_host(err: i32) -> wasi::__wasi_errno_t { + host_impl::errno_from_nix(nix::errno::from_i32(err)).as_wasi_errno() + } + } else if #[cfg(windows)] { + mod windows; + pub(crate) use self::windows::*; + pub use self::windows::preopen_dir; + + pub(crate) fn errno_from_host(err: i32) -> wasi::__wasi_errno_t { + host_impl::errno_from_win(winx::winerror::WinError::from_u32(err as u32)) + } + } else { + compile_error!("wasi-common doesn't compile for this platform yet"); + } +} diff --git a/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs b/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs new file mode 100644 index 0000000000..6c46e2e8f6 --- /dev/null +++ b/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs @@ -0,0 +1,289 @@ +use super::osfile::OsFile; +use crate::hostcalls_impl::PathGet; +use crate::sys::host_impl; +use crate::sys::unix::str_to_cstring; +use crate::{wasi, Error, Result}; +use nix::libc::{self, c_long, c_void}; +use std::convert::TryInto; +use std::fs::File; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use nix::errno; + use nix::libc::unlinkat; + + let path_cstr = str_to_cstring(resolved.path())?; + + // nix doesn't expose unlinkat() yet + match unsafe { unlinkat(resolved.dirfd().as_raw_fd(), path_cstr.as_ptr(), 0) } { + 0 => Ok(()), + _ => { + let mut e = errno::Errno::last(); + + // Non-Linux implementations may return EPERM when attempting to remove a + // directory without REMOVEDIR. While that's what POSIX specifies, it's + // less useful. Adjust this to EISDIR. It doesn't matter that this is not + // atomic with the unlinkat, because if the file is removed and a directory + // is created before fstatat sees it, we're racing with that change anyway + // and unlinkat could have legitimately seen the directory if the race had + // turned out differently. + use nix::fcntl::AtFlags; + use nix::sys::stat::{fstatat, SFlag}; + + if e == errno::Errno::EPERM { + if let Ok(stat) = fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + if SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFDIR) { + e = errno::Errno::EISDIR; + } + } else { + e = errno::Errno::last(); + } + } + + Err(host_impl::errno_from_nix(e)) + } + } +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use nix::{errno::Errno, fcntl::AtFlags, libc::symlinkat, sys::stat::fstatat}; + + let old_path_cstr = str_to_cstring(old_path)?; + let new_path_cstr = str_to_cstring(resolved.path())?; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + let res = unsafe { + symlinkat( + old_path_cstr.as_ptr(), + resolved.dirfd().as_raw_fd(), + new_path_cstr.as_ptr(), + ) + }; + if res != 0 { + match Errno::last() { + Errno::ENOTDIR => { + // On BSD, symlinkat returns ENOTDIR when it should in fact + // return a EEXIST. It seems that it gets confused with by + // the trailing slash in the target path. Thus, we strip + // the trailing slash and check if the path exists, and + // adjust the error code appropriately. + let new_path = resolved.path().trim_end_matches('/'); + if let Ok(_) = fstatat( + resolved.dirfd().as_raw_fd(), + new_path, + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + Err(Error::EEXIST) + } else { + Err(Error::ENOTDIR) + } + } + x => Err(host_impl::errno_from_nix(x)), + } + } else { + Ok(()) + } +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use nix::{errno::Errno, fcntl::AtFlags, libc::renameat, sys::stat::fstatat}; + let old_path_cstr = str_to_cstring(resolved_old.path())?; + let new_path_cstr = str_to_cstring(resolved_new.path())?; + + let res = unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + old_path_cstr.as_ptr(), + resolved_new.dirfd().as_raw_fd(), + new_path_cstr.as_ptr(), + ) + }; + if res != 0 { + // Currently, this is verified to be correct on macOS, where + // ENOENT can be returned in case when we try to rename a file + // into a name with a trailing slash. On macOS, if the latter does + // not exist, an ENOENT is thrown, whereas on Linux we observe the + // correct behaviour of throwing an ENOTDIR since the destination is + // indeed not a directory. + // + // TODO + // Verify on other BSD-based OSes. + match Errno::last() { + Errno::ENOENT => { + // check if the source path exists + if let Ok(_) = fstatat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + // check if destination contains a trailing slash + if resolved_new.path().contains('/') { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + x => Err(host_impl::errno_from_nix(x)), + } + } else { + Ok(()) + } +} + +pub(crate) fn fd_readdir( + os_file: &mut OsFile, + host_buf: &mut [u8], + cookie: wasi::__wasi_dircookie_t, +) -> Result { + use crate::sys::unix::bsd::osfile::DirStream; + use libc::{fdopendir, readdir, rewinddir, seekdir, telldir}; + use nix::errno::Errno; + use std::mem::ManuallyDrop; + use std::sync::Mutex; + + let dir_stream = match os_file.dir_stream { + Some(ref mut dir_stream) => dir_stream, + None => { + let file = os_file.file.try_clone()?; + let dir_ptr = unsafe { fdopendir(file.as_raw_fd()) }; + os_file.dir_stream.get_or_insert(Mutex::new(DirStream { + file: ManuallyDrop::new(file), + dir_ptr, + })) + } + }; + let dir_stream = dir_stream.lock().unwrap(); + + let host_buf_ptr = host_buf.as_mut_ptr(); + let host_buf_len = host_buf.len(); + + if cookie != wasi::__WASI_DIRCOOKIE_START { + unsafe { seekdir(dir_stream.dir_ptr, cookie as c_long) }; + } else { + unsafe { rewinddir(dir_stream.dir_ptr) }; + } + + let mut left = host_buf_len; + let mut host_buf_offset: usize = 0; + + loop { + let host_entry = unsafe { readdir(dir_stream.dir_ptr) }; + if host_entry.is_null() { + // FIXME + // Currently, these are verified to be correct on macOS. + // Need to still verify these on other BSD-based OSes. + match Errno::last() { + Errno::EBADF => return Err(Error::EBADF), + Errno::EFAULT => return Err(Error::EFAULT), + Errno::EIO => return Err(Error::EIO), + _ => break, // not an error + } + } + + let mut entry: wasi::__wasi_dirent_t = + host_impl::dirent_from_host(&unsafe { *host_entry })?; + // Set d_next manually: + // * on macOS d_seekoff is not set for some reason + // * on FreeBSD d_seekoff doesn't exist; there is d_off but it is + // not equivalent to the value read from telldir call + entry.d_next = unsafe { telldir(dir_stream.dir_ptr) } as wasi::__wasi_dircookie_t; + + log::debug!("fd_readdir entry = {:?}", entry); + + let name_len = entry.d_namlen.try_into()?; + let required_space = std::mem::size_of_val(&entry) + name_len; + if required_space > left { + break; + } + unsafe { + let ptr = host_buf_ptr.offset(host_buf_offset.try_into()?) as *mut c_void + as *mut wasi::__wasi_dirent_t; + *ptr = entry; + } + host_buf_offset += std::mem::size_of_val(&entry); + let name_ptr = unsafe { *host_entry }.d_name.as_ptr(); + unsafe { + std::ptr::copy_nonoverlapping( + name_ptr as *const _, + host_buf_ptr.offset(host_buf_offset.try_into()?) as *mut _, + name_len, + ) + }; + host_buf_offset += name_len; + left -= required_space; + } + + Ok(host_buf_len - left) +} + +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub(crate) fn fd_advise( + file: &File, + advice: wasi::__wasi_advice_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + use nix::errno::Errno; + + match advice { + wasi::__WASI_ADVICE_DONTNEED => return Ok(()), + // unfortunately, the advisory syscall in macOS doesn't take any flags of this + // sort (unlike on Linux), hence, they are left here as a noop + wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => {} + _ => return Err(Error::EINVAL), + } + + // From macOS man pages: + // F_RDADVISE Issue an advisory read async with no copy to user. + // + // The F_RDADVISE command operates on the following structure which holds information passed from + // the user to the system: + // + // struct radvisory { + // off_t ra_offset; /* offset into the file */ + // int ra_count; /* size of the read */ + // }; + let advisory = libc::radvisory { + ra_offset: offset.try_into()?, + ra_count: len.try_into()?, + }; + + let res = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_RDADVISE, &advisory) }; + Errno::result(res).map(|_| ()).map_err(Error::from) +} + +// TODO +// It seems that at least some BSDs do support `posix_fadvise`, +// so we should investigate further. +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +pub(crate) fn fd_advise( + _file: &File, + advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, +) -> Result<()> { + match advice { + wasi::__WASI_ADVICE_DONTNEED + | wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => {} + _ => return Err(Error::EINVAL), + } + + Ok(()) +} diff --git a/wasi-common/src/sys/unix/bsd/mod.rs b/wasi-common/src/sys/unix/bsd/mod.rs new file mode 100644 index 0000000000..4cba400a27 --- /dev/null +++ b/wasi-common/src/sys/unix/bsd/mod.rs @@ -0,0 +1,82 @@ +pub(crate) mod hostcalls_impl; +pub(crate) mod osfile; + +pub(crate) mod fdentry_impl { + use crate::{sys::host_impl, Result}; + use std::os::unix::prelude::AsRawFd; + + pub(crate) unsafe fn isatty(fd: &impl AsRawFd) -> Result { + let res = libc::isatty(fd.as_raw_fd()); + if res == 1 { + // isatty() returns 1 if fd is an open file descriptor referring to a terminal... + Ok(true) + } else { + // ... otherwise 0 is returned, and errno is set to indicate the error. + match nix::errno::Errno::last() { + nix::errno::Errno::ENOTTY => Ok(false), + x => Err(host_impl::errno_from_nix(x)), + } + } + } +} + +pub(crate) mod host_impl { + use super::super::host_impl::dirent_filetype_from_host; + use crate::{wasi, Result}; + + pub(crate) const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_SYNC; + + pub(crate) fn dirent_from_host( + host_entry: &nix::libc::dirent, + ) -> Result { + let mut entry = unsafe { std::mem::zeroed::() }; + let d_type = dirent_filetype_from_host(host_entry)?; + entry.d_ino = host_entry.d_ino; + entry.d_next = host_entry.d_seekoff; + entry.d_namlen = u32::from(host_entry.d_namlen); + entry.d_type = d_type; + Ok(entry) + } +} + +pub(crate) mod fs_helpers { + use cfg_if::cfg_if; + + pub(crate) fn utime_now() -> libc::c_long { + cfg_if! { + if #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + -1 + } else if #[cfg(target_os = "openbsd")] { + // https://github.com/openbsd/src/blob/master/sys/sys/stat.h#L187 + -2 + } else if #[cfg(target_os = "netbsd" )] { + // http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/sys/stat.h?rev=1.69&content-type=text/x-cvsweb-markup&only_with_tag=MAIN + 1_073_741_823 + } + } + } + + pub(crate) fn utime_omit() -> libc::c_long { + cfg_if! { + if #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + -2 + } else if #[cfg(target_os = "openbsd")] { + // https://github.com/openbsd/src/blob/master/sys/sys/stat.h#L187 + -1 + } else if #[cfg(target_os = "netbsd")] { + // http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/sys/stat.h?rev=1.69&content-type=text/x-cvsweb-markup&only_with_tag=MAIN + 1_073_741_822 + } + } + } +} diff --git a/wasi-common/src/sys/unix/bsd/osfile.rs b/wasi-common/src/sys/unix/bsd/osfile.rs new file mode 100644 index 0000000000..41340149aa --- /dev/null +++ b/wasi-common/src/sys/unix/bsd/osfile.rs @@ -0,0 +1,52 @@ +use std::fs; +use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::sync::Mutex; + +#[derive(Debug)] +pub(crate) struct DirStream { + pub(crate) file: ManuallyDrop, + pub(crate) dir_ptr: *mut libc::DIR, +} + +impl Drop for DirStream { + fn drop(&mut self) { + unsafe { libc::closedir(self.dir_ptr) }; + } +} + +#[derive(Debug)] +pub(crate) struct OsFile { + pub(crate) file: fs::File, + pub(crate) dir_stream: Option>, +} + +impl From for OsFile { + fn from(file: fs::File) -> Self { + Self { + file, + dir_stream: None, + } + } +} + +impl AsRawFd for OsFile { + fn as_raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } +} + +impl Deref for OsFile { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for OsFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} diff --git a/wasi-common/src/sys/unix/dir.rs b/wasi-common/src/sys/unix/dir.rs new file mode 100644 index 0000000000..20b75f968d --- /dev/null +++ b/wasi-common/src/sys/unix/dir.rs @@ -0,0 +1,216 @@ +// Based on src/dir.rs from nix +#![allow(unused)] // temporarily, until BSD catches up with this change +use crate::hostcalls_impl::FileType; +use libc; +use nix::{errno::Errno, Error, Result}; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use std::{ffi, ptr}; + +#[cfg(target_os = "linux")] +use libc::{dirent64 as dirent, readdir64_r as readdir_r}; + +#[cfg(not(target_os = "linux"))] +use libc::{dirent, readdir_r}; + +/// An open directory. +/// +/// This is a lower-level interface than `std::fs::ReadDir`. Notable differences: +/// * can be opened from a file descriptor (as returned by `openat`, perhaps before knowing +/// if the path represents a file or directory). +/// * implements `AsRawFd`, so it can be passed to `fstat`, `openat`, etc. +/// The file descriptor continues to be owned by the `Dir`, so callers must not keep a `RawFd` +/// after the `Dir` is dropped. +/// * can be iterated through multiple times without closing and reopening the file +/// descriptor. Each iteration rewinds when finished. +/// * returns entries for `.` (current directory) and `..` (parent directory). +/// * returns entries' names as a `CStr` (no allocation or conversion beyond whatever libc +/// does). +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) struct Dir(ptr::NonNull); + +impl Dir { + /// Converts from a descriptor-based object, closing the descriptor on success or failure. + #[inline] + pub(crate) fn from(fd: F) -> Result { + unsafe { Self::from_fd(fd.into_raw_fd()) } + } + + /// Converts from a file descriptor, closing it on success or failure. + unsafe fn from_fd(fd: RawFd) -> Result { + let d = libc::fdopendir(fd); + if d.is_null() { + let e = Error::last(); + libc::close(fd); + return Err(e); + }; + // Always guaranteed to be non-null by the previous check + Ok(Self(ptr::NonNull::new(d).unwrap())) + } + + /// Set the position of the directory stream, see `seekdir(3)`. + #[cfg(not(target_os = "android"))] + pub(crate) fn seek(&mut self, loc: SeekLoc) { + unsafe { libc::seekdir(self.0.as_ptr(), loc.0) } + } + + /// Reset directory stream, see `rewinddir(3)`. + pub(crate) fn rewind(&mut self) { + unsafe { libc::rewinddir(self.0.as_ptr()) } + } + + /// Get the current position in the directory stream. + /// + /// If this location is given to `Dir::seek`, the entries up to the previously returned + /// will be omitted and the iteration will start from the currently pending directory entry. + #[cfg(not(target_os = "android"))] + #[allow(dead_code)] + pub(crate) fn tell(&self) -> SeekLoc { + let loc = unsafe { libc::telldir(self.0.as_ptr()) }; + SeekLoc(loc) + } +} + +// `Dir` is not `Sync`. With the current implementation, it could be, but according to +// https://www.gnu.org/software/libc/manual/html_node/Reading_002fClosing-Directory.html, +// future versions of POSIX are likely to obsolete `readdir_r` and specify that it's unsafe to +// call `readdir` simultaneously from multiple threads. +// +// `Dir` is safe to pass from one thread to another, as it's not reference-counted. +unsafe impl Send for Dir {} + +impl AsRawFd for Dir { + fn as_raw_fd(&self) -> RawFd { + unsafe { libc::dirfd(self.0.as_ptr()) } + } +} + +impl Drop for Dir { + fn drop(&mut self) { + unsafe { libc::closedir(self.0.as_ptr()) }; + } +} + +pub(crate) struct IntoIter(Dir); +impl Iterator for IntoIter { + type Item = Result; + fn next(&mut self) -> Option { + unsafe { + // Note: POSIX specifies that portable applications should dynamically allocate a + // buffer with room for a `d_name` field of size `pathconf(..., _PC_NAME_MAX)` plus 1 + // for the NUL byte. It doesn't look like the std library does this; it just uses + // fixed-sized buffers (and libc's dirent seems to be sized so this is appropriate). + // Probably fine here too then. + // + // See `impl Iterator for ReadDir` [1] for more details. + // [1] https://github.com/rust-lang/rust/blob/master/src/libstd/sys/unix/fs.rs + let mut ent = std::mem::MaybeUninit::::uninit(); + let mut result = ptr::null_mut(); + if let Err(e) = Errno::result(readdir_r( + (self.0).0.as_ptr(), + ent.as_mut_ptr(), + &mut result, + )) { + return Some(Err(e)); + } + if result.is_null() { + None + } else { + assert_eq!(result, ent.as_mut_ptr(), "readdir_r specification violated"); + Some(Ok(Entry(ent.assume_init()))) + } + } + } +} + +impl IntoIterator for Dir { + type IntoIter = IntoIter; + type Item = Result; + + fn into_iter(self) -> IntoIter { + IntoIter(self) + } +} + +/// A directory entry, similar to `std::fs::DirEntry`. +/// +/// Note that unlike the std version, this may represent the `.` or `..` entries. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[repr(transparent)] +pub(crate) struct Entry(dirent); + +pub(crate) type Type = FileType; + +impl Entry { + /// Returns the inode number (`d_ino`) of the underlying `dirent`. + #[cfg(any( + target_os = "android", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "haiku", + target_os = "ios", + target_os = "l4re", + target_os = "linux", + target_os = "macos", + target_os = "solaris" + ))] + pub(crate) fn ino(&self) -> u64 { + self.0.d_ino.into() + } + + /// Returns the inode number (`d_fileno`) of the underlying `dirent`. + #[cfg(not(any( + target_os = "android", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "haiku", + target_os = "ios", + target_os = "l4re", + target_os = "linux", + target_os = "macos", + target_os = "solaris" + )))] + pub(crate) fn ino(&self) -> u64 { + u64::from(self.0.d_fileno) + } + + /// Returns the bare file name of this directory entry without any other leading path component. + pub(crate) fn file_name(&self) -> &ffi::CStr { + unsafe { ::std::ffi::CStr::from_ptr(self.0.d_name.as_ptr()) } + } + + /// Returns the type of this directory entry, if known. + /// + /// See platform `readdir(3)` or `dirent(5)` manpage for when the file type is known; + /// notably, some Linux filesystems don't implement this. The caller should use `stat` or + /// `fstat` if this returns `None`. + pub(crate) fn file_type(&self) -> FileType { + match self.0.d_type { + libc::DT_CHR => Type::CharacterDevice, + libc::DT_DIR => Type::Directory, + libc::DT_BLK => Type::BlockDevice, + libc::DT_REG => Type::RegularFile, + libc::DT_LNK => Type::Symlink, + /* libc::DT_UNKNOWN | libc::DT_SOCK | libc::DT_FIFO */ _ => Type::Unknown, + } + } + + #[cfg(target_os = "linux")] + pub(crate) fn seek_loc(&self) -> SeekLoc { + unsafe { SeekLoc::from_raw(self.0.d_off) } + } +} + +#[cfg(not(target_os = "android"))] +#[derive(Clone, Copy, Debug)] +pub(crate) struct SeekLoc(libc::c_long); + +#[cfg(not(target_os = "android"))] +impl SeekLoc { + pub(crate) unsafe fn from_raw(loc: i64) -> Self { + Self(loc.into()) + } + + pub(crate) fn to_raw(&self) -> i64 { + self.0.into() + } +} diff --git a/wasi-common/src/sys/unix/fdentry_impl.rs b/wasi-common/src/sys/unix/fdentry_impl.rs new file mode 100644 index 0000000000..0a342acbd9 --- /dev/null +++ b/wasi-common/src/sys/unix/fdentry_impl.rs @@ -0,0 +1,135 @@ +use crate::fdentry::Descriptor; +use crate::{wasi, Error, Result}; +use std::io; +use std::os::unix::prelude::{AsRawFd, FileTypeExt, FromRawFd, RawFd}; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub(crate) use super::linux::osfile::*; + pub(crate) use super::linux::fdentry_impl::*; + } else if #[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) use super::bsd::osfile::*; + pub(crate) use super::bsd::fdentry_impl::*; + } +} + +impl AsRawFd for Descriptor { + fn as_raw_fd(&self) -> RawFd { + match self { + Self::OsFile(file) => file.as_raw_fd(), + Self::Stdin => io::stdin().as_raw_fd(), + Self::Stdout => io::stdout().as_raw_fd(), + Self::Stderr => io::stderr().as_raw_fd(), + } + } +} + +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_and_access_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(fd)?; + + use nix::fcntl::{fcntl, OFlag, F_GETFL}; + let flags_bits = fcntl(fd.as_raw_fd(), F_GETFL)?; + let flags = OFlag::from_bits_truncate(flags_bits); + let accmode = flags & OFlag::O_ACCMODE; + if accmode == OFlag::O_RDONLY { + rights_base &= !wasi::__WASI_RIGHT_FD_WRITE; + } else if accmode == OFlag::O_WRONLY { + rights_base &= !wasi::__WASI_RIGHT_FD_READ; + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + // we just make a `File` here for convenience; we don't want it to close when it drops + let file = std::mem::ManuallyDrop::new(std::fs::File::from_raw_fd(fd.as_raw_fd())); + let ft = file.metadata()?.file_type(); + if ft.is_block_device() { + log::debug!("Host fd {:?} is a block device", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_BLOCK_DEVICE, + wasi::RIGHTS_BLOCK_DEVICE_BASE, + wasi::RIGHTS_BLOCK_DEVICE_INHERITING, + ) + } else if ft.is_char_device() { + log::debug!("Host fd {:?} is a char device", fd.as_raw_fd()); + if isatty(fd)? { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_CHARACTER_DEVICE_BASE, + wasi::RIGHTS_CHARACTER_DEVICE_INHERITING, + ) + } + } else if ft.is_dir() { + log::debug!("Host fd {:?} is a directory", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if ft.is_file() { + log::debug!("Host fd {:?} is a file", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else if ft.is_socket() { + log::debug!("Host fd {:?} is a socket", fd.as_raw_fd()); + use nix::sys::socket; + match socket::getsockopt(fd.as_raw_fd(), socket::sockopt::SockType)? { + socket::SockType::Datagram => ( + wasi::__WASI_FILETYPE_SOCKET_DGRAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + socket::SockType::Stream => ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + _ => return Err(Error::EINVAL), + } + } else if ft.is_fifo() { + log::debug!("Host fd {:?} is a fifo", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_UNKNOWN, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + log::debug!("Host fd {:?} is unknown", fd.as_raw_fd()); + return Err(Error::EINVAL); + } + }; + + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/wasi-common/src/sys/unix/host_impl.rs b/wasi-common/src/sys/unix/host_impl.rs new file mode 100644 index 0000000000..8a3116b388 --- /dev/null +++ b/wasi-common/src/sys/unix/host_impl.rs @@ -0,0 +1,246 @@ +//! WASI host types specific to *nix host. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +use crate::hostcalls_impl::FileType; +use crate::{helpers, wasi, Error, Result}; +use log::warn; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub(crate) use super::linux::host_impl::*; + } else if #[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) use super::bsd::host_impl::*; + } +} + +pub(crate) fn errno_from_nix(errno: nix::errno::Errno) -> Error { + match errno { + nix::errno::Errno::EPERM => Error::EPERM, + nix::errno::Errno::ENOENT => Error::ENOENT, + nix::errno::Errno::ESRCH => Error::ESRCH, + nix::errno::Errno::EINTR => Error::EINTR, + nix::errno::Errno::EIO => Error::EIO, + nix::errno::Errno::ENXIO => Error::ENXIO, + nix::errno::Errno::E2BIG => Error::E2BIG, + nix::errno::Errno::ENOEXEC => Error::ENOEXEC, + nix::errno::Errno::EBADF => Error::EBADF, + nix::errno::Errno::ECHILD => Error::ECHILD, + nix::errno::Errno::EAGAIN => Error::EAGAIN, + nix::errno::Errno::ENOMEM => Error::ENOMEM, + nix::errno::Errno::EACCES => Error::EACCES, + nix::errno::Errno::EFAULT => Error::EFAULT, + nix::errno::Errno::EBUSY => Error::EBUSY, + nix::errno::Errno::EEXIST => Error::EEXIST, + nix::errno::Errno::EXDEV => Error::EXDEV, + nix::errno::Errno::ENODEV => Error::ENODEV, + nix::errno::Errno::ENOTDIR => Error::ENOTDIR, + nix::errno::Errno::EISDIR => Error::EISDIR, + nix::errno::Errno::EINVAL => Error::EINVAL, + nix::errno::Errno::ENFILE => Error::ENFILE, + nix::errno::Errno::EMFILE => Error::EMFILE, + nix::errno::Errno::ENOTTY => Error::ENOTTY, + nix::errno::Errno::ETXTBSY => Error::ETXTBSY, + nix::errno::Errno::EFBIG => Error::EFBIG, + nix::errno::Errno::ENOSPC => Error::ENOSPC, + nix::errno::Errno::ESPIPE => Error::ESPIPE, + nix::errno::Errno::EROFS => Error::EROFS, + nix::errno::Errno::EMLINK => Error::EMLINK, + nix::errno::Errno::EPIPE => Error::EPIPE, + nix::errno::Errno::EDOM => Error::EDOM, + nix::errno::Errno::ERANGE => Error::ERANGE, + nix::errno::Errno::EDEADLK => Error::EDEADLK, + nix::errno::Errno::ENAMETOOLONG => Error::ENAMETOOLONG, + nix::errno::Errno::ENOLCK => Error::ENOLCK, + nix::errno::Errno::ENOSYS => Error::ENOSYS, + nix::errno::Errno::ENOTEMPTY => Error::ENOTEMPTY, + nix::errno::Errno::ELOOP => Error::ELOOP, + nix::errno::Errno::ENOMSG => Error::ENOMSG, + nix::errno::Errno::EIDRM => Error::EIDRM, + nix::errno::Errno::ENOLINK => Error::ENOLINK, + nix::errno::Errno::EPROTO => Error::EPROTO, + nix::errno::Errno::EMULTIHOP => Error::EMULTIHOP, + nix::errno::Errno::EBADMSG => Error::EBADMSG, + nix::errno::Errno::EOVERFLOW => Error::EOVERFLOW, + nix::errno::Errno::EILSEQ => Error::EILSEQ, + nix::errno::Errno::ENOTSOCK => Error::ENOTSOCK, + nix::errno::Errno::EDESTADDRREQ => Error::EDESTADDRREQ, + nix::errno::Errno::EMSGSIZE => Error::EMSGSIZE, + nix::errno::Errno::EPROTOTYPE => Error::EPROTOTYPE, + nix::errno::Errno::ENOPROTOOPT => Error::ENOPROTOOPT, + nix::errno::Errno::EPROTONOSUPPORT => Error::EPROTONOSUPPORT, + nix::errno::Errno::EAFNOSUPPORT => Error::EAFNOSUPPORT, + nix::errno::Errno::EADDRINUSE => Error::EADDRINUSE, + nix::errno::Errno::EADDRNOTAVAIL => Error::EADDRNOTAVAIL, + nix::errno::Errno::ENETDOWN => Error::ENETDOWN, + nix::errno::Errno::ENETUNREACH => Error::ENETUNREACH, + nix::errno::Errno::ENETRESET => Error::ENETRESET, + nix::errno::Errno::ECONNABORTED => Error::ECONNABORTED, + nix::errno::Errno::ECONNRESET => Error::ECONNRESET, + nix::errno::Errno::ENOBUFS => Error::ENOBUFS, + nix::errno::Errno::EISCONN => Error::EISCONN, + nix::errno::Errno::ENOTCONN => Error::ENOTCONN, + nix::errno::Errno::ETIMEDOUT => Error::ETIMEDOUT, + nix::errno::Errno::ECONNREFUSED => Error::ECONNREFUSED, + nix::errno::Errno::EHOSTUNREACH => Error::EHOSTUNREACH, + nix::errno::Errno::EALREADY => Error::EALREADY, + nix::errno::Errno::EINPROGRESS => Error::EINPROGRESS, + nix::errno::Errno::ESTALE => Error::ESTALE, + nix::errno::Errno::EDQUOT => Error::EDQUOT, + nix::errno::Errno::ECANCELED => Error::ECANCELED, + nix::errno::Errno::EOWNERDEAD => Error::EOWNERDEAD, + nix::errno::Errno::ENOTRECOVERABLE => Error::ENOTRECOVERABLE, + other => { + warn!("Unknown error from nix: {}", other); + Error::ENOSYS + } + } +} + +pub(crate) fn nix_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> nix::fcntl::OFlag { + use nix::fcntl::OFlag; + let mut nix_flags = OFlag::empty(); + if fdflags & wasi::__WASI_FDFLAG_APPEND != 0 { + nix_flags.insert(OFlag::O_APPEND); + } + if fdflags & wasi::__WASI_FDFLAG_DSYNC != 0 { + nix_flags.insert(OFlag::O_DSYNC); + } + if fdflags & wasi::__WASI_FDFLAG_NONBLOCK != 0 { + nix_flags.insert(OFlag::O_NONBLOCK); + } + if fdflags & wasi::__WASI_FDFLAG_RSYNC != 0 { + nix_flags.insert(O_RSYNC); + } + if fdflags & wasi::__WASI_FDFLAG_SYNC != 0 { + nix_flags.insert(OFlag::O_SYNC); + } + nix_flags +} + +pub(crate) fn fdflags_from_nix(oflags: nix::fcntl::OFlag) -> wasi::__wasi_fdflags_t { + use nix::fcntl::OFlag; + let mut fdflags = 0; + if oflags.contains(OFlag::O_APPEND) { + fdflags |= wasi::__WASI_FDFLAG_APPEND; + } + if oflags.contains(OFlag::O_DSYNC) { + fdflags |= wasi::__WASI_FDFLAG_DSYNC; + } + if oflags.contains(OFlag::O_NONBLOCK) { + fdflags |= wasi::__WASI_FDFLAG_NONBLOCK; + } + if oflags.contains(O_RSYNC) { + fdflags |= wasi::__WASI_FDFLAG_RSYNC; + } + if oflags.contains(OFlag::O_SYNC) { + fdflags |= wasi::__WASI_FDFLAG_SYNC; + } + fdflags +} + +pub(crate) fn nix_from_oflags(oflags: wasi::__wasi_oflags_t) -> nix::fcntl::OFlag { + use nix::fcntl::OFlag; + let mut nix_flags = OFlag::empty(); + if oflags & wasi::__WASI_O_CREAT != 0 { + nix_flags.insert(OFlag::O_CREAT); + } + if oflags & wasi::__WASI_O_DIRECTORY != 0 { + nix_flags.insert(OFlag::O_DIRECTORY); + } + if oflags & wasi::__WASI_O_EXCL != 0 { + nix_flags.insert(OFlag::O_EXCL); + } + if oflags & wasi::__WASI_O_TRUNC != 0 { + nix_flags.insert(OFlag::O_TRUNC); + } + nix_flags +} + +pub(crate) fn filetype_from_nix(sflags: nix::sys::stat::SFlag) -> FileType { + use nix::sys::stat::SFlag; + if sflags.contains(SFlag::S_IFCHR) { + FileType::CharacterDevice + } else if sflags.contains(SFlag::S_IFBLK) { + FileType::BlockDevice + } else if sflags.contains(SFlag::S_IFSOCK) { + FileType::SocketStream + } else if sflags.contains(SFlag::S_IFDIR) { + FileType::Directory + } else if sflags.contains(SFlag::S_IFREG) { + FileType::RegularFile + } else if sflags.contains(SFlag::S_IFLNK) { + FileType::Symlink + } else { + FileType::Unknown + } +} + +pub(crate) fn filestat_from_nix( + filestat: nix::sys::stat::FileStat, +) -> Result { + use std::convert::TryFrom; + fn filestat_to_timestamp(secs: u64, nsecs: u64) -> Result { + secs.checked_mul(1_000_000_000) + .and_then(|sec_nsec| sec_nsec.checked_add(nsecs)) + .ok_or(Error::EOVERFLOW) + } + + let filetype = nix::sys::stat::SFlag::from_bits_truncate(filestat.st_mode); + let dev = wasi::__wasi_device_t::try_from(filestat.st_dev)?; + let ino = wasi::__wasi_inode_t::try_from(filestat.st_ino)?; + let st_atim = filestat_to_timestamp(filestat.st_atime as u64, filestat.st_atime_nsec as u64)?; + let st_ctim = filestat_to_timestamp(filestat.st_ctime as u64, filestat.st_ctime_nsec as u64)?; + let st_mtim = filestat_to_timestamp(filestat.st_mtime as u64, filestat.st_mtime_nsec as u64)?; + + Ok(wasi::__wasi_filestat_t { + st_dev: dev, + st_ino: ino, + st_nlink: filestat.st_nlink as wasi::__wasi_linkcount_t, + st_size: filestat.st_size as wasi::__wasi_filesize_t, + st_atim, + st_ctim, + st_mtim, + st_filetype: filetype_from_nix(filetype).to_wasi(), + }) +} + +pub(crate) fn dirent_filetype_from_host( + host_entry: &nix::libc::dirent, +) -> Result { + match host_entry.d_type { + libc::DT_FIFO => Ok(wasi::__WASI_FILETYPE_UNKNOWN), + libc::DT_CHR => Ok(wasi::__WASI_FILETYPE_CHARACTER_DEVICE), + libc::DT_DIR => Ok(wasi::__WASI_FILETYPE_DIRECTORY), + libc::DT_BLK => Ok(wasi::__WASI_FILETYPE_BLOCK_DEVICE), + libc::DT_REG => Ok(wasi::__WASI_FILETYPE_REGULAR_FILE), + libc::DT_LNK => Ok(wasi::__WASI_FILETYPE_SYMBOLIC_LINK), + libc::DT_SOCK => { + // TODO how to discriminate between STREAM and DGRAM? + // Perhaps, we should create a more general WASI filetype + // such as __WASI_FILETYPE_SOCKET, and then it would be + // up to the client to check whether it's actually + // STREAM or DGRAM? + Ok(wasi::__WASI_FILETYPE_UNKNOWN) + } + libc::DT_UNKNOWN => Ok(wasi::__WASI_FILETYPE_UNKNOWN), + _ => Err(Error::EINVAL), + } +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_EILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + helpers::path_from_slice(s.as_ref().as_bytes()).map(String::from) +} diff --git a/wasi-common/src/sys/unix/hostcalls_impl/fs.rs b/wasi-common/src/sys/unix/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..34d055632f --- /dev/null +++ b/wasi-common/src/sys/unix/hostcalls_impl/fs.rs @@ -0,0 +1,357 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use super::fs_helpers::*; +use crate::helpers::systemtime_to_timestamp; +use crate::hostcalls_impl::{FileType, PathGet}; +use crate::sys::host_impl; +use crate::sys::unix::str_to_cstring; +use crate::{wasi, Error, Result}; +use nix::libc; +use std::convert::TryInto; +use std::fs::{File, Metadata}; +use std::os::unix::fs::FileExt; +use std::os::unix::prelude::{AsRawFd, FromRawFd}; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub(crate) use super::super::linux::hostcalls_impl::*; + } else if #[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) use super::super::bsd::hostcalls_impl::*; + } +} + +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + file.read_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + file.write_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + use nix::fcntl::{fcntl, OFlag, F_GETFL}; + match fcntl(fd.as_raw_fd(), F_GETFL).map(OFlag::from_bits_truncate) { + Ok(flags) => Ok(host_impl::fdflags_from_nix(flags)), + Err(e) => Err(host_impl::errno_from_nix(e.as_errno().unwrap())), + } +} + +pub(crate) fn fd_fdstat_set_flags(fd: &File, fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + use nix::fcntl::{fcntl, F_SETFL}; + let nix_flags = host_impl::nix_from_fdflags(fdflags); + match fcntl(fd.as_raw_fd(), F_SETFL(nix_flags)) { + Ok(_) => Ok(()), + Err(e) => Err(host_impl::errno_from_nix(e.as_errno().unwrap())), + } +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + use nix::libc::mkdirat; + let path_cstr = str_to_cstring(resolved.path())?; + // nix doesn't expose mkdirat() yet + match unsafe { mkdirat(resolved.dirfd().as_raw_fd(), path_cstr.as_ptr(), 0o777) } { + 0 => Ok(()), + _ => Err(host_impl::errno_from_nix(nix::errno::Errno::last())), + } +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use nix::libc::linkat; + let old_path_cstr = str_to_cstring(resolved_old.path())?; + let new_path_cstr = str_to_cstring(resolved_new.path())?; + + // Not setting AT_SYMLINK_FOLLOW fails on most filesystems + let atflags = libc::AT_SYMLINK_FOLLOW; + let res = unsafe { + linkat( + resolved_old.dirfd().as_raw_fd(), + old_path_cstr.as_ptr(), + resolved_new.dirfd().as_raw_fd(), + new_path_cstr.as_ptr(), + atflags, + ) + }; + if res != 0 { + Err(host_impl::errno_from_nix(nix::errno::Errno::last())) + } else { + Ok(()) + } +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> Result { + use nix::errno::Errno; + use nix::fcntl::{openat, AtFlags, OFlag}; + use nix::sys::stat::{fstatat, Mode, SFlag}; + + let mut nix_all_oflags = if read && write { + OFlag::O_RDWR + } else if write { + OFlag::O_WRONLY + } else { + OFlag::O_RDONLY + }; + + // on non-Capsicum systems, we always want nofollow + nix_all_oflags.insert(OFlag::O_NOFOLLOW); + + // convert open flags + nix_all_oflags.insert(host_impl::nix_from_oflags(oflags)); + + // convert file descriptor flags + nix_all_oflags.insert(host_impl::nix_from_fdflags(fs_flags)); + + // Call openat. Use mode 0o666 so that we follow whatever the user's + // umask is, but don't set the executable flag, because it isn't yet + // meaningful for WASI programs to create executable files. + + log::debug!("path_open resolved = {:?}", resolved); + log::debug!("path_open oflags = {:?}", nix_all_oflags); + + let new_fd = match openat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + nix_all_oflags, + Mode::from_bits_truncate(0o666), + ) { + Ok(fd) => fd, + Err(e) => { + match e.as_errno() { + // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket + Some(Errno::ENXIO) => { + if let Ok(stat) = fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + if SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK) { + return Err(Error::ENOTSUP); + } else { + return Err(Error::ENXIO); + } + } else { + return Err(Error::ENXIO); + } + } + // Linux returns ENOTDIR instead of ELOOP when using O_NOFOLLOW|O_DIRECTORY + // on a symlink. + Some(Errno::ENOTDIR) + if !(nix_all_oflags & (OFlag::O_NOFOLLOW | OFlag::O_DIRECTORY)).is_empty() => + { + if let Ok(stat) = fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + if SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFLNK) { + return Err(Error::ELOOP); + } + } + return Err(Error::ENOTDIR); + } + // FreeBSD returns EMLINK instead of ELOOP when using O_NOFOLLOW on + // a symlink. + Some(Errno::EMLINK) if !(nix_all_oflags & OFlag::O_NOFOLLOW).is_empty() => { + return Err(Error::ELOOP); + } + Some(e) => return Err(host_impl::errno_from_nix(e)), + None => return Err(Error::ENOSYS), + } + } + }; + + log::debug!("path_open (host) new_fd = {:?}", new_fd); + + // Determine the type of the new file descriptor and which rights contradict with this type + Ok(unsafe { File::from_raw_fd(new_fd) }) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use nix::errno::Errno; + let path_cstr = str_to_cstring(resolved.path())?; + + // Linux requires that the buffer size is positive, whereas POSIX does not. + // Use a fake buffer to store the results if the size is zero. + // TODO: instead of using raw libc::readlinkat call here, this should really + // be fixed in `nix` crate + let fakebuf: &mut [u8] = &mut [0]; + let buf_len = buf.len(); + let len = unsafe { + libc::readlinkat( + resolved.dirfd().as_raw_fd(), + path_cstr.as_ptr() as *const libc::c_char, + if buf_len == 0 { + fakebuf.as_mut_ptr() + } else { + buf.as_mut_ptr() + } as *mut libc::c_char, + if buf_len == 0 { fakebuf.len() } else { buf_len }, + ) + }; + + if len < 0 { + Err(host_impl::errno_from_nix(Errno::last())) + } else { + let len = len as usize; + Ok(if len < buf_len { len } else { buf_len }) + } +} + +pub(crate) fn fd_filestat_get_impl(file: &std::fs::File) -> Result { + use std::os::unix::fs::MetadataExt; + + let metadata = file.metadata()?; + Ok(wasi::__wasi_filestat_t { + st_dev: metadata.dev(), + st_ino: metadata.ino(), + st_nlink: metadata.nlink().try_into()?, // u64 doesn't fit into u32 + st_size: metadata.len(), + st_atim: systemtime_to_timestamp(metadata.accessed()?)?, + st_ctim: metadata.ctime().try_into()?, // i64 doesn't fit into u64 + st_mtim: systemtime_to_timestamp(metadata.modified()?)?, + st_filetype: filetype(file, &metadata)?.to_wasi(), + }) +} + +fn filetype(file: &File, metadata: &Metadata) -> Result { + use nix::sys::socket::{self, SockType}; + use std::os::unix::fs::FileTypeExt; + let ftype = metadata.file_type(); + if ftype.is_file() { + Ok(FileType::RegularFile) + } else if ftype.is_dir() { + Ok(FileType::Directory) + } else if ftype.is_symlink() { + Ok(FileType::Symlink) + } else if ftype.is_char_device() { + Ok(FileType::CharacterDevice) + } else if ftype.is_block_device() { + Ok(FileType::BlockDevice) + } else if ftype.is_socket() { + match socket::getsockopt(file.as_raw_fd(), socket::sockopt::SockType) + .map_err(|err| err.as_errno().unwrap()) + .map_err(host_impl::errno_from_nix)? + { + SockType::Datagram => Ok(FileType::SocketDgram), + SockType::Stream => Ok(FileType::SocketStream), + _ => Ok(FileType::Unknown), + } + } else { + Ok(FileType::Unknown) + } +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + use nix::fcntl::AtFlags; + use nix::sys::stat::fstatat; + + let atflags = match dirflags { + 0 => AtFlags::empty(), + _ => AtFlags::AT_SYMLINK_NOFOLLOW, + }; + + let filestat = fstatat(resolved.dirfd().as_raw_fd(), resolved.path(), atflags) + .map_err(|err| host_impl::errno_from_nix(err.as_errno().unwrap()))?; + host_impl::filestat_from_nix(filestat) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use nix::sys::stat::{utimensat, UtimensatFlags}; + use nix::sys::time::{TimeSpec, TimeValLike}; + + // FIXME this should be a part of nix + fn timespec_omit() -> TimeSpec { + let raw_ts = libc::timespec { + tv_sec: 0, + tv_nsec: utime_omit(), + }; + unsafe { std::mem::transmute(raw_ts) } + }; + + fn timespec_now() -> TimeSpec { + let raw_ts = libc::timespec { + tv_sec: 0, + tv_nsec: utime_now(), + }; + unsafe { std::mem::transmute(raw_ts) } + }; + + let set_atim = fst_flags & wasi::__WASI_FILESTAT_SET_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FILESTAT_SET_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FILESTAT_SET_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FILESTAT_SET_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + + let atflags = match dirflags { + wasi::__WASI_LOOKUP_SYMLINK_FOLLOW => UtimensatFlags::FollowSymlink, + _ => UtimensatFlags::NoFollowSymlink, + }; + + let atim = if set_atim { + let st_atim = st_atim.try_into()?; + TimeSpec::nanoseconds(st_atim) + } else if set_atim_now { + timespec_now() + } else { + timespec_omit() + }; + + let mtim = if set_mtim { + let st_mtim = st_mtim.try_into()?; + TimeSpec::nanoseconds(st_mtim) + } else if set_mtim_now { + timespec_now() + } else { + timespec_omit() + }; + + let fd = resolved.dirfd().as_raw_fd().into(); + utimensat(fd, resolved.path(), &atim, &mtim, atflags).map_err(Into::into) +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + use nix::errno; + use nix::libc::{unlinkat, AT_REMOVEDIR}; + + let path_cstr = str_to_cstring(resolved.path())?; + + // nix doesn't expose unlinkat() yet + match unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + path_cstr.as_ptr(), + AT_REMOVEDIR, + ) + } { + 0 => Ok(()), + _ => Err(host_impl::errno_from_nix(errno::Errno::last())), + } +} diff --git a/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs b/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..5aa7f820b6 --- /dev/null +++ b/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,83 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::sys::host_impl; +use crate::{wasi, Result}; +use std::fs::File; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub(crate) use super::super::linux::fs_helpers::*; + } else if #[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) use super::super::bsd::fs_helpers::*; + } +} + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + use nix::fcntl::OFlag; + + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHT_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + let oflags = host_impl::nix_from_oflags(oflags); + if oflags.contains(OFlag::O_CREAT) { + needed_base |= wasi::__WASI_RIGHT_PATH_CREATE_FILE; + } + if oflags.contains(OFlag::O_TRUNC) { + needed_base |= wasi::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + let fdflags = host_impl::nix_from_fdflags(fs_flags); + if fdflags.contains(OFlag::O_DSYNC) { + needed_inheriting |= wasi::__WASI_RIGHT_FD_DATASYNC; + } + if fdflags.intersects(host_impl::O_RSYNC | OFlag::O_SYNC) { + needed_inheriting |= wasi::__WASI_RIGHT_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use nix::fcntl::{self, OFlag}; + use nix::sys::stat::Mode; + use std::os::unix::prelude::{AsRawFd, FromRawFd}; + + log::debug!("path_get openat path = {:?}", path); + + fcntl::openat( + dirfd.as_raw_fd(), + path, + OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW, + Mode::empty(), + ) + .map(|new_fd| unsafe { File::from_raw_fd(new_fd) }) + .map_err(Into::into) +} + +pub(crate) fn readlinkat(dirfd: &File, path: &str) -> Result { + use nix::fcntl; + use std::os::unix::prelude::AsRawFd; + + log::debug!("path_get readlinkat path = {:?}", path); + + let readlink_buf = &mut [0u8; libc::PATH_MAX as usize + 1]; + + fcntl::readlinkat(dirfd.as_raw_fd(), path, readlink_buf) + .map_err(Into::into) + .and_then(host_impl::path_from_host) +} diff --git a/wasi-common/src/sys/unix/hostcalls_impl/misc.rs b/wasi-common/src/sys/unix/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..98d81db8d1 --- /dev/null +++ b/wasi-common/src/sys/unix/hostcalls_impl/misc.rs @@ -0,0 +1,226 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::sys::host_impl; +use crate::{wasi, Error, Result}; +use nix::libc::{self, c_int}; +use std::mem::MaybeUninit; + +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + // convert the supported clocks to the libc types, or return EINVAL + let clock_id = match clock_id { + wasi::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, + wasi::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, + wasi::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, + wasi::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, + _ => return Err(Error::EINVAL), + }; + + // no `nix` wrapper for clock_getres, so we do it ourselves + let mut timespec = MaybeUninit::::uninit(); + let res = unsafe { libc::clock_getres(clock_id, timespec.as_mut_ptr()) }; + if res != 0 { + return Err(host_impl::errno_from_nix(nix::errno::Errno::last())); + } + let timespec = unsafe { timespec.assume_init() }; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; + // this is freelancing a bit from the spec but seems like it'll + // be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), |resolution| { + // a supported clock can never return zero; this case will probably never get hit, but + // make sure we follow the spec + if resolution == 0 { + Err(Error::EINVAL) + } else { + Ok(resolution) + } + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + // convert the supported clocks to the libc types, or return EINVAL + let clock_id = match clock_id { + wasi::__WASI_CLOCK_REALTIME => libc::CLOCK_REALTIME, + wasi::__WASI_CLOCK_MONOTONIC => libc::CLOCK_MONOTONIC, + wasi::__WASI_CLOCK_PROCESS_CPUTIME_ID => libc::CLOCK_PROCESS_CPUTIME_ID, + wasi::__WASI_CLOCK_THREAD_CPUTIME_ID => libc::CLOCK_THREAD_CPUTIME_ID, + _ => return Err(Error::EINVAL), + }; + + // no `nix` wrapper for clock_getres, so we do it ourselves + let mut timespec = MaybeUninit::::uninit(); + let res = unsafe { libc::clock_gettime(clock_id, timespec.as_mut_ptr()) }; + if res != 0 { + return Err(host_impl::errno_from_nix(nix::errno::Errno::last())); + } + let timespec = unsafe { timespec.assume_init() }; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit + // from the spec but seems like it'll be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), Ok) +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result<()> { + use nix::{ + errno::Errno, + poll::{poll, PollFd, PollFlags}, + }; + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + + if fd_events.is_empty() && timeout.is_none() { + return Ok(()); + } + + let mut poll_fds: Vec<_> = fd_events + .iter() + .map(|event| { + let mut flags = PollFlags::empty(); + match event.r#type { + wasi::__WASI_EVENTTYPE_FD_READ => flags.insert(PollFlags::POLLIN), + wasi::__WASI_EVENTTYPE_FD_WRITE => flags.insert(PollFlags::POLLOUT), + // An event on a file descriptor can currently only be of type FD_READ or FD_WRITE + // Nothing else has been defined in the specification, and these are also the only two + // events we filtered before. If we get something else here, the code has a serious bug. + _ => unreachable!(), + }; + PollFd::new(event.descriptor.as_raw_fd(), flags) + }) + .collect(); + + let poll_timeout = timeout.map_or(-1, |timeout| { + let delay = timeout.delay / 1_000_000; // poll syscall requires delay to expressed in milliseconds + delay.try_into().unwrap_or(c_int::max_value()) + }); + log::debug!("poll_oneoff poll_timeout = {:?}", poll_timeout); + + let ready = loop { + match poll(&mut poll_fds, poll_timeout) { + Err(_) => { + if Errno::last() == Errno::EINTR { + continue; + } + return Err(host_impl::errno_from_nix(Errno::last())); + } + Ok(ready) => break ready as usize, + } + }; + + Ok(if ready == 0 { + poll_oneoff_handle_timeout_event(timeout.expect("timeout should not be None"), events) + } else { + let ready_events = fd_events.into_iter().zip(poll_fds.into_iter()).take(ready); + poll_oneoff_handle_fd_event(ready_events, events)? + }) +} + +// define the `fionread()` function, equivalent to `ioctl(fd, FIONREAD, *bytes)` +nix::ioctl_read_bad!(fionread, nix::libc::FIONREAD, c_int); + +fn poll_oneoff_handle_timeout_event( + timeout: ClockEventData, + events: &mut Vec, +) { + events.push(wasi::__wasi_event_t { + userdata: timeout.userdata, + r#type: wasi::__WASI_EVENTTYPE_CLOCK, + error: wasi::__WASI_ESUCCESS, + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }, + }); +} + +fn poll_oneoff_handle_fd_event<'a>( + ready_events: impl Iterator, nix::poll::PollFd)>, + events: &mut Vec, +) -> Result<()> { + use nix::poll::PollFlags; + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + + for (fd_event, poll_fd) in ready_events { + log::debug!("poll_oneoff_handle_fd_event fd_event = {:?}", fd_event); + log::debug!("poll_oneoff_handle_fd_event poll_fd = {:?}", poll_fd); + + let revents = match poll_fd.revents() { + Some(revents) => revents, + None => continue, + }; + + log::debug!("poll_oneoff_handle_fd_event revents = {:?}", revents); + + let mut nbytes = 0; + if fd_event.r#type == wasi::__WASI_EVENTTYPE_FD_READ { + let _ = unsafe { fionread(fd_event.descriptor.as_raw_fd(), &mut nbytes) }; + } + + let output_event = if revents.contains(PollFlags::POLLNVAL) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + r#type: fd_event.r#type, + error: wasi::__WASI_EBADF, + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(PollFlags::POLLERR) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + r#type: fd_event.r#type, + error: wasi::__WASI_EIO, + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(PollFlags::POLLHUP) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + r#type: fd_event.r#type, + error: wasi::__WASI_ESUCCESS, + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENT_FD_READWRITE_HANGUP, + }, + }, + } + } else if revents.contains(PollFlags::POLLIN) | revents.contains(PollFlags::POLLOUT) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + r#type: fd_event.r#type, + error: wasi::__WASI_ESUCCESS, + u: wasi::__wasi_event_u { + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: nbytes.try_into()?, + flags: 0, + }, + }, + } + } else { + continue; + }; + + events.push(output_event); + } + + Ok(()) +} diff --git a/wasi-common/src/sys/unix/hostcalls_impl/mod.rs b/wasi-common/src/sys/unix/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..75f8df76c7 --- /dev/null +++ b/wasi-common/src/sys/unix/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Unix-specific hostcalls that implement +//! [WASI](https://github.com/CraneStation/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/wasi-common/src/sys/unix/linux/hostcalls_impl.rs b/wasi-common/src/sys/unix/linux/hostcalls_impl.rs new file mode 100644 index 0000000000..fd267738d4 --- /dev/null +++ b/wasi-common/src/sys/unix/linux/hostcalls_impl.rs @@ -0,0 +1,165 @@ +use super::super::dir::{Dir, Entry, SeekLoc}; +use super::osfile::OsFile; +use crate::hostcalls_impl::{Dirent, PathGet}; +use crate::sys::host_impl; +use crate::sys::unix::str_to_cstring; +use crate::{wasi, Error, Result}; +use log::trace; +use std::convert::TryInto; +use std::fs::File; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use nix::errno; + use nix::libc::unlinkat; + + let path_cstr = str_to_cstring(resolved.path())?; + + // nix doesn't expose unlinkat() yet + let res = unsafe { unlinkat(resolved.dirfd().as_raw_fd(), path_cstr.as_ptr(), 0) }; + if res == 0 { + Ok(()) + } else { + Err(host_impl::errno_from_nix(errno::Errno::last())) + } +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use nix::{errno::Errno, libc::symlinkat}; + + let old_path_cstr = str_to_cstring(old_path)?; + let new_path_cstr = str_to_cstring(resolved.path())?; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + let res = unsafe { + symlinkat( + old_path_cstr.as_ptr(), + resolved.dirfd().as_raw_fd(), + new_path_cstr.as_ptr(), + ) + }; + if res != 0 { + Err(host_impl::errno_from_nix(Errno::last())) + } else { + Ok(()) + } +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use nix::libc::renameat; + let old_path_cstr = str_to_cstring(resolved_old.path())?; + let new_path_cstr = str_to_cstring(resolved_new.path())?; + + let res = unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + old_path_cstr.as_ptr(), + resolved_new.dirfd().as_raw_fd(), + new_path_cstr.as_ptr(), + ) + }; + if res != 0 { + Err(host_impl::errno_from_nix(nix::errno::Errno::last())) + } else { + Ok(()) + } +} + +pub(crate) fn fd_readdir_impl( + fd: &File, + cookie: wasi::__wasi_dircookie_t, +) -> Result>> { + // We need to duplicate the fd, because `opendir(3)`: + // After a successful call to fdopendir(), fd is used internally by the implementation, + // and should not otherwise be used by the application. + // `opendir(3p)` also says that it's undefined behavior to + // modify the state of the fd in a different way than by accessing DIR*. + // + // Still, rewinddir will be needed because the two file descriptors + // share progress. But we can safely execute closedir now. + let fd = fd.try_clone()?; + let mut dir = Dir::from(fd)?; + + // Seek if needed. Unless cookie is wasi::__WASI_DIRCOOKIE_START, + // new items may not be returned to the caller. + // + // According to `opendir(3p)`: + // If a file is removed from or added to the directory after the most recent call + // to opendir() or rewinddir(), whether a subsequent call to readdir() returns an entry + // for that file is unspecified. + if cookie == wasi::__WASI_DIRCOOKIE_START { + trace!(" | fd_readdir: doing rewinddir"); + dir.rewind(); + } else { + trace!(" | fd_readdir: doing seekdir to {}", cookie); + let loc = unsafe { SeekLoc::from_raw(cookie as i64) }; + dir.seek(loc); + } + + Ok(dir.into_iter().map(|entry| { + let entry: Entry = entry?; + Ok(Dirent { + name: entry // TODO can we reuse path_from_host for CStr? + .file_name() + .to_str()? + .to_owned(), + ino: entry.ino(), + ftype: entry.file_type().into(), + cookie: entry.seek_loc().to_raw().try_into()?, + }) + })) +} + +// This should actually be common code with Windows, +// but there's BSD stuff remaining +pub(crate) fn fd_readdir( + os_file: &mut OsFile, + mut host_buf: &mut [u8], + cookie: wasi::__wasi_dircookie_t, +) -> Result { + let iter = fd_readdir_impl(os_file, cookie)?; + let mut used = 0; + for dirent in iter { + let dirent_raw = dirent?.to_wasi_raw()?; + let offset = dirent_raw.len(); + if host_buf.len() < offset { + break; + } else { + host_buf[0..offset].copy_from_slice(&dirent_raw); + used += offset; + host_buf = &mut host_buf[offset..]; + } + } + + trace!(" | *buf_used={:?}", used); + Ok(used) +} + +pub(crate) fn fd_advise( + file: &File, + advice: wasi::__wasi_advice_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + { + use nix::fcntl::{posix_fadvise, PosixFadviseAdvice}; + + let offset = offset.try_into()?; + let len = len.try_into()?; + let host_advice = match advice { + wasi::__WASI_ADVICE_DONTNEED => PosixFadviseAdvice::POSIX_FADV_DONTNEED, + wasi::__WASI_ADVICE_SEQUENTIAL => PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL, + wasi::__WASI_ADVICE_WILLNEED => PosixFadviseAdvice::POSIX_FADV_WILLNEED, + wasi::__WASI_ADVICE_NOREUSE => PosixFadviseAdvice::POSIX_FADV_NOREUSE, + wasi::__WASI_ADVICE_RANDOM => PosixFadviseAdvice::POSIX_FADV_RANDOM, + wasi::__WASI_ADVICE_NORMAL => PosixFadviseAdvice::POSIX_FADV_NORMAL, + _ => return Err(Error::EINVAL), + }; + + posix_fadvise(file.as_raw_fd(), offset, len, host_advice)?; + } + + Ok(()) +} diff --git a/wasi-common/src/sys/unix/linux/mod.rs b/wasi-common/src/sys/unix/linux/mod.rs new file mode 100644 index 0000000000..e4ccc91728 --- /dev/null +++ b/wasi-common/src/sys/unix/linux/mod.rs @@ -0,0 +1,42 @@ +pub(crate) mod hostcalls_impl; +pub(crate) mod osfile; + +pub(crate) mod fdentry_impl { + use crate::{sys::host_impl, Result}; + use std::os::unix::prelude::AsRawFd; + + pub(crate) unsafe fn isatty(fd: &impl AsRawFd) -> Result { + use nix::errno::Errno; + + let res = libc::isatty(fd.as_raw_fd()); + if res == 1 { + // isatty() returns 1 if fd is an open file descriptor referring to a terminal... + Ok(true) + } else { + // ... otherwise 0 is returned, and errno is set to indicate the error. + match Errno::last() { + // While POSIX specifies ENOTTY if the passed + // fd is *not* a tty, on Linux, some implementations + // may return EINVAL instead. + // + // https://linux.die.net/man/3/isatty + Errno::ENOTTY | Errno::EINVAL => Ok(false), + x => Err(host_impl::errno_from_nix(x)), + } + } + } +} + +pub(crate) mod host_impl { + pub(crate) const O_RSYNC: nix::fcntl::OFlag = nix::fcntl::OFlag::O_RSYNC; +} + +pub(crate) mod fs_helpers { + pub(crate) fn utime_now() -> libc::c_long { + libc::UTIME_NOW + } + + pub(crate) fn utime_omit() -> libc::c_long { + libc::UTIME_OMIT + } +} diff --git a/wasi-common/src/sys/unix/linux/osfile.rs b/wasi-common/src/sys/unix/linux/osfile.rs new file mode 100644 index 0000000000..b92a9793a6 --- /dev/null +++ b/wasi-common/src/sys/unix/linux/osfile.rs @@ -0,0 +1,32 @@ +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; + +#[derive(Debug)] +pub(crate) struct OsFile(fs::File); + +impl From for OsFile { + fn from(file: fs::File) -> Self { + Self(file) + } +} + +impl AsRawFd for OsFile { + fn as_raw_fd(&self) -> RawFd { + self.0.as_raw_fd() + } +} + +impl Deref for OsFile { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/wasi-common/src/sys/unix/mod.rs b/wasi-common/src/sys/unix/mod.rs new file mode 100644 index 0000000000..91892df647 --- /dev/null +++ b/wasi-common/src/sys/unix/mod.rs @@ -0,0 +1,38 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +mod dir; + +#[cfg(any( + target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly" +))] +mod bsd; +#[cfg(target_os = "linux")] +mod linux; + +use crate::{Error, Result}; +use std::ffi::CString; +use std::fs::{File, OpenOptions}; +use std::path::Path; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("/dev/null") + .map_err(Into::into) +} + +pub(crate) fn str_to_cstring(s: &str) -> Result { + CString::new(s.as_bytes()).map_err(|_| Error::EILSEQ) +} + +pub fn preopen_dir>(path: P) -> Result { + File::open(path).map_err(Into::into) +} diff --git a/wasi-common/src/sys/windows/fdentry_impl.rs b/wasi-common/src/sys/windows/fdentry_impl.rs new file mode 100644 index 0000000000..7e8bda0567 --- /dev/null +++ b/wasi-common/src/sys/windows/fdentry_impl.rs @@ -0,0 +1,129 @@ +use crate::fdentry::Descriptor; +use crate::{wasi, Error, Result}; +use std::fs::File; +use std::io; +use std::ops::{Deref, DerefMut}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle, RawHandle}; + +#[derive(Debug)] +pub(crate) struct OsFile(File); + +impl From for OsFile { + fn from(file: File) -> Self { + Self(file) + } +} + +impl AsRawHandle for OsFile { + fn as_raw_handle(&self) -> RawHandle { + self.0.as_raw_handle() + } +} + +impl Deref for OsFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRawHandle for Descriptor { + fn as_raw_handle(&self) -> RawHandle { + match self { + Self::OsFile(file) => file.as_raw_handle(), + Self::Stdin => io::stdin().as_raw_handle(), + Self::Stdout => io::stdout().as_raw_handle(), + Self::Stderr => io::stderr().as_raw_handle(), + } + } +} + +/// This function is unsafe because it operates on a raw file handle. +pub(crate) unsafe fn determine_type_and_access_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + use winx::file::{get_file_access_mode, AccessMode}; + + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(handle)?; + + match file_type { + wasi::__WASI_FILETYPE_DIRECTORY | wasi::__WASI_FILETYPE_REGULAR_FILE => { + let mode = get_file_access_mode(handle.as_raw_handle())?; + if mode.contains(AccessMode::FILE_GENERIC_READ) { + rights_base |= wasi::__WASI_RIGHT_FD_READ; + } + if mode.contains(AccessMode::FILE_GENERIC_WRITE) { + rights_base |= wasi::__WASI_RIGHT_FD_WRITE; + } + } + _ => { + // TODO: is there a way around this? On windows, it seems + // we cannot check access rights for anything but dirs and regular files + } + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// This function is unsafe because it operates on a raw file handle. +pub(crate) unsafe fn determine_type_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + let file_type = winx::file::get_file_type(handle.as_raw_handle())?; + if file_type.is_char() { + // character file: LPT device or console + // TODO: rule out LPT device + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else if file_type.is_disk() { + // disk file: file, dir or disk device + let file = std::mem::ManuallyDrop::new(File::from_raw_handle(handle.as_raw_handle())); + let meta = file.metadata().map_err(|_| Error::EINVAL)?; + if meta.is_dir() { + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if meta.is_file() { + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + } else if file_type.is_pipe() { + // pipe object: socket, named pipe or anonymous pipe + // TODO: what about pipes, etc? + ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + }; + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/wasi-common/src/sys/windows/host_impl.rs b/wasi-common/src/sys/windows/host_impl.rs new file mode 100644 index 0000000000..18cf4a4adb --- /dev/null +++ b/wasi-common/src/sys/windows/host_impl.rs @@ -0,0 +1,108 @@ +//! WASI host types specific to Windows host. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused)] +use crate::{wasi, Error, Result}; +use std::ffi::OsStr; +use std::fs::OpenOptions; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::fs::OpenOptionsExt; +use winx::file::{AccessMode, Attributes, CreationDisposition, Flags}; + +pub(crate) fn errno_from_win(error: winx::winerror::WinError) -> wasi::__wasi_errno_t { + // TODO: implement error mapping between Windows and WASI + use winx::winerror::WinError::*; + match error { + ERROR_SUCCESS => wasi::__WASI_ESUCCESS, + ERROR_BAD_ENVIRONMENT => wasi::__WASI_E2BIG, + ERROR_FILE_NOT_FOUND => wasi::__WASI_ENOENT, + ERROR_PATH_NOT_FOUND => wasi::__WASI_ENOENT, + ERROR_TOO_MANY_OPEN_FILES => wasi::__WASI_ENFILE, + ERROR_ACCESS_DENIED => wasi::__WASI_EACCES, + ERROR_SHARING_VIOLATION => wasi::__WASI_EACCES, + ERROR_PRIVILEGE_NOT_HELD => wasi::__WASI_ENOTCAPABLE, // TODO is this the correct mapping? + ERROR_INVALID_HANDLE => wasi::__WASI_EBADF, + ERROR_INVALID_NAME => wasi::__WASI_ENOENT, + ERROR_NOT_ENOUGH_MEMORY => wasi::__WASI_ENOMEM, + ERROR_OUTOFMEMORY => wasi::__WASI_ENOMEM, + ERROR_DIR_NOT_EMPTY => wasi::__WASI_ENOTEMPTY, + ERROR_NOT_READY => wasi::__WASI_EBUSY, + ERROR_BUSY => wasi::__WASI_EBUSY, + ERROR_NOT_SUPPORTED => wasi::__WASI_ENOTSUP, + ERROR_FILE_EXISTS => wasi::__WASI_EEXIST, + ERROR_BROKEN_PIPE => wasi::__WASI_EPIPE, + ERROR_BUFFER_OVERFLOW => wasi::__WASI_ENAMETOOLONG, + ERROR_NOT_A_REPARSE_POINT => wasi::__WASI_EINVAL, + ERROR_NEGATIVE_SEEK => wasi::__WASI_EINVAL, + ERROR_DIRECTORY => wasi::__WASI_ENOTDIR, + ERROR_ALREADY_EXISTS => wasi::__WASI_EEXIST, + _ => wasi::__WASI_ENOTSUP, + } +} + +pub(crate) fn fdflags_from_win(mode: AccessMode) -> wasi::__wasi_fdflags_t { + let mut fdflags = 0; + // TODO verify this! + if mode.contains(AccessMode::FILE_APPEND_DATA) { + fdflags |= wasi::__WASI_FDFLAG_APPEND; + } + if mode.contains(AccessMode::SYNCHRONIZE) { + fdflags |= wasi::__WASI_FDFLAG_DSYNC; + fdflags |= wasi::__WASI_FDFLAG_RSYNC; + fdflags |= wasi::__WASI_FDFLAG_SYNC; + } + // The NONBLOCK equivalent is FILE_FLAG_OVERLAPPED + // but it seems winapi doesn't provide a mechanism + // for checking whether the handle supports async IO. + // On the contrary, I've found some dicsussion online + // which suggests that on Windows all handles should + // generally be assumed to be opened with async support + // and then the program should fallback should that **not** + // be the case at the time of the operation. + // TODO: this requires further investigation + fdflags +} + +pub(crate) fn win_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> (AccessMode, Flags) { + let mut access_mode = AccessMode::empty(); + let mut flags = Flags::empty(); + + // TODO verify this! + if fdflags & wasi::__WASI_FDFLAG_NONBLOCK != 0 { + flags.insert(Flags::FILE_FLAG_OVERLAPPED); + } + if fdflags & wasi::__WASI_FDFLAG_APPEND != 0 { + access_mode.insert(AccessMode::FILE_APPEND_DATA); + } + if fdflags & wasi::__WASI_FDFLAG_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAG_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAG_SYNC != 0 + { + access_mode.insert(AccessMode::SYNCHRONIZE); + } + + (access_mode, flags) +} + +pub(crate) fn win_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition { + if oflags & wasi::__WASI_O_CREAT != 0 { + if oflags & wasi::__WASI_O_EXCL != 0 { + CreationDisposition::CREATE_NEW + } else { + CreationDisposition::CREATE_ALWAYS + } + } else if oflags & wasi::__WASI_O_TRUNC != 0 { + CreationDisposition::TRUNCATE_EXISTING + } else { + CreationDisposition::OPEN_EXISTING + } +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_EILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + let vec: Vec = s.as_ref().encode_wide().collect(); + String::from_utf16(&vec).map_err(|_| Error::EILSEQ) +} diff --git a/wasi-common/src/sys/windows/hostcalls_impl/fs.rs b/wasi-common/src/sys/windows/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..cba0edc623 --- /dev/null +++ b/wasi-common/src/sys/windows/hostcalls_impl/fs.rs @@ -0,0 +1,550 @@ +#![allow(non_camel_case_types)] +#![allow(unused)] +use super::fs_helpers::*; +use crate::ctx::WasiCtx; +use crate::fdentry::FdEntry; +use crate::helpers::systemtime_to_timestamp; +use crate::hostcalls_impl::{fd_filestat_set_times_impl, Dirent, FileType, PathGet}; +use crate::sys::fdentry_impl::{determine_type_rights, OsFile}; +use crate::sys::host_impl::{self, path_from_host}; +use crate::sys::hostcalls_impl::fs_helpers::PathGetExt; +use crate::{wasi, Error, Result}; +use log::{debug, trace}; +use std::convert::TryInto; +use std::fs::{File, Metadata, OpenOptions}; +use std::io::{self, Seek, SeekFrom}; +use std::os::windows::fs::{FileExt, OpenOptionsExt}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle}; +use std::path::{Path, PathBuf}; +use winx::file::{AccessMode, Flags}; + +fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek read by a specified offset + let nread = file.seek_read(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nread) +} + +fn write_at(mut file: &File, buf: &[u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek write by a specified offset + let nwritten = file.seek_write(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nwritten) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + read_at(file, buf, offset).map_err(Into::into) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + write_at(file, buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + use winx::file::AccessMode; + unsafe { winx::file::get_file_access_mode(fd.as_raw_handle()) } + .map(host_impl::fdflags_from_win) + .map_err(Into::into) +} + +pub(crate) fn fd_fdstat_set_flags(fd: &File, fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + unimplemented!("fd_fdstat_set_flags") +} + +pub(crate) fn fd_advise( + _file: &File, + advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, +) -> Result<()> { + match advice { + wasi::__WASI_ADVICE_DONTNEED + | wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => {} + _ => return Err(Error::EINVAL), + } + + Ok(()) +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::create_dir(&path).map_err(Into::into) +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + unimplemented!("path_link") +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result { + use winx::file::{AccessMode, CreationDisposition, Flags}; + + let mut access_mode = AccessMode::READ_CONTROL; + if read { + access_mode.insert(AccessMode::FILE_GENERIC_READ); + } + if write { + access_mode.insert(AccessMode::FILE_GENERIC_WRITE); + } + + let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS; + + // convert open flags + let mut opts = OpenOptions::new(); + match host_impl::win_from_oflags(oflags) { + CreationDisposition::CREATE_ALWAYS => { + opts.create(true).append(true); + } + CreationDisposition::CREATE_NEW => { + opts.create_new(true).write(true); + } + CreationDisposition::TRUNCATE_EXISTING => { + opts.truncate(true); + } + _ => {} + } + + // convert file descriptor flags + let (add_access_mode, add_flags) = host_impl::win_from_fdflags(fdflags); + access_mode.insert(add_access_mode); + flags.insert(add_flags); + + let path = resolved.concatenate()?; + + match path.symlink_metadata().map(|metadata| metadata.file_type()) { + Ok(file_type) => { + // check if we are trying to open a symlink + if file_type.is_symlink() { + return Err(Error::ELOOP); + } + // check if we are trying to open a file as a dir + if file_type.is_file() && oflags & wasi::__WASI_O_DIRECTORY != 0 { + return Err(Error::ENOTDIR); + } + } + Err(e) => match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + log::debug!("path_open at symlink_metadata error code={:?}", e); + let e = WinError::from_u32(e as u32); + + if e != WinError::ERROR_FILE_NOT_FOUND { + return Err(e.into()); + } + // file not found, let it proceed to actually + // trying to open it + } + None => { + log::debug!("Inconvertible OS error: {}", e); + return Err(Error::EIO); + } + }, + } + + opts.access_mode(access_mode.bits()) + .custom_flags(flags.bits()) + .open(&path) + .map_err(Into::into) +} + +fn dirent_from_path>( + path: P, + name: &str, + cookie: wasi::__wasi_dircookie_t, +) -> Result { + let path = path.as_ref(); + trace!("dirent_from_path: opening {}", path.to_string_lossy()); + + // To open a directory on Windows, FILE_FLAG_BACKUP_SEMANTICS flag must be used + let file = OpenOptions::new() + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .read(true) + .open(path)?; + let ty = file.metadata()?.file_type(); + Ok(Dirent { + ftype: filetype_from_std(&ty), + name: name.to_owned(), + cookie, + ino: file_serial_no(&file)?, + }) +} + +// On Windows there is apparently no support for seeking the directory stream in the OS. +// cf. https://github.com/WebAssembly/WASI/issues/61 +// +// The implementation here may perform in O(n^2) if the host buffer is O(1) +// and the number of directory entries is O(n). +// TODO: Add a heuristic optimization to achieve O(n) time in the most common case +// where fd_readdir is resumed where it previously finished +// +// Correctness of this approach relies upon one assumption: that the order of entries +// returned by `FindNextFileW` is stable, i.e. doesn't change if the directory +// contents stay the same. This invariant is crucial to be able to implement +// any kind of seeking whatsoever without having to read the whole directory at once +// and then return the data from cache. (which leaks memory) +// +// The MSDN documentation explicitly says that the order in which the search returns the files +// is not guaranteed, and is dependent on the file system. +// cf. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew +// +// This stackoverflow post suggests that `FindNextFileW` is indeed stable and that +// the order of directory entries depends **only** on the filesystem used, but the +// MSDN documentation is not clear about this. +// cf. https://stackoverflow.com/questions/47380739/is-findfirstfile-and-findnextfile-order-random-even-for-dvd +// +// Implementation details: +// Cookies for the directory entries start from 1. (0 is reserved by wasi::__WASI_DIRCOOKIE_START) +// . gets cookie = 1 +// .. gets cookie = 2 +// other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies +pub(crate) fn fd_readdir_impl( + fd: &File, + cookie: wasi::__wasi_dircookie_t, +) -> Result>> { + use winx::file::get_file_path; + + let cookie = cookie.try_into()?; + let path = get_file_path(fd)?; + // std::fs::ReadDir doesn't return . and .., so we need to emulate it + let path = Path::new(&path); + // The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too + let parent = path.parent().unwrap_or(path); + let dot = dirent_from_path(path, ".", 1)?; + let dotdot = dirent_from_path(parent, "..", 2)?; + + trace!(" | fd_readdir impl: executing std::fs::ReadDir"); + let iter = path.read_dir()?.zip(3..).map(|(dir, no)| { + let dir: std::fs::DirEntry = dir?; + + Ok(Dirent { + name: path_from_host(dir.file_name())?, + ftype: filetype_from_std(&dir.file_type()?), + ino: File::open(dir.path()).and_then(|f| file_serial_no(&f))?, + cookie: no, + }) + }); + + // into_iter for arrays is broken and returns references instead of values, + // so we need to use vec![...] and do heap allocation + // See https://github.com/rust-lang/rust/issues/25725 + let iter = vec![dot, dotdot].into_iter().map(Ok).chain(iter); + + // Emulate seekdir(). This may give O(n^2) complexity if used with a + // small host_buf, but this is difficult to implement efficiently. + // + // See https://github.com/WebAssembly/WASI/issues/61 for more details. + Ok(iter.skip(cookie)) +} + +// This should actually be common code with Linux +pub(crate) fn fd_readdir( + os_file: &mut OsFile, + mut host_buf: &mut [u8], + cookie: wasi::__wasi_dircookie_t, +) -> Result { + let iter = fd_readdir_impl(os_file, cookie)?; + let mut used = 0; + for dirent in iter { + let dirent_raw = dirent?.to_wasi_raw()?; + let offset = dirent_raw.len(); + if host_buf.len() < offset { + break; + } else { + host_buf[0..offset].copy_from_slice(&dirent_raw); + used += offset; + host_buf = &mut host_buf[offset..]; + } + } + + trace!(" | *buf_used={:?}", used); + Ok(used) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use winx::file::get_file_path; + + let path = resolved.concatenate()?; + let target_path = path.read_link()?; + + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(resolved.dirfd())?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + let target_path = target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ))?; + + if buf.len() > 0 { + let mut chars = target_path.chars(); + let mut nread = 0usize; + + for i in 0..buf.len() { + match chars.next() { + Some(ch) => { + buf[i] = ch as u8; + nread += 1; + } + None => break, + } + } + + Ok(nread) + } else { + Ok(0) + } +} + +fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result> { + if resolved.path().ends_with('/') { + let suffix = resolved.path().trim_end_matches('/'); + concatenate(resolved.dirfd(), Path::new(suffix)).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use std::fs; + + let old_path = resolved_old.concatenate()?; + let new_path = resolved_new.concatenate()?; + + // First sanity check: check we're not trying to rename dir to file or vice versa. + // NB on Windows, the former is actually permitted [std::fs::rename]. + // + // [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html + if old_path.is_dir() && new_path.is_file() { + return Err(Error::ENOTDIR); + } + // Second sanity check: check we're not trying to rename a file into a path + // ending in a trailing slash. + if old_path.is_file() && resolved_new.path().ends_with('/') { + return Err(Error::ENOTDIR); + } + + // TODO handle symlinks + + fs::rename(&old_path, &new_path).or_else(|e| match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + + log::debug!("path_rename at rename error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // So most likely dealing with new_path == dir. + // Eliminate case old_path == file first. + if old_path.is_file() { + Err(Error::EISDIR) + } else { + // Ok, let's try removing an empty dir at new_path if it exists + // and is a nonempty dir. + fs::remove_dir(&new_path) + .and_then(|()| fs::rename(old_path, new_path)) + .map_err(Into::into) + } + } + WinError::ERROR_INVALID_NAME => { + // If source contains trailing slashes, check if we are dealing with + // a file instead of a dir, and if so, throw ENOTDIR. + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? { + if path.is_file() { + return Err(Error::ENOTDIR); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }) +} + +pub(crate) fn num_hardlinks(file: &File, _metadata: &Metadata) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.nNumberOfLinks.into()) +} + +pub(crate) fn device_id(file: &File, _metadata: &Metadata) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into()) +} + +pub(crate) fn file_serial_no(file: &File) -> io::Result { + let info = winx::file::get_fileinfo(file)?; + let high = info.nFileIndexHigh; + let low = info.nFileIndexLow; + let no = ((high as u64) << 32) | (low as u64); + Ok(no) +} + +pub(crate) fn change_time(file: &File, _metadata: &Metadata) -> io::Result { + winx::file::change_time(file) +} + +pub(crate) fn fd_filestat_get_impl(file: &std::fs::File) -> Result { + let metadata = file.metadata()?; + Ok(wasi::__wasi_filestat_t { + st_dev: device_id(file, &metadata)?, + st_ino: file_serial_no(file)?, + st_nlink: num_hardlinks(file, &metadata)?.try_into()?, // u64 doesn't fit into u32 + st_size: metadata.len(), + st_atim: systemtime_to_timestamp(metadata.accessed()?)?, + st_ctim: change_time(file, &metadata)?.try_into()?, // i64 doesn't fit into u64 + st_mtim: systemtime_to_timestamp(metadata.modified()?)?, + st_filetype: filetype_from_std(&metadata.file_type()).to_wasi(), + }) +} + +pub(crate) fn filetype_from_std(ftype: &std::fs::FileType) -> FileType { + if ftype.is_file() { + FileType::RegularFile + } else if ftype.is_dir() { + FileType::Directory + } else if ftype.is_symlink() { + FileType::Symlink + } else { + FileType::Unknown + } +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + let path = resolved.concatenate()?; + let file = File::open(path)?; + fd_filestat_get_impl(&file) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + mut st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use winx::file::AccessMode; + let path = resolved.concatenate()?; + let file = OpenOptions::new() + .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits()) + .open(path)?; + fd_filestat_set_times_impl(&file, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + use winx::winerror::WinError; + + let old_path = concatenate(resolved.dirfd(), Path::new(old_path))?; + let new_path = resolved.concatenate()?; + + // try creating a file symlink + symlink_file(&old_path, &new_path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_symlink at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_NOT_A_REPARSE_POINT => { + // try creating a dir symlink instead + symlink_dir(old_path, new_path).map_err(Into::into) + } + WinError::ERROR_ACCESS_DENIED => { + // does the target exist? + if new_path.exists() { + Err(Error::EEXIST) + } else { + Err(WinError::ERROR_ACCESS_DENIED.into()) + } + } + WinError::ERROR_INVALID_NAME => { + // does the target without trailing slashes exist? + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? { + if path.exists() { + return Err(Error::EEXIST); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) +} + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use std::fs; + use winx::winerror::WinError; + + let path = resolved.concatenate()?; + let file_type = path + .symlink_metadata() + .map(|metadata| metadata.file_type())?; + + // check if we're unlinking a symlink + // NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt] + // stabilises + // + // [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html + if file_type.is_symlink() { + fs::remove_file(&path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_unlink_file at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // try unlinking a dir symlink instead + fs::remove_dir(path).map_err(Into::into) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) + } else if file_type.is_dir() { + Err(Error::EISDIR) + } else if file_type.is_file() { + fs::remove_file(path).map_err(Into::into) + } else { + Err(Error::EINVAL) + } +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::remove_dir(&path).map_err(Into::into) +} diff --git a/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs b/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..ddbfed5e20 --- /dev/null +++ b/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,149 @@ +#![allow(non_camel_case_types)] +use crate::hostcalls_impl::PathGet; +use crate::{wasi, Error, Result}; +use std::ffi::{OsStr, OsString}; +use std::fs::File; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; + +pub(crate) trait PathGetExt { + fn concatenate(&self) -> Result; +} + +impl PathGetExt for PathGet { + fn concatenate(&self) -> Result { + concatenate(self.dirfd(), Path::new(self.path())) + } +} + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHT_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + if oflags & wasi::__WASI_O_CREAT != 0 { + needed_base |= wasi::__WASI_RIGHT_PATH_CREATE_FILE; + } else if oflags & wasi::__WASI_O_TRUNC != 0 { + needed_base |= wasi::__WASI_RIGHT_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + if fdflags & wasi::__WASI_FDFLAG_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAG_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAG_SYNC != 0 + { + needed_inheriting |= wasi::__WASI_RIGHT_FD_DATASYNC; + needed_inheriting |= wasi::__WASI_RIGHT_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winx::file::Flags; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(path))?; + OpenOptions::new() + .read(true) + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .open(&path) + .map_err(|e| match e.raw_os_error() { + Some(e) => { + log::debug!("openat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => Error::ENOTDIR, + e => e.into(), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Error::EIO + } + }) +} + +pub(crate) fn readlinkat(dirfd: &File, s_path: &str) -> Result { + use winx::file::get_file_path; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(s_path))?; + match path.read_link() { + Ok(target_path) => { + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(dirfd)?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ)) + } + Err(e) => match e.raw_os_error() { + Some(e) => { + log::debug!("readlinkat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => { + if s_path.ends_with('/') { + // strip "/" and check if exists + let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?; + if path.exists() && !path.is_dir() { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }, + } +} + +pub(crate) fn strip_extended_prefix>(path: P) -> OsString { + let path: Vec = path.as_ref().encode_wide().collect(); + if &[92, 92, 63, 92] == &path[0..4] { + OsString::from_wide(&path[4..]) + } else { + OsString::from_wide(&path) + } +} + +pub(crate) fn concatenate>(dirfd: &File, path: P) -> Result { + use winx::file::get_file_path; + + // WASI is not able to deal with absolute paths + // so error out if absolute + if path.as_ref().is_absolute() { + return Err(Error::ENOTCAPABLE); + } + + let dir_path = get_file_path(dirfd)?; + // concatenate paths + let mut out_path = PathBuf::from(dir_path); + out_path.push(path.as_ref()); + // strip extended prefix; otherwise we will error out on any relative + // components with `out_path` + let out_path = PathBuf::from(strip_extended_prefix(out_path)); + + log::debug!("out_path={:?}", out_path); + + Ok(out_path) +} diff --git a/wasi-common/src/sys/windows/hostcalls_impl/misc.rs b/wasi-common/src/sys/windows/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..b77503f1f3 --- /dev/null +++ b/wasi-common/src/sys/windows/hostcalls_impl/misc.rs @@ -0,0 +1,119 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +#![allow(unused)] +use crate::helpers::systemtime_to_timestamp; +use crate::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::memory::*; +use crate::sys::host_impl; +use crate::{wasi, wasi32, Error, Result}; +use cpu_time::{ProcessTime, ThreadTime}; +use lazy_static::lazy_static; +use std::convert::TryInto; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +lazy_static! { + static ref START_MONOTONIC: Instant = Instant::now(); + static ref PERF_COUNTER_RES: u64 = get_perf_counter_resolution_ns(); +} + +// Timer resolution on Windows is really hard. We may consider exposing the resolution of the respective +// timers as an associated function in the future. +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + Ok(match clock_id { + // This is the best that we can do with std::time::SystemTime. + // Rust uses GetSystemTimeAsFileTime, which is said to have the resolution of + // 10ms or 55ms, [1] but MSDN doesn't confirm this in any way. + // Even the MSDN article on high resolution timestamps doesn't even mention the precision + // for this method. [3] + // + // The timer resolution can be queried using one of the functions: [2, 5] + // * NtQueryTimerResolution, which is undocumented and thus not exposed by the winapi crate + // * timeGetDevCaps, which returns the upper and lower bound for the precision, in ms. + // While the upper bound seems like something we could use, it's typically too high to be meaningful. + // For instance, the intervals return by the syscall are: + // * [1, 65536] on Wine + // * [1, 1000000] on Windows 10, which is up to (sic) 1000 seconds. + // + // It's possible to manually set the timer resolution, but this sounds like something which should + // only be done temporarily. [5] + // + // Alternatively, we could possibly use GetSystemTimePreciseAsFileTime in clock_time_get, but + // this syscall is only available starting from Windows 8. + // (we could possibly emulate it on earlier versions of Windows, see [4]) + // The MSDN are not clear on the resolution of GetSystemTimePreciseAsFileTime either, but a + // Microsoft devblog entry [1] suggests that it kind of combines GetSystemTimeAsFileTime with + // QueryPeformanceCounter, which probably means that those two should have the same resolution. + // + // See also this discussion about the use of GetSystemTimePreciseAsFileTime in Python stdlib, + // which in particular contains some resolution benchmarks. + // + // [1] https://devblogs.microsoft.com/oldnewthing/20170921-00/?p=97057 + // [2] http://www.windowstimestamp.com/description + // [3] https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN + // [4] https://www.codeproject.com/Tips/1011902/High-Resolution-Time-For-Windows + // [5] https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly + // [6] https://bugs.python.org/issue19007 + wasi::__WASI_CLOCK_REALTIME => 55_000_000, + // std::time::Instant uses QueryPerformanceCounter & QueryPerformanceFrequency internally + wasi::__WASI_CLOCK_MONOTONIC => *PERF_COUNTER_RES, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes + wasi::__WASI_CLOCK_PROCESS_CPUTIME_ID => 100, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes + wasi::__WASI_CLOCK_THREAD_CPUTIME_ID => 100, + _ => return Err(Error::EINVAL), + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let duration = match clock_id { + wasi::__WASI_CLOCK_REALTIME => get_monotonic_time(), + wasi::__WASI_CLOCK_MONOTONIC => get_realtime_time()?, + wasi::__WASI_CLOCK_PROCESS_CPUTIME_ID => get_proc_cputime()?, + wasi::__WASI_CLOCK_THREAD_CPUTIME_ID => get_thread_cputime()?, + _ => return Err(Error::EINVAL), + }; + duration.as_nanos().try_into().map_err(Into::into) +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result> { + unimplemented!("poll_oneoff") +} + +fn get_monotonic_time() -> Duration { + // We're circumventing the fact that we can't get a Duration from an Instant + // The epoch of __WASI_CLOCK_MONOTONIC is undefined, so we fix a time point once + // and count relative to this time point. + // + // The alternative would be to copy over the implementation of std::time::Instant + // to our source tree and add a conversion to std::time::Duration + START_MONOTONIC.elapsed() +} + +fn get_realtime_time() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| Error::EFAULT) +} + +fn get_proc_cputime() -> Result { + Ok(ProcessTime::try_now()?.as_duration()) +} + +fn get_thread_cputime() -> Result { + Ok(ThreadTime::try_now()?.as_duration()) +} + +fn get_perf_counter_resolution_ns() -> u64 { + use winx::time::perf_counter_frequency; + const NANOS_PER_SEC: u64 = 1_000_000_000; + // This should always succeed starting from Windows XP, so it's fine to panic in case of an error. + let freq = perf_counter_frequency().expect("QueryPerformanceFrequency returned an error"); + let epsilon = NANOS_PER_SEC / freq; + epsilon +} diff --git a/wasi-common/src/sys/windows/hostcalls_impl/mod.rs b/wasi-common/src/sys/windows/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..9bf4b539a5 --- /dev/null +++ b/wasi-common/src/sys/windows/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Windows-specific hostcalls that implement +//! [WASI](https://github.com/CraneStation/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/wasi-common/src/sys/windows/mod.rs b/wasi-common/src/sys/windows/mod.rs new file mode 100644 index 0000000000..398d945323 --- /dev/null +++ b/wasi-common/src/sys/windows/mod.rs @@ -0,0 +1,32 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +use crate::Result; +use std::fs::{File, OpenOptions}; +use std::path::Path; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("NUL") + .map_err(Into::into) +} + +pub fn preopen_dir>(path: P) -> Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS; + + // To open a directory using CreateFile, specify the + // FILE_FLAG_BACKUP_SEMANTICS flag as part of dwFileFlags... + // cf. https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-createfile2 + OpenOptions::new() + .create(false) + .write(true) + .read(true) + .attributes(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) + .map_err(Into::into) +} diff --git a/wasi-common/src/wasi.rs b/wasi-common/src/wasi.rs new file mode 100644 index 0000000000..21e230fcfd --- /dev/null +++ b/wasi-common/src/wasi.rs @@ -0,0 +1,957 @@ +//! Types and constants shared between 32-bit and 64-bit wasi. Types involving +//! pointer or `usize`-sized data are excluded here, so this file only contains +//! fixed-size types, so it's host/target independent. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use wig::witx_wasi_types; + +witx_wasi_types!("unstable" "wasi_unstable_preview0"); + +pub(crate) const RIGHTS_ALL: __wasi_rights_t = __WASI_RIGHT_FD_DATASYNC + | __WASI_RIGHT_FD_READ + | __WASI_RIGHT_FD_SEEK + | __WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHT_FD_SYNC + | __WASI_RIGHT_FD_TELL + | __WASI_RIGHT_FD_WRITE + | __WASI_RIGHT_FD_ADVISE + | __WASI_RIGHT_FD_ALLOCATE + | __WASI_RIGHT_PATH_CREATE_DIRECTORY + | __WASI_RIGHT_PATH_CREATE_FILE + | __WASI_RIGHT_PATH_LINK_SOURCE + | __WASI_RIGHT_PATH_LINK_TARGET + | __WASI_RIGHT_PATH_OPEN + | __WASI_RIGHT_FD_READDIR + | __WASI_RIGHT_PATH_READLINK + | __WASI_RIGHT_PATH_RENAME_SOURCE + | __WASI_RIGHT_PATH_RENAME_TARGET + | __WASI_RIGHT_PATH_FILESTAT_GET + | __WASI_RIGHT_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHT_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHT_FD_FILESTAT_GET + | __WASI_RIGHT_FD_FILESTAT_SET_SIZE + | __WASI_RIGHT_FD_FILESTAT_SET_TIMES + | __WASI_RIGHT_PATH_SYMLINK + | __WASI_RIGHT_PATH_UNLINK_FILE + | __WASI_RIGHT_PATH_REMOVE_DIRECTORY + | __WASI_RIGHT_POLL_FD_READWRITE + | __WASI_RIGHT_SOCK_SHUTDOWN; + +// Block and character device interaction is outside the scope of +// WASI. Simply allow everything. +pub(crate) const RIGHTS_BLOCK_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_BLOCK_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Only allow directory operations on directories. Directories can only +// yield file descriptors to other directories and files. +pub(crate) const RIGHTS_DIRECTORY_BASE: __wasi_rights_t = __WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHT_FD_SYNC + | __WASI_RIGHT_FD_ADVISE + | __WASI_RIGHT_PATH_CREATE_DIRECTORY + | __WASI_RIGHT_PATH_CREATE_FILE + | __WASI_RIGHT_PATH_LINK_SOURCE + | __WASI_RIGHT_PATH_LINK_TARGET + | __WASI_RIGHT_PATH_OPEN + | __WASI_RIGHT_FD_READDIR + | __WASI_RIGHT_PATH_READLINK + | __WASI_RIGHT_PATH_RENAME_SOURCE + | __WASI_RIGHT_PATH_RENAME_TARGET + | __WASI_RIGHT_PATH_FILESTAT_GET + | __WASI_RIGHT_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHT_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHT_FD_FILESTAT_GET + | __WASI_RIGHT_FD_FILESTAT_SET_TIMES + | __WASI_RIGHT_PATH_SYMLINK + | __WASI_RIGHT_PATH_UNLINK_FILE + | __WASI_RIGHT_PATH_REMOVE_DIRECTORY + | __WASI_RIGHT_POLL_FD_READWRITE; +pub(crate) const RIGHTS_DIRECTORY_INHERITING: __wasi_rights_t = + RIGHTS_DIRECTORY_BASE | RIGHTS_REGULAR_FILE_BASE; + +// Operations that apply to regular files. +pub(crate) const RIGHTS_REGULAR_FILE_BASE: __wasi_rights_t = __WASI_RIGHT_FD_DATASYNC + | __WASI_RIGHT_FD_READ + | __WASI_RIGHT_FD_SEEK + | __WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHT_FD_SYNC + | __WASI_RIGHT_FD_TELL + | __WASI_RIGHT_FD_WRITE + | __WASI_RIGHT_FD_ADVISE + | __WASI_RIGHT_FD_ALLOCATE + | __WASI_RIGHT_FD_FILESTAT_GET + | __WASI_RIGHT_FD_FILESTAT_SET_SIZE + | __WASI_RIGHT_FD_FILESTAT_SET_TIMES + | __WASI_RIGHT_POLL_FD_READWRITE; +pub(crate) const RIGHTS_REGULAR_FILE_INHERITING: __wasi_rights_t = 0; + +// Operations that apply to sockets and socket pairs. +pub(crate) const RIGHTS_SOCKET_BASE: __wasi_rights_t = __WASI_RIGHT_FD_READ + | __WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHT_FD_WRITE + | __WASI_RIGHT_FD_FILESTAT_GET + | __WASI_RIGHT_POLL_FD_READWRITE + | __WASI_RIGHT_SOCK_SHUTDOWN; +pub(crate) const RIGHTS_SOCKET_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Operations that apply to TTYs. +pub(crate) const RIGHTS_TTY_BASE: __wasi_rights_t = __WASI_RIGHT_FD_READ + | __WASI_RIGHT_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHT_FD_WRITE + | __WASI_RIGHT_FD_FILESTAT_GET + | __WASI_RIGHT_POLL_FD_READWRITE; +#[allow(unused)] +pub(crate) const RIGHTS_TTY_INHERITING: __wasi_rights_t = 0; + +pub fn strerror(errno: __wasi_errno_t) -> &'static str { + match errno { + __WASI_ESUCCESS => "__WASI_ESUCCESS", + __WASI_E2BIG => "__WASI_E2BIG", + __WASI_EACCES => "__WASI_EACCES", + __WASI_EADDRINUSE => "__WASI_EADDRINUSE", + __WASI_EADDRNOTAVAIL => "__WASI_EADDRNOTAVAIL", + __WASI_EAFNOSUPPORT => "__WASI_EAFNOSUPPORT", + __WASI_EAGAIN => "__WASI_EAGAIN", + __WASI_EALREADY => "__WASI_EALREADY", + __WASI_EBADF => "__WASI_EBADF", + __WASI_EBADMSG => "__WASI_EBADMSG", + __WASI_EBUSY => "__WASI_EBUSY", + __WASI_ECANCELED => "__WASI_ECANCELED", + __WASI_ECHILD => "__WASI_ECHILD", + __WASI_ECONNABORTED => "__WASI_ECONNABORTED", + __WASI_ECONNREFUSED => "__WASI_ECONNREFUSED", + __WASI_ECONNRESET => "__WASI_ECONNRESET", + __WASI_EDEADLK => "__WASI_EDEADLK", + __WASI_EDESTADDRREQ => "__WASI_EDESTADDRREQ", + __WASI_EDOM => "__WASI_EDOM", + __WASI_EDQUOT => "__WASI_EDQUOT", + __WASI_EEXIST => "__WASI_EEXIST", + __WASI_EFAULT => "__WASI_EFAULT", + __WASI_EFBIG => "__WASI_EFBIG", + __WASI_EHOSTUNREACH => "__WASI_EHOSTUNREACH", + __WASI_EIDRM => "__WASI_EIDRM", + __WASI_EILSEQ => "__WASI_EILSEQ", + __WASI_EINPROGRESS => "__WASI_EINPROGRESS", + __WASI_EINTR => "__WASI_EINTR", + __WASI_EINVAL => "__WASI_EINVAL", + __WASI_EIO => "__WASI_EIO", + __WASI_EISCONN => "__WASI_EISCONN", + __WASI_EISDIR => "__WASI_EISDIR", + __WASI_ELOOP => "__WASI_ELOOP", + __WASI_EMFILE => "__WASI_EMFILE", + __WASI_EMLINK => "__WASI_EMLINK", + __WASI_EMSGSIZE => "__WASI_EMSGSIZE", + __WASI_EMULTIHOP => "__WASI_EMULTIHOP", + __WASI_ENAMETOOLONG => "__WASI_ENAMETOOLONG", + __WASI_ENETDOWN => "__WASI_ENETDOWN", + __WASI_ENETRESET => "__WASI_ENETRESET", + __WASI_ENETUNREACH => "__WASI_ENETUNREACH", + __WASI_ENFILE => "__WASI_ENFILE", + __WASI_ENOBUFS => "__WASI_ENOBUFS", + __WASI_ENODEV => "__WASI_ENODEV", + __WASI_ENOENT => "__WASI_ENOENT", + __WASI_ENOEXEC => "__WASI_ENOEXEC", + __WASI_ENOLCK => "__WASI_ENOLCK", + __WASI_ENOLINK => "__WASI_ENOLINK", + __WASI_ENOMEM => "__WASI_ENOMEM", + __WASI_ENOMSG => "__WASI_ENOMSG", + __WASI_ENOPROTOOPT => "__WASI_ENOPROTOOPT", + __WASI_ENOSPC => "__WASI_ENOSPC", + __WASI_ENOSYS => "__WASI_ENOSYS", + __WASI_ENOTCONN => "__WASI_ENOTCONN", + __WASI_ENOTDIR => "__WASI_ENOTDIR", + __WASI_ENOTEMPTY => "__WASI_ENOTEMPTY", + __WASI_ENOTRECOVERABLE => "__WASI_ENOTRECOVERABLE", + __WASI_ENOTSOCK => "__WASI_ENOTSOCK", + __WASI_ENOTSUP => "__WASI_ENOTSUP", + __WASI_ENOTTY => "__WASI_ENOTTY", + __WASI_ENXIO => "__WASI_ENXIO", + __WASI_EOVERFLOW => "__WASI_EOVERFLOW", + __WASI_EOWNERDEAD => "__WASI_EOWNERDEAD", + __WASI_EPERM => "__WASI_EPERM", + __WASI_EPIPE => "__WASI_EPIPE", + __WASI_EPROTO => "__WASI_EPROTO", + __WASI_EPROTONOSUPPORT => "__WASI_EPROTONOSUPPORT", + __WASI_EPROTOTYPE => "__WASI_EPROTOTYPE", + __WASI_ERANGE => "__WASI_ERANGE", + __WASI_EROFS => "__WASI_EROFS", + __WASI_ESPIPE => "__WASI_ESPIPE", + __WASI_ESRCH => "__WASI_ESRCH", + __WASI_ESTALE => "__WASI_ESTALE", + __WASI_ETIMEDOUT => "__WASI_ETIMEDOUT", + __WASI_ETXTBSY => "__WASI_ETXTBSY", + __WASI_EXDEV => "__WASI_EXDEV", + __WASI_ENOTCAPABLE => "__WASI_ENOTCAPABLE", + other => panic!("Undefined errno value {:?}", other), + } +} + +pub fn whence_to_str(whence: __wasi_whence_t) -> &'static str { + match whence { + __WASI_WHENCE_CUR => "__WASI_WHENCE_CUR", + __WASI_WHENCE_END => "__WASI_WHENCE_END", + __WASI_WHENCE_SET => "__WASI_WHENCE_SET", + other => panic!("Undefined whence value {:?}", other), + } +} + +pub const __WASI_DIRCOOKIE_START: __wasi_dircookie_t = 0; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn bindgen_test_layout_wasi_dirent_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_dirent_t>(), + 24usize, + concat!("Size of: ", stringify!(__wasi_dirent_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_next as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_next) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_ino as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_ino) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_namlen as *const _ as usize }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_namlen) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_type as *const _ as usize }, + 20usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_type) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_event_t___wasi_event_u___wasi_event_u_fd_readwrite_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_event_fd_readwrite_t>(), + 16usize, + concat!("Size of: ", stringify!(__wasi_event_fd_readwrite_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_event_fd_readwrite_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_event_fd_readwrite_t)) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_event_fd_readwrite_t>())).nbytes as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_fd_readwrite_t), + "::", + stringify!(nbytes) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_event_fd_readwrite_t>())).flags as *const _ as usize + }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_fd_readwrite_t), + "::", + stringify!(flags) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_event_t___wasi_event_u() { + assert_eq!( + ::std::mem::size_of::<__wasi_event_u>(), + 16usize, + concat!("Size of: ", stringify!(__wasi_event_u)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_event_u>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_event_u)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_u>())).fd_readwrite as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_u), + "::", + stringify!(fd_readwrite) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_event_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_event_t>(), + 32usize, + concat!("Size of: ", stringify!(__wasi_event_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_event_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_event_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).userdata as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(userdata) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).error as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(error) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).r#type as *const _ as usize }, + 10usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(r#type) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).u as *const _ as usize }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(u) + ) + ); + } + + #[test] + fn bindgen_test_layout_wasi_event_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_event_t>(), + 32usize, + concat!("Size of: ", stringify!(__wasi_event_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).userdata as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(userdata) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).error as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(error) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_event_t>())).r#type as *const _ as usize }, + 10usize, + concat!( + "Offset of field: ", + stringify!(__wasi_event_t), + "::", + stringify!(r#type) + ) + ); + } + + #[test] + fn bindgen_test_layout_wasi_fdstat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_fdstat_t>(), + 24usize, + concat!("Size of: ", stringify!(__wasi_fdstat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_filetype as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_filetype) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_flags as *const _ as usize }, + 2usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_flags) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_rights_base as *const _ as usize + }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_rights_base) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_rights_inheriting as *const _ + as usize + }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_rights_inheriting) + ) + ); + } + + #[test] + fn bindgen_test_layout_wasi_filestat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_filestat_t>(), + 56usize, + concat!("Size of: ", stringify!(__wasi_filestat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_dev as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_dev) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_ino as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_ino) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_filestat_t>())).st_filetype as *const _ as usize + }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_filetype) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_nlink as *const _ as usize }, + 20usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_nlink) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_size as *const _ as usize }, + 24usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_size) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_atim as *const _ as usize }, + 32usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_atim) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_mtim as *const _ as usize }, + 40usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_mtim) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_ctim as *const _ as usize }, + 48usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_ctim) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_subscription_clock_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_subscription_clock_t>(), + 40usize, + concat!("Size of: ", stringify!(__wasi_subscription_clock_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_subscription_clock_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_subscription_clock_t)) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_clock_t>())).identifier as *const _ + as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_clock_t), + "::", + stringify!(identifier) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_clock_t>())).clock_id as *const _ + as usize + }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_clock_t), + "::", + stringify!(clock_id) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_clock_t>())).timeout as *const _ as usize + }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_clock_t), + "::", + stringify!(timeout) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_clock_t>())).precision as *const _ + as usize + }, + 24usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_clock_t), + "::", + stringify!(precision) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_clock_t>())).flags as *const _ as usize + }, + 32usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_clock_t), + "::", + stringify!(flags) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_subscription_t___wasi_subscription_u___wasi_subscription_u_fd_readwrite_t( + ) { + assert_eq!( + ::std::mem::size_of::<__wasi_subscription_fd_readwrite_t>(), + 4usize, + concat!("Size of: ", stringify!(__wasi_subscription_fd_readwrite_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_subscription_fd_readwrite_t>(), + 4usize, + concat!( + "Alignment of ", + stringify!(__wasi_subscription_fd_readwrite_t) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_fd_readwrite_t>())).file_descriptor + as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_fd_readwrite_t), + "::", + stringify!(fd) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_subscription_t___wasi_subscription_u() { + assert_eq!( + ::std::mem::size_of::<__wasi_subscription_u>(), + 40usize, + concat!("Size of: ", stringify!(__wasi_subscription_u)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_subscription_u>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_subscription_u)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_subscription_u>())).clock as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_u), + "::", + stringify!(clock) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_u>())).fd_readwrite as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_u), + "::", + stringify!(fd_readwrite) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_subscription_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_subscription_t>(), + 56usize, + concat!("Size of: ", stringify!(__wasi_subscription_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_subscription_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_subscription_t)) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_t>())).userdata as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_t), + "::", + stringify!(userdata) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_subscription_t>())).r#type as *const _ as usize + }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_t), + "::", + stringify!(r#type) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_subscription_t>())).u as *const _ as usize }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_subscription_t), + "::", + stringify!(u) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_filestat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_filestat_t>(), + 56usize, + concat!("Size of: ", stringify!(__wasi_filestat_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_filestat_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_filestat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_dev as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_dev) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_ino as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_ino) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_filestat_t>())).st_filetype as *const _ as usize + }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_filetype) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_nlink as *const _ as usize }, + 20usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_nlink) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_size as *const _ as usize }, + 24usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_size) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_atim as *const _ as usize }, + 32usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_atim) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_mtim as *const _ as usize }, + 40usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_mtim) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_filestat_t>())).st_ctim as *const _ as usize }, + 48usize, + concat!( + "Offset of field: ", + stringify!(__wasi_filestat_t), + "::", + stringify!(st_ctim) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_fdstat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_fdstat_t>(), + 24usize, + concat!("Size of: ", stringify!(__wasi_fdstat_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_fdstat_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_fdstat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_filetype as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_filetype) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_flags as *const _ as usize }, + 2usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_flags) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_rights_base as *const _ as usize + }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_rights_base) + ) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_fdstat_t>())).fs_rights_inheriting as *const _ + as usize + }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_fdstat_t), + "::", + stringify!(fs_rights_inheriting) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_dirent_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_dirent_t>(), + 24usize, + concat!("Size of: ", stringify!(__wasi_dirent_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_dirent_t>(), + 8usize, + concat!("Alignment of ", stringify!(__wasi_dirent_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_next as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_next) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_ino as *const _ as usize }, + 8usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_ino) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_namlen as *const _ as usize }, + 16usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_namlen) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_dirent_t>())).d_type as *const _ as usize }, + 20usize, + concat!( + "Offset of field: ", + stringify!(__wasi_dirent_t), + "::", + stringify!(d_type) + ) + ); + } +} diff --git a/wasi-common/src/wasi32.rs b/wasi-common/src/wasi32.rs new file mode 100644 index 0000000000..c46d4f6141 --- /dev/null +++ b/wasi-common/src/wasi32.rs @@ -0,0 +1,173 @@ +//! Types and constants specific to 32-bit wasi. These are similar to the types +//! in the `host` module, but pointers and `usize` values are replaced with +//! `u32`-sized types. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use wig::witx_wasi32_types; + +use crate::wasi::*; + +pub type uintptr_t = u32; +pub type size_t = u32; + +witx_wasi32_types!("unstable" "wasi_unstable_preview0"); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn bindgen_test_layout_wasi_ciovec_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_ciovec_t>(), + 8usize, + concat!("Size of: ", stringify!(__wasi_ciovec_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_ciovec_t>(), + 4usize, + concat!("Alignment of ", stringify!(__wasi_ciovec_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_ciovec_t>())).buf as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_ciovec_t), + "::", + stringify!(buf) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_ciovec_t>())).buf_len as *const _ as usize }, + 4usize, + concat!( + "Offset of field: ", + stringify!(__wasi_ciovec_t), + "::", + stringify!(buf_len) + ) + ); + } + + #[test] + fn bindgen_test_layout_wasi_iovec_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_iovec_t>(), + 8usize, + concat!("Size of: ", stringify!(__wasi_iovec_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_iovec_t>(), + 4usize, + concat!("Alignment of ", stringify!(__wasi_iovec_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_iovec_t>())).buf as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_iovec_t), + "::", + stringify!(buf) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_iovec_t>())).buf_len as *const _ as usize }, + 4usize, + concat!( + "Offset of field: ", + stringify!(__wasi_iovec_t), + "::", + stringify!(buf_len) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_prestat_t___wasi_prestat_u___wasi_prestat_u_dir_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_dir>(), + 4usize, + concat!("Size of: ", stringify!(__wasi_prestat_dir)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_dir>(), + 4usize, + concat!("Alignment of ", stringify!(__wasi_prestat_dir)) + ); + assert_eq!( + unsafe { + &(*(::std::ptr::null::<__wasi_prestat_dir>())).pr_name_len as *const _ as usize + }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_dir), + "::", + stringify!(pr_name_len) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_prestat_t___wasi_prestat_u() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_u>(), + 4usize, + concat!("Size of: ", stringify!(__wasi_prestat_u)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_u>(), + 4usize, + concat!("Alignment of ", stringify!(__wasi_prestat_u)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_u>())).dir as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_u), + "::", + stringify!(dir) + ) + ); + } + + #[test] + fn bindgen_test_layout___wasi_prestat_t() { + assert_eq!( + ::std::mem::size_of::<__wasi_prestat_t>(), + 8usize, + concat!("Size of: ", stringify!(__wasi_prestat_t)) + ); + assert_eq!( + ::std::mem::align_of::<__wasi_prestat_t>(), + 4usize, + concat!("Alignment of ", stringify!(__wasi_prestat_t)) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_t>())).pr_type as *const _ as usize }, + 0usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_t), + "::", + stringify!(pr_type) + ) + ); + assert_eq!( + unsafe { &(*(::std::ptr::null::<__wasi_prestat_t>())).u as *const _ as usize }, + 4usize, + concat!( + "Offset of field: ", + stringify!(__wasi_prestat_t), + "::", + stringify!(u) + ) + ); + } +} diff --git a/wasi-common/tests/runtime.rs b/wasi-common/tests/runtime.rs new file mode 100644 index 0000000000..91eda11dec --- /dev/null +++ b/wasi-common/tests/runtime.rs @@ -0,0 +1,92 @@ +use cranelift_codegen::settings::{self, Configurable}; +use std::{collections::HashMap, path::Path}; +use wasmtime_api::{Config, Engine, HostRef, Instance, Module, Store}; +use wasmtime_jit::{CompilationStrategy, Features}; + +pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> Result<(), String> { + // Prepare runtime + let mut flag_builder = settings::builder(); + + // Enable proper trap for division + flag_builder + .enable("avoid_div_traps") + .map_err(|err| format!("error while enabling proper division trap: {}", err))?; + + let config = Config::new( + settings::Flags::new(flag_builder), + Features::default(), + false, + CompilationStrategy::Auto, + ); + let engine = HostRef::new(Engine::new(config)); + let store = HostRef::new(Store::new(engine)); + + let mut module_registry = HashMap::new(); + let global_exports = store.borrow().global_exports().clone(); + let get_preopens = |workspace: Option<&Path>| -> Result, String> { + if let Some(workspace) = workspace { + let preopen_dir = wasi_common::preopen_dir(workspace).map_err(|e| { + format!( + "error while preopening directory '{}': {}", + workspace.display(), + e + ) + })?; + + Ok(vec![(".".to_owned(), preopen_dir)]) + } else { + Ok(vec![]) + } + }; + module_registry.insert( + "wasi_unstable".to_owned(), + Instance::from_handle( + store.clone(), + wasmtime_wasi::instantiate_wasi( + "", + global_exports.clone(), + &get_preopens(workspace)?, + &[bin_name.to_owned(), ".".to_owned()], + &[], + ) + .map_err(|e| format!("error instantiating WASI: {}", e))?, + ) + .map_err(|err| format!("error instantiating from handle: {}", err))?, + ); + + let module = HostRef::new( + Module::new(store.clone(), &data) + .map_err(|err| format!("error while creating Wasm module '{}': {}", bin_name, err))?, + ); + let imports = module + .borrow() + .imports() + .iter() + .map(|i| { + let module_name = i.module().to_string(); + if let Some((instance, map)) = module_registry.get(&module_name) { + let field_name = i.name().to_string(); + if let Some(export_index) = map.get(&field_name) { + Ok(instance.exports()[*export_index].clone()) + } else { + Err(format!( + "import {} was not found in module {}", + field_name, module_name + )) + } + } else { + Err(format!("import module {} was not found", module_name)) + } + }) + .collect::, _>>()?; + let _ = HostRef::new( + Instance::new(store.clone(), module.clone(), &imports).map_err(|err| { + format!( + "error while instantiating Wasm module '{}': {}", + bin_name, err + ) + })?, + ); + + Ok(()) +} diff --git a/wasi-common/tests/utils.rs b/wasi-common/tests/utils.rs new file mode 100644 index 0000000000..160412d93a --- /dev/null +++ b/wasi-common/tests/utils.rs @@ -0,0 +1,30 @@ +use std::fs; +use std::path::Path; +use tempfile::{Builder, TempDir}; + +pub fn read_wasm(path: &Path) -> Result, String> { + let data = fs::read(path).map_err(|err| err.to_string())?; + if data.starts_with(&[b'\0', b'a', b's', b'm']) { + Ok(data) + } else { + Err("Invalid Wasm file encountered".to_owned()) + } +} + +pub fn prepare_workspace(exe_name: &str) -> Result { + let prefix = format!("wasi_common_{}", exe_name); + Builder::new() + .prefix(&prefix) + .tempdir() + .map_err(|e| format!("couldn't create workspace in temp files: {}", e)) +} + +pub fn extract_exec_name_from_path(path: &Path) -> Result { + path.file_stem() + .and_then(|s| s.to_str()) + .map(String::from) + .ok_or(format!( + "couldn't extract the file stem from path {}", + path.display() + )) +} diff --git a/wasi-common/tests/wasm_tests.rs b/wasi-common/tests/wasm_tests.rs new file mode 100644 index 0000000000..0a5515d52b --- /dev/null +++ b/wasi-common/tests/wasm_tests.rs @@ -0,0 +1,16 @@ +#![cfg(feature = "wasm_tests")] + +mod runtime; +mod utils; + +use std::sync::Once; + +static LOG_INIT: Once = Once::new(); + +fn setup_log() { + LOG_INIT.call_once(|| { + pretty_env_logger::init(); + }) +} + +include!(concat!(env!("OUT_DIR"), "/wasi_misc_tests.rs")); diff --git a/wasi-common/wasi-common-cbindgen/Cargo.toml b/wasi-common/wasi-common-cbindgen/Cargo.toml new file mode 100644 index 0000000000..ae4d9ab28b --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasi-common-cbindgen" +version = "0.5.0" +authors = ["Jakub Konka "] +edition = "2018" +license = "Apache-2.0 WITH LLVM-exception" +description = "Interface generator utilities used by wasi-common" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0.5", features = ["full"] } +quote = "1.0.2" + +[dev-dependencies] +trybuild = "1.0.4" diff --git a/wasi-common/wasi-common-cbindgen/LICENSE b/wasi-common/wasi-common-cbindgen/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/wasi-common/wasi-common-cbindgen/src/lib.rs b/wasi-common/wasi-common-cbindgen/src/lib.rs new file mode 100644 index 0000000000..dcb7f0730d --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/src/lib.rs @@ -0,0 +1,107 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{FnArg, Pat, PatType, Type, TypeReference, TypeSlice}; + +#[proc_macro_attribute] +pub fn wasi_common_cbindgen(attr: TokenStream, function: TokenStream) -> TokenStream { + assert!(attr.is_empty()); + + let function = syn::parse_macro_input!(function as syn::ItemFn); + + // capture visibility + let vis = &function.vis; + + // generate C fn name prefixed with __wasi_ + let fn_ident = &function.sig.ident; + let concatenated = format!("wasi_common_{}", fn_ident); + let c_fn_ident = syn::Ident::new(&concatenated, fn_ident.span()); + + // capture input args + let mut arg_ident = Vec::new(); + let mut arg_type = Vec::new(); + let mut call_arg_ident = Vec::new(); + for input in &function.sig.inputs { + match input { + FnArg::Typed(PatType { + attrs, + pat, + colon_token: _, + ty, + }) => { + // parse arg identifier + let ident = if let Pat::Ident(ident) = &**pat { + &ident.ident + } else { + panic!("expected function input to be an identifier") + }; + if !attrs.is_empty() { + panic!("unsupported attributes on function arg"); + } + arg_ident.push(quote!(#ident)); + // parse arg type + if let Type::Reference(ty @ TypeReference { .. }) = &**ty { + // if we're here, then we found a &-ref + // so substitute it for *mut since we're exporting to C + let elem = &*ty.elem; + if let Type::Slice(elem @ TypeSlice { .. }) = &elem { + // slice: &[type] or &mut [type] + // in C it requires a signature *mut type + let elem = &elem.elem; + arg_type.push(quote!(*mut #elem)); + // since it's a slice, we'll need to do more work here + // simple dereferencing is not enough + // firstly, we need to add a len arg to C fn + // secondly, we need to invoke std::slice::from_raw_parts_mut(..) + let concatenated = format!("{}_len", ident); + let len_ident = syn::Ident::new(&concatenated, ident.span()); + call_arg_ident.push(quote! { + std::slice::from_raw_parts_mut(#ident, #len_ident) + }); + arg_ident.push(quote!(#len_ident)); + arg_type.push(quote!(usize)); + } else { + // & or &mut type; substitute with *const or *mut type. + // Also, we need to properly dereference the substituted raw + // pointer if we are to properly call the hostcall fn. + if ty.mutability.is_none() { + arg_type.push(quote!(*const #elem)); + call_arg_ident.push(quote!(&*#ident)); + } else { + arg_type.push(quote!(*mut #elem)); + call_arg_ident.push(quote!(&mut *#ident)); + } + } + } else { + arg_type.push(quote!(#ty)); + // non-&-ref type, so preserve whatever the arg was + call_arg_ident.push(quote!(#ident)); + } + } + _ => { + unimplemented!("unrecognized function input pattern"); + } + } + } + + // capture output arg + let output = &function.sig.output; + + let result = quote! { + #function + + #[no_mangle] + #vis unsafe extern "C" fn #c_fn_ident( + #( + #arg_ident: #arg_type, + )* + ) #output { + #fn_ident(#( + #call_arg_ident, + )*) + } + }; + + result.into() +} diff --git a/wasi-common/wasi-common-cbindgen/tests/array_args.rs b/wasi-common/wasi-common-cbindgen/tests/array_args.rs new file mode 100644 index 0000000000..1aee2a702a --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/array_args.rs @@ -0,0 +1,20 @@ +extern crate wasi_common_cbindgen; + +pub use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +fn array_args(a: &mut [u8]) { + a[0] = 1; +} + +fn main() { + let mut expected: &mut [u8] = &mut [0, 0]; + array_args(&mut expected); + + let given: &mut [u8] = &mut [0, 0]; + unsafe { + wasi_common_array_args(given.as_mut_ptr(), given.len()); + } + + assert_eq!(given, expected); +} diff --git a/wasi-common/wasi-common-cbindgen/tests/mut_args.rs b/wasi-common/wasi-common-cbindgen/tests/mut_args.rs new file mode 100644 index 0000000000..5ddaa2da19 --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/mut_args.rs @@ -0,0 +1,20 @@ +extern crate wasi_common_cbindgen; + +pub use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +fn mut_args(a: &mut usize) { + *a = *a + 1 +} + +fn main() { + let mut expected = Box::new(2); + mut_args(expected.as_mut()); + let given = unsafe { + let given = Box::new(2); + let raw = Box::into_raw(given); + wasi_common_mut_args(raw); + Box::from_raw(raw) + }; + assert_eq!(*given, *expected); +} diff --git a/wasi-common/wasi-common-cbindgen/tests/no_args.rs b/wasi-common/wasi-common-cbindgen/tests/no_args.rs new file mode 100644 index 0000000000..60204ecb31 --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/no_args.rs @@ -0,0 +1,12 @@ +extern crate wasi_common_cbindgen; + +pub use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +fn no_args() -> u32 { + 0 +} + +fn main() { + assert_eq!(unsafe { wasi_common_no_args() }, no_args()); +} diff --git a/wasi-common/wasi-common-cbindgen/tests/ref_args.rs b/wasi-common/wasi-common-cbindgen/tests/ref_args.rs new file mode 100644 index 0000000000..3a234b1128 --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/ref_args.rs @@ -0,0 +1,20 @@ +extern crate wasi_common_cbindgen; + +pub use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +fn ref_args(a: &usize) -> usize { + a + 1 +} + +fn main() { + let a = Box::new(2); + let expected = ref_args(a.as_ref()); + let given = unsafe { + let raw = Box::into_raw(a); + let res = wasi_common_ref_args(raw); + Box::from_raw(raw); + res + }; + assert_eq!(given, expected); +} diff --git a/wasi-common/wasi-common-cbindgen/tests/test.rs b/wasi-common/wasi-common-cbindgen/tests/test.rs new file mode 100644 index 0000000000..678e1f4dac --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/test.rs @@ -0,0 +1,9 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/no_args.rs"); + t.pass("tests/val_args.rs"); + t.pass("tests/ref_args.rs"); + t.pass("tests/mut_args.rs"); + t.pass("tests/array_args.rs"); +} diff --git a/wasi-common/wasi-common-cbindgen/tests/val_args.rs b/wasi-common/wasi-common-cbindgen/tests/val_args.rs new file mode 100644 index 0000000000..47313be032 --- /dev/null +++ b/wasi-common/wasi-common-cbindgen/tests/val_args.rs @@ -0,0 +1,12 @@ +extern crate wasi_common_cbindgen; + +pub use wasi_common_cbindgen::wasi_common_cbindgen; + +#[wasi_common_cbindgen] +fn val_args(a: usize, b: usize) -> usize { + a + b +} + +fn main() { + assert_eq!(unsafe { wasi_common_val_args(1, 2) }, val_args(1, 2)); +} diff --git a/wasi-common/wasi-misc-tests/Cargo.toml b/wasi-common/wasi-misc-tests/Cargo.toml new file mode 100644 index 0000000000..a8693af0ea --- /dev/null +++ b/wasi-common/wasi-misc-tests/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasi-misc-tests" +version = "0.1.0" +authors = ["The Wasmtime Project Developers"] +edition = "2018" +publish = false + +[dependencies] +libc = "0.2.65" +wasi = "0.7.0" +more-asserts = "0.2.1" diff --git a/wasi-common/wasi-misc-tests/README.md b/wasi-common/wasi-misc-tests/README.md new file mode 100644 index 0000000000..ded59fb000 --- /dev/null +++ b/wasi-common/wasi-misc-tests/README.md @@ -0,0 +1,4 @@ +This is the `wasi-misc-test` crate, which contains source code for the system-level WASI tests. + +Building these tests requires `wasm32-wasi` target installed + diff --git a/wasi-common/wasi-misc-tests/src/bin/big_random_buf.rs b/wasi-common/wasi-misc-tests/src/bin/big_random_buf.rs new file mode 100644 index 0000000000..5cbbc11928 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/big_random_buf.rs @@ -0,0 +1,18 @@ +use wasi::wasi_unstable; + +fn test_big_random_buf() { + let mut buf = Vec::new(); + buf.resize(1024, 0); + assert!( + wasi_unstable::random_get(&mut buf).is_ok(), + "calling get_random on a large buffer" + ); + // Chances are pretty good that at least *one* byte will be non-zero in + // any meaningful random function producing 1024 u8 values. + assert!(buf.iter().any(|x| *x != 0), "random_get returned all zeros"); +} + +fn main() { + // Run the tests. + test_big_random_buf() +} diff --git a/wasi-common/wasi-misc-tests/src/bin/clock_time_get.rs b/wasi-common/wasi-misc-tests/src/bin/clock_time_get.rs new file mode 100644 index 0000000000..5de9b5c1aa --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/clock_time_get.rs @@ -0,0 +1,37 @@ +use more_asserts::assert_le; +use wasi::wasi_unstable; +use wasi_misc_tests::wasi_wrappers::wasi_clock_time_get; + +unsafe fn test_clock_time_get() { + // Test that clock_time_get succeeds. Even in environments where it's not + // desirable to expose high-precision timers, it should still succeed. + // clock_res_get is where information about precision can be provided. + let mut time: wasi_unstable::Timestamp = 0; + let status = wasi_clock_time_get(wasi_unstable::CLOCK_MONOTONIC, 1, &mut time); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "clock_time_get with a precision of 1" + ); + + let status = wasi_clock_time_get(wasi_unstable::CLOCK_MONOTONIC, 0, &mut time); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "clock_time_get with a precision of 0" + ); + let first_time = time; + + let status = wasi_clock_time_get(wasi_unstable::CLOCK_MONOTONIC, 0, &mut time); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "clock_time_get with a precision of 0" + ); + assert_le!(first_time, time, "CLOCK_MONOTONIC should be monotonic"); +} + +fn main() { + // Run the tests. + unsafe { test_clock_time_get() } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/close_preopen.rs b/wasi-common/wasi-misc-tests/src/bin/close_preopen.rs new file mode 100644 index 0000000000..000be99418 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/close_preopen.rs @@ -0,0 +1,83 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, mem, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::wasi_wrappers::wasi_fd_fdstat_get; + +unsafe fn test_close_preopen(dir_fd: wasi_unstable::Fd) { + let pre_fd: wasi_unstable::Fd = (libc::STDERR_FILENO + 1) as wasi_unstable::Fd; + + assert_gt!(dir_fd, pre_fd, "dir_fd number"); + + // Try to close a preopened directory handle. + assert_eq!( + wasi_unstable::fd_close(pre_fd), + Err(wasi_unstable::ENOTSUP), + "closing a preopened file descriptor", + ); + + // Try to renumber over a preopened directory handle. + assert_eq!( + wasi_unstable::fd_renumber(dir_fd, pre_fd), + Err(wasi_unstable::ENOTSUP), + "renumbering over a preopened file descriptor", + ); + + // Ensure that dir_fd is still open. + let mut dir_fdstat: wasi_unstable::FdStat = mem::zeroed(); + let mut status = wasi_fd_fdstat_get(dir_fd, &mut dir_fdstat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on the scratch directory" + ); + assert_eq!( + dir_fdstat.fs_filetype, + wasi_unstable::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + + // Try to renumber a preopened directory handle. + assert_eq!( + wasi_unstable::fd_renumber(pre_fd, dir_fd), + Err(wasi_unstable::ENOTSUP), + "renumbering over a preopened file descriptor", + ); + + // Ensure that dir_fd is still open. + status = wasi_fd_fdstat_get(dir_fd, &mut dir_fdstat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on the scratch directory" + ); + assert_eq!( + dir_fdstat.fs_filetype, + wasi_unstable::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_close_preopen(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/dangling_symlink.rs b/wasi-common/wasi-misc-tests/src/bin/dangling_symlink.rs new file mode 100644 index 0000000000..8e30fc4ee7 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/dangling_symlink.rs @@ -0,0 +1,62 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::cleanup_file; +use wasi_misc_tests::wasi_wrappers::{wasi_path_open, wasi_path_symlink}; + +unsafe fn test_dangling_symlink(dir_fd: wasi_unstable::Fd) { + // First create a dangling symlink. + assert!( + wasi_path_symlink("target", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Try to open it as a directory with O_NOFOLLOW. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "symlink", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ELOOP, + "opening a dangling symlink as a directory", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Clean up. + cleanup_file(dir_fd, "symlink"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_dangling_symlink(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/directory_seek.rs b/wasi-common/wasi-misc-tests/src/bin/directory_seek.rs new file mode 100644 index 0000000000..01783e4a27 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/directory_seek.rs @@ -0,0 +1,90 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, mem, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, close_fd, create_dir}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_fdstat_get, wasi_fd_seek, wasi_path_open}; + +unsafe fn test_directory_seek(dir_fd: wasi_unstable::Fd) { + // Create a directory in the scratch directory. + create_dir(dir_fd, "dir"); + + // Open the directory and attempt to request rights for seeking. + let mut fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "dir", + 0, + wasi_unstable::RIGHT_FD_SEEK, + 0, + 0, + &mut fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Attempt to seek. + let mut newoffset = 1; + status = wasi_fd_seek(fd, 0, wasi_unstable::WHENCE_CUR, &mut newoffset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTCAPABLE, + "seek on a directory" + ); + + // Check if we obtained the right to seek. + let mut fdstat: wasi_unstable::FdStat = mem::zeroed(); + status = wasi_fd_fdstat_get(fd, &mut fdstat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on a directory" + ); + assert_eq!( + fdstat.fs_filetype, + wasi_unstable::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + assert_eq!( + (fdstat.fs_rights_base & wasi_unstable::RIGHT_FD_SEEK), + 0, + "directory has the seek right", + ); + + // Clean up. + close_fd(fd); + cleanup_dir(dir_fd, "dir"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_directory_seek(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/fd_advise.rs b/wasi-common/wasi-misc-tests/src/bin/fd_advise.rs new file mode 100644 index 0000000000..2b48b11bbb --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/fd_advise.rs @@ -0,0 +1,98 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_advise, wasi_fd_filestat_get, wasi_path_open}; + +unsafe fn test_fd_advise(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Check file size + let mut stat = wasi_unstable::FileStat { + st_dev: 0, + st_ino: 0, + st_filetype: 0, + st_nlink: 0, + st_size: 0, + st_atim: 0, + st_mtim: 0, + st_ctim: 0, + }; + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats" + ); + assert_eq!(stat.st_size, 0, "file size should be 0"); + + // Allocate some size + assert!( + wasi_unstable::fd_allocate(file_fd, 0, 100).is_ok(), + "allocating size" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after initial allocation" + ); + assert_eq!(stat.st_size, 100, "file size should be 100"); + + // Advise the kernel + let status = wasi_fd_advise(file_fd, 10, 50, wasi_unstable::ADVICE_NORMAL); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "advising the kernel" + ); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_advise(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/fd_filestat_set.rs b/wasi-common/wasi-misc-tests/src/bin/fd_filestat_set.rs new file mode 100644 index 0000000000..90c39b99b1 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/fd_filestat_set.rs @@ -0,0 +1,120 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_filestat_get, wasi_path_open}; + +unsafe fn test_fd_filestat_set(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Check file size + let mut stat = wasi_unstable::FileStat { + st_dev: 0, + st_ino: 0, + st_filetype: 0, + st_nlink: 0, + st_size: 0, + st_atim: 0, + st_mtim: 0, + st_ctim: 0, + }; + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats" + ); + assert_eq!(stat.st_size, 0, "file size should be 0"); + + // Check fd_filestat_set_size + assert!( + wasi_unstable::fd_filestat_set_size(file_fd, 100).is_ok(), + "fd_filestat_set_size" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after fd_filestat_set_size" + ); + assert_eq!(stat.st_size, 100, "file size should be 100"); + + // Check fd_filestat_set_times + let old_atim = stat.st_atim; + let new_mtim = stat.st_mtim - 100; + assert!( + wasi_unstable::fd_filestat_set_times( + file_fd, + new_mtim, + new_mtim, + wasi_unstable::FILESTAT_SET_MTIM, + ) + .is_ok(), + "fd_filestat_set_times" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after fd_filestat_set_times" + ); + assert_eq!( + stat.st_size, 100, + "file size should remain unchanged at 100" + ); + assert_eq!(stat.st_mtim, new_mtim, "mtim should change"); + assert_eq!(stat.st_atim, old_atim, "atim should not change"); + + // let status = wasi_fd_filestat_set_times(file_fd, new_mtim, new_mtim, wasi_unstable::FILESTAT_SET_MTIM | wasi_unstable::FILESTAT_SET_MTIM_NOW); + // assert_eq!(status, wasi_unstable::EINVAL, "ATIM & ATIM_NOW can't both be set"); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_filestat_set(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/fd_readdir.rs b/wasi-common/wasi-misc-tests/src/bin/fd_readdir.rs new file mode 100644 index 0000000000..30ff87a953 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/fd_readdir.rs @@ -0,0 +1,189 @@ +use libc; +use more_asserts::assert_gt; +use std::{cmp::min, env, mem, process, slice, str}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_filestat_get, wasi_fd_readdir, wasi_path_open}; + +const BUF_LEN: usize = 256; + +#[derive(Debug)] +struct DirEntry { + dirent: wasi_unstable::Dirent, + name: String, +} + +// Manually reading the output from fd_readdir is tedious and repetitive, +// so encapsulate it into an iterator +struct ReadDir<'a> { + buf: &'a [u8], +} + +impl<'a> ReadDir<'a> { + fn from_slice(buf: &'a [u8]) -> Self { + Self { buf } + } +} + +impl<'a> Iterator for ReadDir<'a> { + type Item = DirEntry; + + fn next(&mut self) -> Option { + unsafe { + if self.buf.is_empty() { + return None; + } + + // Read the data + let dirent_ptr = self.buf.as_ptr() as *const wasi_unstable::Dirent; + let dirent = *dirent_ptr; + let name_ptr = dirent_ptr.offset(1) as *const u8; + // NOTE Linux syscall returns a NULL-terminated name, but WASI doesn't + let namelen = dirent.d_namlen as usize; + let slice = slice::from_raw_parts(name_ptr, namelen); + let name = str::from_utf8(slice).expect("invalid utf8").to_owned(); + + // Update the internal state + let delta = mem::size_of_val(&dirent) + namelen; + self.buf = &self.buf[delta..]; + + DirEntry { dirent, name }.into() + } + } +} + +unsafe fn exec_fd_readdir( + fd: wasi_unstable::Fd, + cookie: wasi_unstable::DirCookie, +) -> Vec { + let mut buf: [u8; BUF_LEN] = [0; BUF_LEN]; + let mut bufused = 0; + let status = wasi_fd_readdir(fd, &mut buf, BUF_LEN, cookie, &mut bufused); + assert_eq!(status, wasi_unstable::raw::__WASI_ESUCCESS, "fd_readdir"); + + let sl = slice::from_raw_parts(buf.as_ptr(), min(BUF_LEN, bufused)); + let dirs: Vec<_> = ReadDir::from_slice(sl).collect(); + dirs +} + +unsafe fn test_fd_readdir(dir_fd: wasi_unstable::Fd) { + let mut stat: wasi_unstable::FileStat = mem::zeroed(); + let status = wasi_fd_filestat_get(dir_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading scratch directory stats" + ); + + // Check the behavior in an empty directory + let mut dirs = exec_fd_readdir(dir_fd, wasi_unstable::DIRCOOKIE_START); + dirs.sort_by_key(|d| d.name.clone()); + assert_eq!(dirs.len(), 2, "expected two entries in an empty directory"); + let mut dirs = dirs.into_iter(); + + // the first entry should be `.` + let dir = dirs.next().expect("first entry is None"); + assert_eq!(dir.name, ".", "first name"); + assert_eq!( + dir.dirent.d_type, + wasi_unstable::FILETYPE_DIRECTORY, + "first type" + ); + assert_eq!(dir.dirent.d_ino, stat.st_ino); + assert_eq!(dir.dirent.d_namlen, 1); + + // the second entry should be `..` + let dir = dirs.next().expect("second entry is None"); + assert_eq!(dir.name, "..", "second name"); + assert_eq!( + dir.dirent.d_type, + wasi_unstable::FILETYPE_DIRECTORY, + "second type" + ); + + assert!( + dirs.next().is_none(), + "the directory should be seen as empty" + ); + + // Add a file and check the behavior + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats" + ); + + // Execute another readdir + let mut dirs = exec_fd_readdir(dir_fd, wasi_unstable::DIRCOOKIE_START); + assert_eq!(dirs.len(), 3, "expected three entries"); + // Save the data about the last entry. We need to do it before sorting. + let lastfile_cookie = dirs[1].dirent.d_next; + let lastfile_name = dirs[2].name.clone(); + dirs.sort_by_key(|d| d.name.clone()); + let mut dirs = dirs.into_iter(); + + let dir = dirs.next().expect("first entry is None"); + assert_eq!(dir.name, ".", "first name"); + let dir = dirs.next().expect("second entry is None"); + assert_eq!(dir.name, "..", "second name"); + let dir = dirs.next().expect("third entry is None"); + // check the file info + assert_eq!(dir.name, "file", "file name doesn't match"); + assert_eq!( + dir.dirent.d_type, + wasi_unstable::FILETYPE_REGULAR_FILE, + "type for the real file" + ); + assert_eq!(dir.dirent.d_ino, stat.st_ino); + + // check if cookie works as expected + let dirs = exec_fd_readdir(dir_fd, lastfile_cookie); + assert_eq!(dirs.len(), 1, "expected one entry"); + assert_eq!(dirs[0].name, lastfile_name, "name of the only entry"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_readdir(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/file_allocate.rs b/wasi-common/wasi-misc-tests/src/bin/file_allocate.rs new file mode 100644 index 0000000000..c1e3d6a07d --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/file_allocate.rs @@ -0,0 +1,125 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_filestat_get, wasi_path_open}; + +unsafe fn test_file_allocate(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Check file size + let mut stat = wasi_unstable::FileStat { + st_dev: 0, + st_ino: 0, + st_filetype: 0, + st_nlink: 0, + st_size: 0, + st_atim: 0, + st_mtim: 0, + st_ctim: 0, + }; + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats" + ); + assert_eq!(stat.st_size, 0, "file size should be 0"); + + // Allocate some size + assert!( + wasi_unstable::fd_allocate(file_fd, 0, 100).is_ok(), + "allocating size" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after initial allocation" + ); + assert_eq!(stat.st_size, 100, "file size should be 100"); + + // Allocate should not modify if less than current size + assert!( + wasi_unstable::fd_allocate(file_fd, 10, 10).is_ok(), + "allocating size less than current size" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after additional allocation was not required" + ); + assert_eq!( + stat.st_size, 100, + "file size should remain unchanged at 100" + ); + + // Allocate should modify if offset+len > current_len + assert!( + wasi_unstable::fd_allocate(file_fd, 90, 20).is_ok(), + "allocating size larger than current size" + ); + + let status = wasi_fd_filestat_get(file_fd, &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after additional allocation was required" + ); + assert_eq!( + stat.st_size, 110, + "file size should increase from 100 to 110" + ); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_allocate(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/file_pread_pwrite.rs b/wasi-common/wasi-misc-tests/src/bin/file_pread_pwrite.rs new file mode 100644 index 0000000000..8887ead5de --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/file_pread_pwrite.rs @@ -0,0 +1,131 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_pread, wasi_fd_pwrite, wasi_path_open}; + +unsafe fn test_file_pread_pwrite(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + let contents = &[0u8, 1, 2, 3]; + let ciovec = wasi_unstable::CIoVec { + buf: contents.as_ptr() as *const libc::c_void, + buf_len: contents.len(), + }; + let mut nwritten = 0; + status = wasi_fd_pwrite(file_fd, &mut [ciovec], 0, &mut nwritten); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "writing bytes at offset 0" + ); + assert_eq!(nwritten, 4, "nwritten bytes check"); + + let contents = &mut [0u8; 4]; + let iovec = wasi_unstable::IoVec { + buf: contents.as_mut_ptr() as *mut libc::c_void, + buf_len: contents.len(), + }; + let mut nread = 0; + status = wasi_fd_pread(file_fd, &[iovec], 0, &mut nread); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading bytes at offset 0" + ); + assert_eq!(nread, 4, "nread bytes check"); + assert_eq!(contents, &[0u8, 1, 2, 3], "written bytes equal read bytes"); + + let contents = &mut [0u8; 4]; + let iovec = wasi_unstable::IoVec { + buf: contents.as_mut_ptr() as *mut libc::c_void, + buf_len: contents.len(), + }; + let mut nread = 0; + status = wasi_fd_pread(file_fd, &[iovec], 2, &mut nread); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading bytes at offset 2" + ); + assert_eq!(nread, 2, "nread bytes check"); + assert_eq!(contents, &[2u8, 3, 0, 0], "file cursor was overwritten"); + + let contents = &[1u8, 0]; + let ciovec = wasi_unstable::CIoVec { + buf: contents.as_ptr() as *const libc::c_void, + buf_len: contents.len(), + }; + let mut nwritten = 0; + status = wasi_fd_pwrite(file_fd, &mut [ciovec], 2, &mut nwritten); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "writing bytes at offset 2" + ); + assert_eq!(nwritten, 2, "nwritten bytes check"); + + let contents = &mut [0u8; 4]; + let iovec = wasi_unstable::IoVec { + buf: contents.as_mut_ptr() as *mut libc::c_void, + buf_len: contents.len(), + }; + let mut nread = 0; + status = wasi_fd_pread(file_fd, &[iovec], 0, &mut nread); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading bytes at offset 0" + ); + assert_eq!(nread, 4, "nread bytes check"); + assert_eq!(contents, &[0u8, 1, 1, 0], "file cursor was overwritten"); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_pread_pwrite(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/file_seek_tell.rs b/wasi-common/wasi-misc-tests/src/bin/file_seek_tell.rs new file mode 100644 index 0000000000..fa7f0cd22d --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/file_seek_tell.rs @@ -0,0 +1,133 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_seek, wasi_fd_tell, wasi_fd_write, wasi_path_open}; + +unsafe fn test_file_seek_tell(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Check current offset + let mut offset: wasi_unstable::FileSize = 0; + status = wasi_fd_tell(file_fd, &mut offset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "getting initial file offset" + ); + assert_eq!(offset, 0, "current offset should be 0"); + + // Write to file + let buf = &[0u8; 100]; + let iov = wasi_unstable::CIoVec { + buf: buf.as_ptr() as *const _, + buf_len: buf.len(), + }; + let iovs = &[iov]; + let mut nwritten = 0; + status = wasi_fd_write(file_fd, iovs, &mut nwritten); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "writing to a file" + ); + assert_eq!(nwritten, 100, "should write 100 bytes to file"); + + // Check current offset + status = wasi_fd_tell(file_fd, &mut offset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "getting file offset after writing" + ); + assert_eq!(offset, 100, "offset after writing should be 100"); + + // Seek to middle of the file + let mut newoffset = 1; + status = wasi_fd_seek(file_fd, -50, wasi_unstable::WHENCE_CUR, &mut newoffset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "seeking to the middle of a file" + ); + assert_eq!( + newoffset, 50, + "offset after seeking to the middle should be at 50" + ); + + // Seek to the beginning of the file + status = wasi_fd_seek(file_fd, 0, wasi_unstable::WHENCE_SET, &mut newoffset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "seeking to the beginning of the file" + ); + assert_eq!( + newoffset, 0, + "offset after seeking to the beginning of the file should be at 0" + ); + + // Seek beyond the file should be possible + status = wasi_fd_seek(file_fd, 1000, wasi_unstable::WHENCE_CUR, &mut newoffset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "seeking beyond the end of the file" + ); + + // Seek before byte 0 is an error though + status = wasi_fd_seek(file_fd, -2000, wasi_unstable::WHENCE_CUR, &mut newoffset); + assert_eq!( + status, + wasi_unstable::raw::__WASI_EINVAL, + "seeking before byte 0 is an error" + ); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_seek_tell(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/file_unbuffered_write.rs b/wasi-common/wasi-misc-tests/src/bin/file_unbuffered_write.rs new file mode 100644 index 0000000000..270df3db83 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/file_unbuffered_write.rs @@ -0,0 +1,116 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd, create_file}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_read, wasi_fd_write, wasi_path_open}; + +unsafe fn test_file_unbuffered_write(dir_fd: wasi_unstable::Fd) { + // Create file + create_file(dir_fd, "file"); + + // Open file for reading + let mut fd_read = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "file", + 0, + wasi_unstable::RIGHT_FD_READ, + 0, + 0, + &mut fd_read, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + fd_read, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Open the same file but for writing + let mut fd_write = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open( + dir_fd, + 0, + "file", + 0, + wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut fd_write, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + fd_write, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Write to file + let contents = &[1u8]; + let ciovec = wasi_unstable::CIoVec { + buf: contents.as_ptr() as *const libc::c_void, + buf_len: contents.len(), + }; + let mut nwritten = 0; + status = wasi_fd_write(fd_write, &[ciovec], &mut nwritten); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "writing byte to file" + ); + assert_eq!(nwritten, 1, "nwritten bytes check"); + + // Read from file + let contents = &mut [0u8; 1]; + let iovec = wasi_unstable::IoVec { + buf: contents.as_mut_ptr() as *mut libc::c_void, + buf_len: contents.len(), + }; + let mut nread = 0; + status = wasi_fd_read(fd_read, &[iovec], &mut nread); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading bytes from file" + ); + assert_eq!(nread, 1, "nread bytes check"); + assert_eq!(contents, &[1u8], "written bytes equal read bytes"); + + // Clean up + close_fd(fd_write); + close_fd(fd_read); + cleanup_file(dir_fd, "file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_unbuffered_write(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/interesting_paths.rs b/wasi-common/wasi-misc-tests/src/bin/interesting_paths.rs new file mode 100644 index 0000000000..cfb68bc81a --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/interesting_paths.rs @@ -0,0 +1,173 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{close_fd, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::{ + wasi_path_open, wasi_path_remove_directory, wasi_path_unlink_file, +}; + +unsafe fn test_interesting_paths(dir_fd: wasi_unstable::Fd, arg: &str) { + // Create a directory in the scratch directory. + create_dir(dir_fd, "dir"); + + // Create a directory in the directory we just created. + create_dir(dir_fd, "dir/nested"); + + // Create a file in the nested directory. + create_file(dir_fd, "dir/nested/file"); + + // Now open it with an absolute path. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open(dir_fd, 0, "/dir/nested/file", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTCAPABLE, + "opening a file with an absolute path" + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Now open it with a path containing "..". + status = wasi_path_open( + dir_fd, + 0, + "dir/.//nested/../../dir/nested/../nested///./file", + 0, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file with \"..\" in the path" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + close_fd(file_fd); + + // Now open it with a trailing NUL. + status = wasi_path_open(dir_fd, 0, "dir/nested/file\0", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_EILSEQ, + "opening a file with a trailing NUL" + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Now open it with a trailing slash. + status = wasi_path_open(dir_fd, 0, "dir/nested/file/", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTDIR, + "opening a file with a trailing slash" + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Now open it with trailing slashes. + status = wasi_path_open(dir_fd, 0, "dir/nested/file///", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTDIR, + "opening a file with trailing slashes" + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Now open the directory with a trailing slash. + status = wasi_path_open(dir_fd, 0, "dir/nested/", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a directory with a trailing slash" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + close_fd(file_fd); + + // Now open the directory with trailing slashes. + status = wasi_path_open(dir_fd, 0, "dir/nested///", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a directory with trailing slashes" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + close_fd(file_fd); + + // Now open it with a path containing too many ".."s. + let bad_path = format!("dir/nested/../../../{}/dir/nested/file", arg); + status = wasi_path_open(dir_fd, 0, &bad_path, 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTCAPABLE, + "opening a file with too many \"..\"s in the path" + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + assert!( + wasi_path_unlink_file(dir_fd, "dir/nested/file").is_ok(), + "unlink_file on a symlink should succeed" + ); + assert!( + wasi_path_remove_directory(dir_fd, "dir/nested").is_ok(), + "remove_directory on a directory should succeed" + ); + assert!( + wasi_path_remove_directory(dir_fd, "dir").is_ok(), + "remove_directory on a directory should succeed" + ); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_interesting_paths(dir_fd, &arg) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/isatty.rs b/wasi-common/wasi-misc-tests/src/bin/isatty.rs new file mode 100644 index 0000000000..945998a203 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/isatty.rs @@ -0,0 +1,63 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::wasi_path_open; + +unsafe fn test_isatty(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory and test if it's a tty. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + assert_eq!( + libc::isatty(file_fd as std::os::raw::c_int), + 0, + "file is a tty" + ); + close_fd(file_fd); + + cleanup_file(dir_fd, "file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_isatty(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/nofollow_errors.rs b/wasi-common/wasi-misc-tests/src/bin/nofollow_errors.rs new file mode 100644 index 0000000000..5dd31f66d0 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/nofollow_errors.rs @@ -0,0 +1,177 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::{ + wasi_path_open, wasi_path_remove_directory, wasi_path_symlink, +}; + +unsafe fn test_nofollow_errors(dir_fd: wasi_unstable::Fd) { + // Create a directory for the symlink to point to. + create_dir(dir_fd, "target"); + + // Create a symlink. + assert!( + wasi_path_symlink("target", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Try to open it as a directory with O_NOFOLLOW again. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "symlink", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ELOOP, + "opening a directory symlink as a directory", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Try to open it with just O_NOFOLLOW. + status = wasi_path_open(dir_fd, 0, "symlink", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ELOOP, + "opening a symlink with O_NOFOLLOW should return ELOOP", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Try to open it as a directory without O_NOFOLLOW. + status = wasi_path_open( + dir_fd, + wasi_unstable::LOOKUP_SYMLINK_FOLLOW, + "symlink", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a symlink as a directory" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + close_fd(file_fd); + + // Replace the target directory with a file. + cleanup_file(dir_fd, "symlink"); + + assert!( + wasi_path_remove_directory(dir_fd, "target").is_ok(), + "remove_directory on a directory should succeed" + ); + create_file(dir_fd, "target"); + + assert!( + wasi_path_symlink("target", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Try to open it as a directory with O_NOFOLLOW again. + status = wasi_path_open( + dir_fd, + 0, + "symlink", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ELOOP, + "opening a directory symlink as a directory", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Try to open it with just O_NOFOLLOW. + status = wasi_path_open(dir_fd, 0, "symlink", 0, 0, 0, 0, &mut file_fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ELOOP, + "opening a symlink with O_NOFOLLOW should return ELOOP", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Try to open it as a directory without O_NOFOLLOW. + status = wasi_path_open( + dir_fd, + wasi_unstable::LOOKUP_SYMLINK_FOLLOW, + "symlink", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTDIR, + "opening a symlink to a file as a directory", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Clean up. + cleanup_file(dir_fd, "target"); + cleanup_file(dir_fd, "symlink"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_nofollow_errors(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/path_filestat.rs b/wasi-common/wasi-misc-tests/src/bin/path_filestat.rs new file mode 100644 index 0000000000..2c831466e4 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/path_filestat.rs @@ -0,0 +1,184 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd}; +use wasi_misc_tests::wasi_wrappers::{ + wasi_fd_fdstat_get, wasi_path_filestat_get, wasi_path_filestat_set_times, wasi_path_open, +}; + +unsafe fn test_path_filestat(dir_fd: wasi_unstable::Fd) { + let mut fdstat: wasi_unstable::FdStat = std::mem::zeroed(); + let status = wasi_fd_fdstat_get(dir_fd, &mut fdstat); + assert_eq!(status, wasi_unstable::raw::__WASI_ESUCCESS, "fd_fdstat_get"); + + assert_ne!( + fdstat.fs_rights_base & wasi_unstable::RIGHT_PATH_FILESTAT_GET, + 0, + "the scratch directory should have RIGHT_PATH_FILESTAT_GET as base right", + ); + assert_ne!( + fdstat.fs_rights_inheriting & wasi_unstable::RIGHT_PATH_FILESTAT_GET, + 0, + "the scratch directory should have RIGHT_PATH_FILESTAT_GET as base right", + ); + + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let filename = "file"; + let status = wasi_path_open( + dir_fd, + 0, + filename, + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ + | wasi_unstable::RIGHT_FD_WRITE + | wasi_unstable::RIGHT_PATH_FILESTAT_GET, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + let status = wasi_fd_fdstat_get(file_fd, &mut fdstat); + assert_eq!(status, wasi_unstable::raw::__WASI_ESUCCESS, "fd_fdstat_get"); + + assert_eq!( + fdstat.fs_rights_base & wasi_unstable::RIGHT_PATH_FILESTAT_GET, + 0, + "files shouldn't have rights for path_* syscalls even if manually given", + ); + assert_eq!( + fdstat.fs_rights_inheriting & wasi_unstable::RIGHT_PATH_FILESTAT_GET, + 0, + "files shouldn't have rights for path_* syscalls even if manually given", + ); + + // Check file size + let mut stat = wasi_unstable::FileStat { + st_dev: 0, + st_ino: 0, + st_filetype: 0, + st_nlink: 0, + st_size: 0, + st_atim: 0, + st_mtim: 0, + st_ctim: 0, + }; + let status = wasi_path_filestat_get(dir_fd, 0, filename, filename.len(), &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats" + ); + assert_eq!(stat.st_size, 0, "file size should be 0"); + + // Check path_filestat_set_times + let old_atim = stat.st_atim; + let new_mtim = stat.st_mtim - 100; + assert!( + wasi_path_filestat_set_times( + dir_fd, + 0, + filename, + // on purpose: the syscall should not touch atim, because + // neither of the ATIM flags is set + new_mtim, + new_mtim, + wasi_unstable::FILESTAT_SET_MTIM, + ) + .is_ok(), + "path_filestat_set_times should succeed" + ); + + let status = wasi_path_filestat_get(dir_fd, 0, filename, filename.len(), &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after path_filestat_set_times" + ); + assert_eq!(stat.st_mtim, new_mtim, "mtim should change"); + assert_eq!(stat.st_atim, old_atim, "atim should not change"); + + assert_eq!( + wasi_path_filestat_set_times( + dir_fd, + 0, + filename, + new_mtim, + new_mtim, + wasi_unstable::FILESTAT_SET_MTIM | wasi_unstable::FILESTAT_SET_MTIM_NOW, + ), + Err(wasi_unstable::EINVAL), + "MTIM & MTIM_NOW can't both be set" + ); + + // check if the times were untouched + let status = wasi_path_filestat_get(dir_fd, 0, filename, filename.len(), &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after EINVAL fd_filestat_set_times" + ); + assert_eq!(stat.st_mtim, new_mtim, "mtim should not change"); + assert_eq!(stat.st_atim, old_atim, "atim should not change"); + + let new_atim = old_atim - 100; + assert_eq!( + wasi_path_filestat_set_times( + dir_fd, + 0, + filename, + new_atim, + new_atim, + wasi_unstable::FILESTAT_SET_ATIM | wasi_unstable::FILESTAT_SET_ATIM_NOW, + ), + Err(wasi_unstable::EINVAL), + "ATIM & ATIM_NOW can't both be set" + ); + + // check if the times were untouched + let status = wasi_path_filestat_get(dir_fd, 0, filename, filename.len(), &mut stat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading file stats after EINVAL path_filestat_set_times" + ); + assert_eq!(stat.st_mtim, new_mtim, "mtim should not change"); + assert_eq!(stat.st_atim, old_atim, "atim should not change"); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_filestat(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/path_open_dirfd_not_dir.rs b/wasi-common/wasi-misc-tests/src/bin/path_open_dirfd_not_dir.rs new file mode 100644 index 0000000000..2b99971d9c --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/path_open_dirfd_not_dir.rs @@ -0,0 +1,67 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::close_fd; +use wasi_misc_tests::wasi_wrappers::wasi_path_open; + +unsafe fn test_dirfd_not_dir(dir_fd: wasi_unstable::Fd) { + // Open a file. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + + // Now try to open a file underneath it as if it were a directory. + let mut new_file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open( + file_fd, + 0, + "foo", + wasi_unstable::O_CREAT, + 0, + 0, + 0, + &mut new_file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTDIR, + "non-directory base fd should get ENOTDIR" + ); + close_fd(file_fd); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_dirfd_not_dir(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/path_rename.rs b/wasi-common/wasi-misc-tests/src/bin/path_rename.rs new file mode 100644 index 0000000000..f7e5b7af44 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/path_rename.rs @@ -0,0 +1,259 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, cleanup_file, close_fd, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::{wasi_path_open, wasi_path_rename}; + +unsafe fn test_path_rename(dir_fd: wasi_unstable::Fd) { + // First, try renaming a dir to nonexistent path + // Create source directory + create_dir(dir_fd, "source"); + + // Try renaming the directory + assert!( + wasi_path_rename(dir_fd, "source", dir_fd, "target").is_ok(), + "renaming a directory" + ); + + // Check that source directory doesn't exist anymore + let mut fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "source", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOENT, + "opening a nonexistent path as a directory" + ); + assert_eq!( + fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Check that target directory exists + status = wasi_path_open( + dir_fd, + 0, + "target", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening renamed path as a directory" + ); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + close_fd(fd); + cleanup_dir(dir_fd, "target"); + + // Now, try renaming renaming a dir to existing empty dir + create_dir(dir_fd, "source"); + create_dir(dir_fd, "target"); + + assert!( + wasi_path_rename(dir_fd, "source", dir_fd, "target").is_ok(), + "renaming a directory" + ); + + // Check that source directory doesn't exist anymore + fd = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open( + dir_fd, + 0, + "source", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOENT, + "opening a nonexistent path as a directory" + ); + assert_eq!( + fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Check that target directory exists + status = wasi_path_open( + dir_fd, + 0, + "target", + wasi_unstable::O_DIRECTORY, + 0, + 0, + 0, + &mut fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening renamed path as a directory" + ); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + close_fd(fd); + cleanup_dir(dir_fd, "target"); + + // Now, try renaming a dir to existing non-empty dir + create_dir(dir_fd, "source"); + create_dir(dir_fd, "target"); + create_file(dir_fd, "target/file"); + + assert_eq!( + wasi_path_rename(dir_fd, "source", dir_fd, "target"), + Err(wasi_unstable::ENOTEMPTY), + "renaming directory to a nonempty directory" + ); + + // Try renaming dir to a file + assert_eq!( + wasi_path_rename(dir_fd, "source", dir_fd, "target/file"), + Err(wasi_unstable::ENOTDIR), + "renaming directory to a file" + ); + + cleanup_file(dir_fd, "target/file"); + cleanup_dir(dir_fd, "target"); + cleanup_dir(dir_fd, "source"); + + // Now, try renaming a file to a nonexistent path + create_file(dir_fd, "source"); + + assert!( + wasi_path_rename(dir_fd, "source", dir_fd, "target").is_ok(), + "renaming a file" + ); + + // Check that source file doesn't exist anymore + fd = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open(dir_fd, 0, "source", 0, 0, 0, 0, &mut fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOENT, + "opening a nonexistent path" + ); + assert_eq!( + fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Check that target file exists + status = wasi_path_open(dir_fd, 0, "target", 0, 0, 0, 0, &mut fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening renamed path" + ); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + close_fd(fd); + cleanup_file(dir_fd, "target"); + + // Now, try renaming file to an existing file + create_file(dir_fd, "source"); + create_file(dir_fd, "target"); + + assert!( + wasi_path_rename(dir_fd, "source", dir_fd, "target").is_ok(), + "renaming file to another existing file" + ); + + // Check that source file doesn't exist anymore + fd = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open(dir_fd, 0, "source", 0, 0, 0, 0, &mut fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOENT, + "opening a nonexistent path" + ); + assert_eq!( + fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + + // Check that target file exists + status = wasi_path_open(dir_fd, 0, "target", 0, 0, 0, 0, &mut fd); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening renamed path" + ); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + close_fd(fd); + cleanup_file(dir_fd, "target"); + + // Try renaming to an (empty) directory instead + create_file(dir_fd, "source"); + create_dir(dir_fd, "target"); + + assert_eq!( + wasi_path_rename(dir_fd, "source", dir_fd, "target"), + Err(wasi_unstable::EISDIR), + "renaming file to existing directory" + ); + + cleanup_dir(dir_fd, "target"); + cleanup_file(dir_fd, "source"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_rename(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/path_rename_trailing_slashes.rs b/wasi-common/wasi-misc-tests/src/bin/path_rename_trailing_slashes.rs new file mode 100644 index 0000000000..17a9785735 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/path_rename_trailing_slashes.rs @@ -0,0 +1,68 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, cleanup_file, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::wasi_path_rename; + +unsafe fn test_path_rename_trailing_slashes(dir_fd: wasi_unstable::Fd) { + // Test renaming a file with a trailing slash in the name. + create_file(dir_fd, "source"); + assert_eq!( + wasi_path_rename(dir_fd, "source/", dir_fd, "target"), + Err(wasi_unstable::ENOTDIR), + "renaming a file with a trailing slash in the source name" + ); + assert_eq!( + wasi_path_rename(dir_fd, "source", dir_fd, "target/"), + Err(wasi_unstable::ENOTDIR), + "renaming a file with a trailing slash in the destination name" + ); + assert_eq!( + wasi_path_rename(dir_fd, "source/", dir_fd, "target/"), + Err(wasi_unstable::ENOTDIR), + "renaming a file with a trailing slash in the source and destination names" + ); + cleanup_file(dir_fd, "source"); + + // Test renaming a directory with a trailing slash in the name. + create_dir(dir_fd, "source"); + assert_eq!( + wasi_path_rename(dir_fd, "source/", dir_fd, "target"), + Ok(()), + "renaming a directory with a trailing slash in the source name" + ); + assert_eq!( + wasi_path_rename(dir_fd, "target", dir_fd, "source/"), + Ok(()), + "renaming a directory with a trailing slash in the destination name" + ); + assert_eq!( + wasi_path_rename(dir_fd, "source/", dir_fd, "target/"), + Ok(()), + "renaming a directory with a trailing slash in the source and destination names" + ); + cleanup_dir(dir_fd, "target"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_rename_trailing_slashes(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/path_symlink_trailing_slashes.rs b/wasi-common/wasi-misc-tests/src/bin/path_symlink_trailing_slashes.rs new file mode 100644 index 0000000000..c10a61c723 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/path_symlink_trailing_slashes.rs @@ -0,0 +1,81 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, cleanup_file, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::wasi_path_symlink; + +unsafe fn test_path_symlink_trailing_slashes(dir_fd: wasi_unstable::Fd) { + // Link destination shouldn't end with a slash. + assert_eq!( + wasi_path_symlink("source", dir_fd, "target/"), + Err(wasi_unstable::ENOENT), + "link destination ending with a slash" + ); + + // Without the trailing slash, this should succeed. + assert_eq!( + wasi_path_symlink("source", dir_fd, "target"), + Ok(()), + "link destination ending with a slash" + ); + cleanup_file(dir_fd, "target"); + + // Link destination already exists, target has trailing slash. + create_dir(dir_fd, "target"); + assert_eq!( + wasi_path_symlink("source", dir_fd, "target/"), + Err(wasi_unstable::EEXIST), + "link destination already exists" + ); + cleanup_dir(dir_fd, "target"); + + // Link destination already exists, target has no trailing slash. + create_dir(dir_fd, "target"); + assert_eq!( + wasi_path_symlink("source", dir_fd, "target"), + Err(wasi_unstable::EEXIST), + "link destination already exists" + ); + cleanup_dir(dir_fd, "target"); + + // Link destination already exists, target has trailing slash. + create_file(dir_fd, "target"); + assert_eq!( + wasi_path_symlink("source", dir_fd, "target/"), + Err(wasi_unstable::EEXIST), + "link destination already exists" + ); + cleanup_file(dir_fd, "target"); + + // Link destination already exists, target has no trailing slash. + create_file(dir_fd, "target"); + assert_eq!( + wasi_path_symlink("source", dir_fd, "target"), + Err(wasi_unstable::EEXIST), + "link destination already exists" + ); + cleanup_file(dir_fd, "target"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_symlink_trailing_slashes(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/poll_oneoff.rs b/wasi-common/wasi-misc-tests/src/bin/poll_oneoff.rs new file mode 100644 index 0000000000..c303c08926 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/poll_oneoff.rs @@ -0,0 +1,271 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, mem::MaybeUninit, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::{ + open_scratch_directory, + utils::{cleanup_file, close_fd}, + wasi_wrappers::wasi_path_open, +}; + +const CLOCK_ID: wasi_unstable::Userdata = 0x0123_45678; + +unsafe fn poll_oneoff_impl( + in_: &[wasi_unstable::Subscription], + nexpected: usize, +) -> Vec { + let mut out: Vec = Vec::new(); + out.resize_with(in_.len(), || { + MaybeUninit::::zeroed().assume_init() + }); + let res = wasi_unstable::poll_oneoff(&in_, out.as_mut_slice()); + let res = res.expect("poll_oneoff should succeed"); + assert_eq!( + res, nexpected, + "poll_oneoff should return {} events", + nexpected + ); + out +} + +unsafe fn test_timeout() { + let clock = wasi_unstable::raw::__wasi_subscription_u_clock_t { + identifier: CLOCK_ID, + clock_id: wasi_unstable::CLOCK_MONOTONIC, + timeout: 5_000_000u64, // 5 milliseconds + precision: 0, + flags: 0, + }; + let in_ = [wasi_unstable::Subscription { + userdata: CLOCK_ID, + type_: wasi_unstable::EVENTTYPE_CLOCK, + u: wasi_unstable::raw::__wasi_subscription_u { clock }, + }]; + let out = poll_oneoff_impl(&in_, 1); + let event = &out[0]; + assert_eq!( + event.userdata, CLOCK_ID, + "the event.userdata should contain clock_id specified by the user" + ); + assert_eq!( + event.error, + wasi_unstable::raw::__WASI_ESUCCESS, + "the event.error should be set to ESUCCESS" + ); + assert_eq!( + event.type_, + wasi_unstable::EVENTTYPE_CLOCK, + "the event.type_ should equal clock" + ); +} + +unsafe fn test_stdin_read() { + let clock = wasi_unstable::raw::__wasi_subscription_u_clock_t { + identifier: CLOCK_ID, + clock_id: wasi_unstable::CLOCK_MONOTONIC, + timeout: 5_000_000u64, // 5 milliseconds + precision: 0, + flags: 0, + }; + let fd_readwrite = wasi_unstable::raw::__wasi_subscription_u_fd_readwrite_t { + fd: wasi_unstable::STDIN_FD, + }; + let in_ = [ + wasi_unstable::Subscription { + userdata: CLOCK_ID, + type_: wasi_unstable::EVENTTYPE_CLOCK, + u: wasi_unstable::raw::__wasi_subscription_u { clock }, + }, + wasi_unstable::Subscription { + userdata: 1, + type_: wasi_unstable::EVENTTYPE_FD_READ, + u: wasi_unstable::raw::__wasi_subscription_u { fd_readwrite }, + }, + ]; + let out = poll_oneoff_impl(&in_, 1); + let event = &out[0]; + assert_eq!( + event.userdata, CLOCK_ID, + "the event.userdata should contain clock_id specified by the user" + ); + assert_eq!( + event.error, + wasi_unstable::raw::__WASI_ESUCCESS, + "the event.error should be set to ESUCCESS" + ); + assert_eq!( + event.type_, + wasi_unstable::EVENTTYPE_CLOCK, + "the event.type_ should equal clock" + ); +} + +unsafe fn test_stdout_stderr_write() { + let stdout_readwrite = wasi_unstable::raw::__wasi_subscription_u_fd_readwrite_t { + fd: wasi_unstable::STDOUT_FD, + }; + let stderr_readwrite = wasi_unstable::raw::__wasi_subscription_u_fd_readwrite_t { + fd: wasi_unstable::STDERR_FD, + }; + let in_ = [ + wasi_unstable::Subscription { + userdata: 1, + type_: wasi_unstable::EVENTTYPE_FD_WRITE, + u: wasi_unstable::raw::__wasi_subscription_u { + fd_readwrite: stdout_readwrite, + }, + }, + wasi_unstable::Subscription { + userdata: 2, + type_: wasi_unstable::EVENTTYPE_FD_WRITE, + u: wasi_unstable::raw::__wasi_subscription_u { + fd_readwrite: stderr_readwrite, + }, + }, + ]; + let out = poll_oneoff_impl(&in_, 2); + assert_eq!( + out[0].userdata, 1, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[0].error, + wasi_unstable::raw::__WASI_ESUCCESS, + "the event.error should be set to {}", + wasi_unstable::raw::__WASI_ESUCCESS + ); + assert_eq!( + out[0].type_, + wasi_unstable::EVENTTYPE_FD_WRITE, + "the event.type_ should equal FD_WRITE" + ); + assert_eq!( + out[1].userdata, 2, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[1].error, + wasi_unstable::raw::__WASI_ESUCCESS, + "the event.error should be set to {}", + wasi_unstable::raw::__WASI_ESUCCESS + ); + assert_eq!( + out[1].type_, + wasi_unstable::EVENTTYPE_FD_WRITE, + "the event.type_ should equal FD_WRITE" + ); +} + +unsafe fn test_fd_readwrite(fd: wasi_unstable::Fd, error_code: wasi_unstable::raw::__wasi_errno_t) { + let fd_readwrite = wasi_unstable::raw::__wasi_subscription_u_fd_readwrite_t { fd }; + let in_ = [ + wasi_unstable::Subscription { + userdata: 1, + type_: wasi_unstable::EVENTTYPE_FD_READ, + u: wasi_unstable::raw::__wasi_subscription_u { fd_readwrite }, + }, + wasi_unstable::Subscription { + userdata: 2, + type_: wasi_unstable::EVENTTYPE_FD_WRITE, + u: wasi_unstable::raw::__wasi_subscription_u { fd_readwrite }, + }, + ]; + let out = poll_oneoff_impl(&in_, 2); + assert_eq!( + out[0].userdata, 1, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[0].error, error_code, + "the event.error should be set to {}", + error_code + ); + assert_eq!( + out[0].type_, + wasi_unstable::EVENTTYPE_FD_READ, + "the event.type_ should equal FD_READ" + ); + assert_eq!( + out[1].userdata, 2, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[1].error, error_code, + "the event.error should be set to {}", + error_code + ); + assert_eq!( + out[1].type_, + wasi_unstable::EVENTTYPE_FD_WRITE, + "the event.type_ should equal FD_WRITE" + ); +} + +unsafe fn test_fd_readwrite_valid_fd(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + test_fd_readwrite(file_fd, wasi_unstable::raw::__WASI_ESUCCESS); + + close_fd(file_fd); + cleanup_file(dir_fd, "file"); +} + +unsafe fn test_fd_readwrite_invalid_fd() { + test_fd_readwrite( + wasi_unstable::Fd::max_value(), + wasi_unstable::raw::__WASI_EBADF, + ) +} + +unsafe fn test_poll_oneoff(dir_fd: wasi_unstable::Fd) { + test_timeout(); + // NB we assume that stdin/stdout/stderr are valid and open + // for the duration of the test case + test_stdin_read(); + test_stdout_stderr_write(); + test_fd_readwrite_valid_fd(dir_fd); + test_fd_readwrite_invalid_fd(); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_poll_oneoff(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/readlink.rs b/wasi-common/wasi-misc-tests/src/bin/readlink.rs new file mode 100644 index 0000000000..23ee02d56b --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/readlink.rs @@ -0,0 +1,72 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, create_file}; +use wasi_misc_tests::wasi_wrappers::{wasi_path_readlink, wasi_path_symlink}; + +unsafe fn test_readlink(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + create_file(dir_fd, "target"); + + // Create a symlink + assert!( + wasi_path_symlink("target", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Read link into the buffer + let buf = &mut [0u8; 10]; + let mut bufused: usize = 0; + let mut status = wasi_path_readlink(dir_fd, "symlink", buf, &mut bufused); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "readlink should succeed" + ); + assert_eq!(bufused, 6, "should use 6 bytes of the buffer"); + assert_eq!(&buf[..6], b"target", "buffer should contain 'target'"); + assert_eq!( + &buf[6..], + &[0u8; 4], + "the remaining bytes should be untouched" + ); + + // Read link into smaller buffer than the actual link's length + let buf = &mut [0u8; 4]; + let mut bufused: usize = 0; + status = wasi_path_readlink(dir_fd, "symlink", buf, &mut bufused); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "readlink should succeed" + ); + assert_eq!(bufused, 4, "should use all 4 bytes of the buffer"); + assert_eq!(buf, b"targ", "buffer should contain 'targ'"); + + // Clean up. + cleanup_file(dir_fd, "target"); + cleanup_file(dir_fd, "symlink"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_readlink(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/readlink_no_buffer.rs b/wasi-common/wasi-misc-tests/src/bin/readlink_no_buffer.rs new file mode 100644 index 0000000000..695aff6951 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/readlink_no_buffer.rs @@ -0,0 +1,51 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::cleanup_file; +use wasi_misc_tests::wasi_wrappers::{wasi_path_readlink, wasi_path_symlink}; + +unsafe fn test_readlink_no_buffer(dir_fd: wasi_unstable::Fd) { + // First create a dangling symlink. + assert!( + wasi_path_symlink("target", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Readlink it into a non-existent buffer. + let mut bufused: usize = 1; + let status = wasi_path_readlink(dir_fd, "symlink", &mut [], &mut bufused); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "readlink with a 0-sized buffer should succeed" + ); + assert_eq!( + bufused, 0, + "readlink with a 0-sized buffer should return 'bufused' 0" + ); + + // Clean up. + cleanup_file(dir_fd, "symlink"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_readlink_no_buffer(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/remove_directory_trailing_slashes.rs b/wasi-common/wasi-misc-tests/src/bin/remove_directory_trailing_slashes.rs new file mode 100644 index 0000000000..85f83eb0fd --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/remove_directory_trailing_slashes.rs @@ -0,0 +1,68 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::wasi_path_remove_directory; + +unsafe fn test_remove_directory_trailing_slashes(dir_fd: wasi_unstable::Fd) { + // Create a directory in the scratch directory. + create_dir(dir_fd, "dir"); + + // Test that removing it succeeds. + assert_eq!( + wasi_path_remove_directory(dir_fd, "dir"), + Ok(()), + "remove_directory on a directory should succeed" + ); + + create_dir(dir_fd, "dir"); + + // Test that removing it with a trailing flash succeeds. + assert_eq!( + wasi_path_remove_directory(dir_fd, "dir/"), + Ok(()), + "remove_directory with a trailing slash on a directory should succeed" + ); + + // Create a temporary file. + create_file(dir_fd, "file"); + + // Test that removing it with no trailing flash fails. + assert_eq!( + wasi_path_remove_directory(dir_fd, "file"), + Err(wasi_unstable::ENOTDIR), + "remove_directory without a trailing slash on a file should fail" + ); + + // Test that removing it with a trailing flash fails. + assert_eq!( + wasi_path_remove_directory(dir_fd, "file/"), + Err(wasi_unstable::ENOTDIR), + "remove_directory with a trailing slash on a file should fail" + ); + + cleanup_file(dir_fd, "file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_remove_directory_trailing_slashes(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/remove_nonempty_directory.rs b/wasi-common/wasi-misc-tests/src/bin/remove_nonempty_directory.rs new file mode 100644 index 0000000000..8f990d6958 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/remove_nonempty_directory.rs @@ -0,0 +1,50 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, create_dir}; +use wasi_misc_tests::wasi_wrappers::wasi_path_remove_directory; + +unsafe fn test_remove_nonempty_directory(dir_fd: wasi_unstable::Fd) { + // Create a directory in the scratch directory. + create_dir(dir_fd, "dir"); + + // Create a directory in the directory we just created. + create_dir(dir_fd, "dir/nested"); + + // Test that attempting to unlink the first directory returns the expected error code. + assert_eq!( + wasi_path_remove_directory(dir_fd, "dir"), + Err(wasi_unstable::ENOTEMPTY), + "remove_directory on a directory should return ENOTEMPTY", + ); + + // Removing the directories. + assert!( + wasi_path_remove_directory(dir_fd, "dir/nested").is_ok(), + "remove_directory on a nested directory should succeed", + ); + cleanup_dir(dir_fd, "dir"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_remove_nonempty_directory(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/renumber.rs b/wasi-common/wasi-misc-tests/src/bin/renumber.rs new file mode 100644 index 0000000000..7e4a097c0a --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/renumber.rs @@ -0,0 +1,131 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, mem, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::close_fd; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_fdstat_get, wasi_path_open}; + +unsafe fn test_renumber(dir_fd: wasi_unstable::Fd) { + let pre_fd: wasi_unstable::Fd = (libc::STDERR_FILENO + 1) as wasi_unstable::Fd; + + assert_gt!(dir_fd, pre_fd, "dir_fd number"); + + // Create a file in the scratch directory. + let mut fd_from = wasi_unstable::Fd::max_value() - 1; + let mut status = wasi_path_open( + dir_fd, + 0, + "file1", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut fd_from, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + fd_from, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Get fd_from fdstat attributes + let mut fdstat_from: wasi_unstable::FdStat = mem::zeroed(); + status = wasi_fd_fdstat_get(fd_from, &mut fdstat_from); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on the open file descriptor" + ); + + // Create another file in the scratch directory. + let mut fd_to = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open( + dir_fd, + 0, + "file2", + wasi_unstable::O_CREAT, + wasi_unstable::RIGHT_FD_READ | wasi_unstable::RIGHT_FD_WRITE, + 0, + 0, + &mut fd_to, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "opening a file" + ); + assert_gt!( + fd_to, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + + // Renumber fd of file1 into fd of file2 + assert!( + wasi_unstable::fd_renumber(fd_from, fd_to).is_ok(), + "renumbering two descriptors", + ); + + // Ensure that fd_from is closed + assert_eq!( + wasi_unstable::fd_close(fd_from), + Err(wasi_unstable::EBADF), + "closing already closed file descriptor" + ); + + // Ensure that fd_to is still open. + let mut fdstat_to: wasi_unstable::FdStat = mem::zeroed(); + status = wasi_fd_fdstat_get(fd_to, &mut fdstat_to); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on the open file descriptor" + ); + assert_eq!( + fdstat_from.fs_filetype, fdstat_to.fs_filetype, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_flags, fdstat_to.fs_flags, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_rights_base, fdstat_to.fs_rights_base, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_rights_inheriting, fdstat_to.fs_rights_inheriting, + "expected fd_to have the same fdstat as fd_from" + ); + + close_fd(fd_to); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_renumber(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/sched_yield.rs b/wasi-common/wasi-misc-tests/src/bin/sched_yield.rs new file mode 100644 index 0000000000..d498f88495 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/sched_yield.rs @@ -0,0 +1,10 @@ +use wasi::wasi_unstable; + +fn test_sched_yield() { + assert!(wasi_unstable::sched_yield().is_ok(), "sched_yield"); +} + +fn main() { + // Run tests + test_sched_yield() +} diff --git a/wasi-common/wasi-misc-tests/src/bin/symlink_loop.rs b/wasi-common/wasi-misc-tests/src/bin/symlink_loop.rs new file mode 100644 index 0000000000..6972f5ad62 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/symlink_loop.rs @@ -0,0 +1,47 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::cleanup_file; +use wasi_misc_tests::wasi_wrappers::{wasi_path_open, wasi_path_symlink}; + +unsafe fn test_symlink_loop(dir_fd: wasi_unstable::Fd) { + // Create a self-referencing symlink. + assert!( + wasi_path_symlink("symlink", dir_fd, "symlink").is_ok(), + "creating a symlink" + ); + + // Try to open it. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + assert_eq!( + wasi_path_open(dir_fd, 0, "symlink", 0, 0, 0, 0, &mut file_fd), + wasi_unstable::raw::__WASI_ELOOP, + "opening a self-referencing symlink", + ); + + // Clean up. + cleanup_file(dir_fd, "symlink"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_symlink_loop(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/truncation_rights.rs b/wasi-common/wasi-misc-tests/src/bin/truncation_rights.rs new file mode 100644 index 0000000000..e400c4e2a5 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/truncation_rights.rs @@ -0,0 +1,157 @@ +use std::{env, mem, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_file, close_fd, create_file}; +use wasi_misc_tests::wasi_wrappers::{wasi_fd_fdstat_get, wasi_path_open}; + +unsafe fn test_truncation_rights(dir_fd: wasi_unstable::Fd) { + // Create a file in the scratch directory. + create_file(dir_fd, "file"); + + // Get the rights for the scratch directory. + let mut dir_fdstat: wasi_unstable::FdStat = mem::zeroed(); + let mut status = wasi_fd_fdstat_get(dir_fd, &mut dir_fdstat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "calling fd_fdstat on the scratch directory" + ); + assert_eq!( + dir_fdstat.fs_filetype, + wasi_unstable::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + assert_eq!( + dir_fdstat.fs_flags, 0, + "expected the scratch directory to have no special flags", + ); + assert_eq!( + dir_fdstat.fs_rights_base & wasi_unstable::RIGHT_FD_FILESTAT_SET_SIZE, + 0, + "directories shouldn't have the fd_filestat_set_size right", + ); + + // If we have the right to set sizes from paths, test that it works. + if (dir_fdstat.fs_rights_base & wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE) == 0 { + eprintln!("implementation doesn't support setting file sizes, skipping"); + } else { + // Test that we can truncate the file. + let mut file_fd: wasi_unstable::Fd = wasi_unstable::Fd::max_value() - 1; + status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_TRUNC, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "truncating a file" + ); + close_fd(file_fd); + + let mut rights_base: wasi_unstable::Rights = dir_fdstat.fs_rights_base; + let mut rights_inheriting: wasi_unstable::Rights = dir_fdstat.fs_rights_inheriting; + + if (rights_inheriting & wasi_unstable::RIGHT_FD_FILESTAT_SET_SIZE) == 0 { + eprintln!("implementation doesn't support setting file sizes through file descriptors, skipping"); + } else { + rights_inheriting &= !wasi_unstable::RIGHT_FD_FILESTAT_SET_SIZE; + assert!( + wasi_unstable::fd_fdstat_set_rights(dir_fd, rights_base, rights_inheriting).is_ok(), + "droping fd_filestat_set_size inheriting right on a directory", + ); + } + + // Test that we can truncate the file without the + // wasi_unstable::RIGHT_FD_FILESTAT_SET_SIZE right. + status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_TRUNC, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "truncating a file without fd_filestat_set_size right", + ); + close_fd(file_fd); + + rights_base &= !wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE; + assert!( + wasi_unstable::fd_fdstat_set_rights(dir_fd, rights_base, rights_inheriting).is_ok(), + "droping path_filestat_set_size base right on a directory", + ); + + // Test that clearing wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE actually + // took effect. + status = wasi_fd_fdstat_get(dir_fd, &mut dir_fdstat); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "reading the fdstat from a directory", + ); + assert_eq!( + (dir_fdstat.fs_rights_base & wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE), + 0, + "reading the fdstat from a directory", + ); + + // Test that we can't truncate the file without the + // wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE right. + status = wasi_path_open( + dir_fd, + 0, + "file", + wasi_unstable::O_TRUNC, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ENOTCAPABLE, + "truncating a file without path_filestat_set_size right", + ); + assert_eq!( + file_fd, + wasi_unstable::Fd::max_value(), + "failed open should set the file descriptor to -1", + ); + } + + cleanup_file(dir_fd, "file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_truncation_rights(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/bin/unlink_file_trailing_slashes.rs b/wasi-common/wasi-misc-tests/src/bin/unlink_file_trailing_slashes.rs new file mode 100644 index 0000000000..5d7b3aa69a --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/bin/unlink_file_trailing_slashes.rs @@ -0,0 +1,67 @@ +use std::{env, process}; +use wasi::wasi_unstable; +use wasi_misc_tests::open_scratch_directory; +use wasi_misc_tests::utils::{cleanup_dir, create_dir, create_file}; +use wasi_misc_tests::wasi_wrappers::wasi_path_unlink_file; + +unsafe fn test_unlink_file_trailing_slashes(dir_fd: wasi_unstable::Fd) { + // Create a directory in the scratch directory. + create_dir(dir_fd, "dir"); + + // Test that unlinking it fails. + assert_eq!( + wasi_path_unlink_file(dir_fd, "dir"), + Err(wasi_unstable::EISDIR), + "unlink_file on a directory should fail" + ); + + // Test that unlinking it with a trailing flash fails. + assert_eq!( + wasi_path_unlink_file(dir_fd, "dir/"), + Err(wasi_unstable::EISDIR), + "unlink_file on a directory should fail" + ); + + // Clean up. + cleanup_dir(dir_fd, "dir"); + + // Create a temporary file. + create_file(dir_fd, "file"); + + // Test that unlinking it with a trailing flash fails. + assert_eq!( + wasi_path_unlink_file(dir_fd, "file/"), + Err(wasi_unstable::ENOTDIR), + "unlink_file with a trailing slash should fail" + ); + + // Test that unlinking it with no trailing flash succeeds. + assert_eq!( + wasi_path_unlink_file(dir_fd, "file"), + Ok(()), + "unlink_file with no trailing slash should succeed" + ); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_unlink_file_trailing_slashes(dir_fd) } +} diff --git a/wasi-common/wasi-misc-tests/src/lib.rs b/wasi-common/wasi-misc-tests/src/lib.rs new file mode 100644 index 0000000000..5960d09f5c --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/lib.rs @@ -0,0 +1,25 @@ +pub mod utils; +pub mod wasi_wrappers; + +use libc; +use std::ffi::CString; +use std::io; +use wasi::wasi_unstable; + +pub fn open_scratch_directory(path: &str) -> Result { + // Open the scratch directory. + let dir_fd: wasi_unstable::Fd = unsafe { + let cstr = CString::new(path.as_bytes()).unwrap(); + libc::open(cstr.as_ptr(), libc::O_RDONLY | libc::O_DIRECTORY) + } as wasi_unstable::Fd; + + if (dir_fd as std::os::raw::c_int) < 0 { + Err(format!( + "error opening scratch directory '{}': {}", + path, + io::Error::last_os_error() + )) + } else { + Ok(dir_fd) + } +} diff --git a/wasi-common/wasi-misc-tests/src/utils.rs b/wasi-common/wasi-misc-tests/src/utils.rs new file mode 100644 index 0000000000..b9c1f1620c --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/utils.rs @@ -0,0 +1,54 @@ +use crate::wasi_wrappers::*; +use more_asserts::assert_gt; +use wasi::wasi_unstable; + +pub unsafe fn create_dir(dir_fd: wasi_unstable::Fd, dir_name: &str) { + assert!( + wasi_path_create_directory(dir_fd, dir_name).is_ok(), + "creating a directory" + ); +} + +pub unsafe fn cleanup_dir(dir_fd: wasi_unstable::Fd, dir_name: &str) { + assert!( + wasi_path_remove_directory(dir_fd, dir_name).is_ok(), + "remove_directory on an empty directory should succeed" + ); +} + +/// Create an empty file with the given name. +pub unsafe fn create_file(dir_fd: wasi_unstable::Fd, file_name: &str) { + let mut file_fd = wasi_unstable::Fd::max_value() - 1; + let status = wasi_path_open( + dir_fd, + 0, + file_name, + wasi_unstable::O_CREAT, + 0, + 0, + 0, + &mut file_fd, + ); + assert_eq!( + status, + wasi_unstable::raw::__WASI_ESUCCESS, + "creating a file" + ); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi_unstable::Fd, + "file descriptor range check", + ); + close_fd(file_fd); +} + +pub unsafe fn cleanup_file(dir_fd: wasi_unstable::Fd, file_name: &str) { + assert!( + wasi_path_unlink_file(dir_fd, file_name).is_ok(), + "unlink_file on a symlink should succeed" + ); +} + +pub unsafe fn close_fd(fd: wasi_unstable::Fd) { + assert!(wasi_unstable::fd_close(fd).is_ok(), "closing a file"); +} diff --git a/wasi-common/wasi-misc-tests/src/wasi_wrappers.rs b/wasi-common/wasi-misc-tests/src/wasi_wrappers.rs new file mode 100644 index 0000000000..584dfb09c0 --- /dev/null +++ b/wasi-common/wasi-misc-tests/src/wasi_wrappers.rs @@ -0,0 +1,217 @@ +//! Minimal wrappers around WASI functions to allow use of `&str` rather than +//! pointer-length pairs. +//! +//! Where possible, we use the idiomatic wasi_unstable wrappers rather than the +//! raw interfaces, however for functions with out parameters, we use the raw +//! interfaces so that we can test whether they are stored to. In the future, +//! WASI should switch to multi-value and eliminate out parameters altogether. + +use wasi::wasi_unstable; + +pub unsafe fn wasi_path_create_directory( + dir_fd: wasi_unstable::Fd, + dir_name: &str, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_create_directory(dir_fd, dir_name.as_bytes()) +} + +pub unsafe fn wasi_path_remove_directory( + dir_fd: wasi_unstable::Fd, + dir_name: &str, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_remove_directory(dir_fd, dir_name.as_bytes()) +} + +pub unsafe fn wasi_path_unlink_file( + dir_fd: wasi_unstable::Fd, + file_name: &str, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_unlink_file(dir_fd, file_name.as_bytes()) +} + +#[allow(clippy::too_many_arguments)] +pub unsafe fn wasi_path_open( + dirfd: wasi_unstable::Fd, + dirflags: wasi_unstable::LookupFlags, + path: &str, + oflags: wasi_unstable::OFlags, + fs_rights_base: wasi_unstable::Rights, + fs_rights_inheriting: wasi_unstable::Rights, + fs_flags: wasi_unstable::FdFlags, + fd: &mut wasi_unstable::Fd, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_path_open( + dirfd, + dirflags, + path.as_ptr(), + path.len(), + oflags, + fs_rights_base, + fs_rights_inheriting, + fs_flags, + fd, + ) +} + +pub unsafe fn wasi_path_symlink( + old_path: &str, + dirfd: wasi_unstable::Fd, + new_path: &str, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_symlink(old_path.as_bytes(), dirfd, new_path.as_bytes()) +} + +pub unsafe fn wasi_path_readlink( + dirfd: wasi_unstable::Fd, + path: &str, + buf: &mut [u8], + bufused: &mut usize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_path_readlink( + dirfd, + path.as_ptr(), + path.len(), + buf.as_mut_ptr(), + buf.len(), + bufused, + ) +} + +pub unsafe fn wasi_path_rename( + old_dirfd: wasi_unstable::Fd, + old_path: &str, + new_dirfd: wasi_unstable::Fd, + new_path: &str, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_rename( + old_dirfd, + old_path.as_bytes(), + new_dirfd, + new_path.as_bytes(), + ) +} + +pub unsafe fn wasi_fd_fdstat_get( + fd: wasi_unstable::Fd, + fdstat: &mut wasi_unstable::FdStat, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_fdstat_get(fd, fdstat) +} + +pub unsafe fn wasi_fd_seek( + fd: wasi_unstable::Fd, + offset: wasi_unstable::FileDelta, + whence: wasi_unstable::Whence, + newoffset: &mut wasi_unstable::FileSize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_seek(fd, offset, whence, newoffset) +} + +pub unsafe fn wasi_fd_tell( + fd: wasi_unstable::Fd, + offset: &mut wasi_unstable::FileSize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_tell(fd, offset) +} + +pub unsafe fn wasi_clock_time_get( + clock_id: wasi_unstable::ClockId, + precision: wasi_unstable::Timestamp, + time: &mut wasi_unstable::Timestamp, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_clock_time_get(clock_id, precision, time) +} + +pub unsafe fn wasi_fd_filestat_get( + fd: wasi_unstable::Fd, + filestat: &mut wasi_unstable::FileStat, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_filestat_get(fd, filestat) +} + +pub unsafe fn wasi_fd_write( + fd: wasi_unstable::Fd, + iovs: &[wasi_unstable::CIoVec], + nwritten: &mut libc::size_t, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_write(fd, iovs.as_ptr(), iovs.len(), nwritten) +} + +pub unsafe fn wasi_fd_read( + fd: wasi_unstable::Fd, + iovs: &[wasi_unstable::IoVec], + nread: &mut libc::size_t, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_read(fd, iovs.as_ptr(), iovs.len(), nread) +} + +pub unsafe fn wasi_fd_pread( + fd: wasi_unstable::Fd, + iovs: &[wasi_unstable::IoVec], + offset: wasi_unstable::FileSize, + nread: &mut usize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_pread(fd, iovs.as_ptr(), iovs.len(), offset, nread) +} + +pub unsafe fn wasi_fd_pwrite( + fd: wasi_unstable::Fd, + iovs: &mut [wasi_unstable::CIoVec], + offset: wasi_unstable::FileSize, + nwritten: &mut usize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_pwrite(fd, iovs.as_ptr(), iovs.len(), offset, nwritten) +} + +pub unsafe fn wasi_path_filestat_get( + fd: wasi_unstable::Fd, + dirflags: wasi_unstable::LookupFlags, + path: &str, + path_len: usize, + filestat: &mut wasi_unstable::FileStat, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_path_filestat_get(fd, dirflags, path.as_ptr(), path_len, filestat) +} + +pub unsafe fn wasi_path_filestat_set_times( + fd: wasi_unstable::Fd, + dirflags: wasi_unstable::LookupFlags, + path: &str, + st_atim: wasi_unstable::Timestamp, + st_mtim: wasi_unstable::Timestamp, + fst_flags: wasi_unstable::FstFlags, +) -> Result<(), wasi_unstable::Error> { + wasi_unstable::path_filestat_set_times( + fd, + dirflags, + path.as_bytes(), + st_atim, + st_mtim, + fst_flags, + ) +} + +pub unsafe fn wasi_fd_readdir( + fd: wasi_unstable::Fd, + buf: &mut [u8], + buf_len: usize, + cookie: wasi_unstable::DirCookie, + buf_used: &mut usize, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_readdir( + fd, + buf.as_mut_ptr() as *mut libc::c_void, + buf_len, + cookie, + buf_used, + ) +} + +pub unsafe fn wasi_fd_advise( + fd: wasi_unstable::Fd, + offset: wasi_unstable::FileSize, + len: wasi_unstable::FileSize, + advice: wasi_unstable::Advice, +) -> wasi_unstable::Errno { + wasi_unstable::raw::__wasi_fd_advise(fd, offset, len, advice) +} diff --git a/wasi-common/wig/Cargo.toml b/wasi-common/wig/Cargo.toml new file mode 100644 index 0000000000..c51b0f6a44 --- /dev/null +++ b/wasi-common/wig/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wig" +version = "0.1.0" +authors = ["Dan Gohman "] +edition = "2018" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/CraneStation/wasmtime" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.2" +proc-macro2 = "1.0.6" +# We include the WASI repo primarily for the witx files, but it's also useful +# to use the witx parser it contains, rather than the witx crate from +# crates.io, so that it always matches the version of the witx files. +witx = { path = "../WASI/tools/witx" } diff --git a/wasi-common/wig/LICENSE b/wasi-common/wig/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/wasi-common/wig/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/wasi-common/wig/src/lib.rs b/wasi-common/wig/src/lib.rs new file mode 100644 index 0000000000..ec4b3371c9 --- /dev/null +++ b/wasi-common/wig/src/lib.rs @@ -0,0 +1,34 @@ +extern crate proc_macro; +extern crate proc_macro2; +extern crate quote; +extern crate witx; + +mod raw_types; +mod utils; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; + +#[proc_macro] +pub fn witx_host_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Host, + )) +} + +#[proc_macro] +pub fn witx_wasi_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Wasi, + )) +} + +#[proc_macro] +pub fn witx_wasi32_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Wasi32, + )) +} diff --git a/wasi-common/wig/src/raw_types.rs b/wasi-common/wig/src/raw_types.rs new file mode 100644 index 0000000000..8068054794 --- /dev/null +++ b/wasi-common/wig/src/raw_types.rs @@ -0,0 +1,250 @@ +//! Translate witx types to Rust. + +use crate::utils; +use proc_macro2::{Delimiter, Group, Literal, TokenStream, TokenTree}; +use quote::{format_ident, quote}; +use std::convert::TryFrom; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Mode { + Host, + Wasi32, + Wasi, +} + +impl Mode { + pub fn include_target_types(&self) -> bool { + match self { + Mode::Host | Mode::Wasi32 => true, + Mode::Wasi => false, + } + } +} + +pub fn gen(args: TokenStream, mode: Mode) -> TokenStream { + let mut output = TokenStream::new(); + + let (path, _phase) = utils::witx_path_from_args(args); + let doc = match witx::load(&path) { + Ok(doc) => doc, + Err(e) => { + panic!("error opening file {}: {}", path, e); + } + }; + + gen_datatypes(&mut output, &doc, mode); + + output +} + +fn gen_datatypes(output: &mut TokenStream, doc: &witx::Document, mode: Mode) { + for datatype in doc.datatypes() { + if mode.include_target_types() != type_has_target_size(doc, &datatype) { + continue; + } + + gen_datatype(output, doc, mode, &datatype); + } +} + +fn gen_datatype( + output: &mut TokenStream, + doc: &witx::Document, + mode: Mode, + datatype: &witx::Datatype, +) { + match &datatype.variant { + witx::DatatypeVariant::Alias(a) => { + if a.name.as_str() == "size_t" { + let wasi_name = format_ident!("__wasi_{}", a.name.as_str()); + match mode { + Mode::Host => output.extend(quote!(pub type #wasi_name = usize;)), + Mode::Wasi => panic!("size_t has target-specific size"), + Mode::Wasi32 => output.extend(quote!(pub type #wasi_name = u32;)), + } + } else { + let wasi_name = format_ident!("__wasi_{}", a.name.as_str()); + let to = ident_tokens(mode, &a.to); + output.extend(quote!(pub type #wasi_name = #to;)); + } + } + witx::DatatypeVariant::Enum(e) => { + let wasi_name = format_ident!("__wasi_{}", e.name.as_str()); + let repr = int_repr_tokens(e.repr); + output.extend(quote!(pub type #wasi_name = #repr;)); + for (index, variant) in e.variants.iter().enumerate() { + let value_name = format_ident!("__WASI_{}", variant.as_str()); + let index_name = Literal::usize_unsuffixed(index); + output.extend(quote!(pub const #value_name: #wasi_name = #index_name;)); + } + } + witx::DatatypeVariant::Flags(f) => { + let wasi_name = format_ident!("__wasi_{}", f.name.as_str()); + let repr = int_repr_tokens(f.repr); + output.extend(quote!(pub type #wasi_name = #repr;)); + for (index, flag) in f.flags.iter().enumerate() { + let value_name = format_ident!("__WASI_{}", flag.as_str()); + let flag_value = Literal::u128_unsuffixed( + 1u128 + .checked_shl(u32::try_from(index).expect("flag value overflow")) + .expect("flag value overflow"), + ); + output.extend(quote!(pub const #value_name: #wasi_name = #flag_value;)); + } + } + witx::DatatypeVariant::Struct(s) => { + output.extend(quote!(#[repr(C)])); + + // Types which contain unions can't trivially implement Debug, + // Hash, or Eq, because the type itself doesn't record which + // union member is active. + if struct_has_union(&doc, s) { + output.extend(quote!(#[derive(Copy, Clone)])); + output.extend(quote!(#[allow(missing_debug_implementations)])); + } else { + output.extend(quote!(#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)])); + } + + let wasi_name = format_ident!("__wasi_{}", s.name.as_str()); + output.extend(quote!(pub struct #wasi_name)); + + let mut inner = TokenStream::new(); + for member in &s.members { + let member_name = format_ident!("r#{}", member.name.as_str()); + let member_type = ident_tokens(mode, &member.type_); + inner.extend(quote!(pub #member_name: #member_type,)); + } + let braced = Group::new(Delimiter::Brace, inner); + output.extend(TokenStream::from(TokenTree::Group(braced))); + } + witx::DatatypeVariant::Union(u) => { + output.extend(quote!(#[repr(C)])); + output.extend(quote!(#[derive(Copy, Clone)])); + output.extend(quote!(#[allow(missing_debug_implementations)])); + + let wasi_name = format_ident!("__wasi_{}", u.name.as_str()); + output.extend(quote!(pub union #wasi_name)); + + let mut inner = TokenStream::new(); + for variant in &u.variants { + let variant_name = format_ident!("r#{}", variant.name.as_str()); + let variant_type = ident_tokens(mode, &variant.type_); + inner.extend(quote!(pub #variant_name: #variant_type,)); + } + let braced = Group::new(Delimiter::Brace, inner); + output.extend(TokenStream::from(TokenTree::Group(braced))); + } + } +} + +fn int_repr_tokens(int_repr: witx::IntRepr) -> TokenStream { + match int_repr { + witx::IntRepr::U8 => quote!(u8), + witx::IntRepr::U16 => quote!(u16), + witx::IntRepr::U32 => quote!(u32), + witx::IntRepr::U64 => quote!(u64), + } +} + +fn builtin_tokens(mode: Mode, builtin: witx::BuiltinType) -> TokenStream { + match builtin { + witx::BuiltinType::String => match mode { + Mode::Host => quote!((*const u8, usize)), + Mode::Wasi => panic!("strings have target-specific size"), + Mode::Wasi32 => quote!((u32, u32)), + }, + witx::BuiltinType::U8 => quote!(u8), + witx::BuiltinType::U16 => quote!(u16), + witx::BuiltinType::U32 => quote!(u32), + witx::BuiltinType::U64 => quote!(u64), + witx::BuiltinType::S8 => quote!(i8), + witx::BuiltinType::S16 => quote!(i16), + witx::BuiltinType::S32 => quote!(i32), + witx::BuiltinType::S64 => quote!(i64), + witx::BuiltinType::F32 => quote!(f32), + witx::BuiltinType::F64 => quote!(f64), + } +} + +fn ident_tokens(mode: Mode, ident: &witx::DatatypeIdent) -> TokenStream { + match ident { + witx::DatatypeIdent::Builtin(builtin) => builtin_tokens(mode, *builtin), + witx::DatatypeIdent::Ident(ident) => TokenStream::from(TokenTree::Ident(format_ident!( + "__wasi_{}", + ident.name.as_str() + ))), + witx::DatatypeIdent::Pointer(pointee) => { + let pointee = ident_tokens(mode, pointee); + match mode { + Mode::Host => quote!(*mut #pointee), + Mode::Wasi => panic!("pointers have target-specific size"), + Mode::Wasi32 => quote!(u32), + } + } + witx::DatatypeIdent::ConstPointer(pointee) => { + let pointee = ident_tokens(mode, pointee); + match mode { + Mode::Host => quote!(*const #pointee), + Mode::Wasi => panic!("pointers have target-specific size"), + Mode::Wasi32 => quote!(u32), + } + } + witx::DatatypeIdent::Array(element) => { + let element_name = ident_tokens(mode, element); + match mode { + Mode::Host => quote!((*const #element_name, usize)), + Mode::Wasi => panic!("arrays have target-specific size"), + Mode::Wasi32 => quote!((u32, u32)), + } + } + } +} + +/// Test whether the given struct contains any union members. +fn struct_has_union(doc: &witx::Document, s: &witx::StructDatatype) -> bool { + s.members.iter().any(|member| match &member.type_ { + witx::DatatypeIdent::Ident(ident) => match &doc.datatype(&ident.name).unwrap().variant { + witx::DatatypeVariant::Union(_) => true, + witx::DatatypeVariant::Struct(s) => struct_has_union(doc, &s), + _ => false, + }, + _ => false, + }) +} + +/// Test whether the given type has a target-specific size. +fn type_has_target_size(doc: &witx::Document, type_: &witx::Datatype) -> bool { + match &type_.variant { + witx::DatatypeVariant::Alias(a) => { + a.name.as_str() == "size_t" || ident_has_target_size(doc, &a.to) + } + witx::DatatypeVariant::Enum(_) => false, + witx::DatatypeVariant::Flags(_) => false, + witx::DatatypeVariant::Struct(s) => s + .members + .iter() + .any(|m| ident_has_target_size(doc, &m.type_)), + witx::DatatypeVariant::Union(u) => u + .variants + .iter() + .any(|v| ident_has_target_size(doc, &v.type_)), + } +} + +/// Test whether the given type ident has a target-specific size. +fn ident_has_target_size(doc: &witx::Document, ident: &witx::DatatypeIdent) -> bool { + match ident { + witx::DatatypeIdent::Ident(ident) => { + type_has_target_size(doc, &doc.datatype(&ident.name).unwrap()) + } + witx::DatatypeIdent::Builtin(builtin) => { + if let witx::BuiltinType::String = builtin { + true + } else { + false + } + } + witx::DatatypeIdent::Pointer(_) | witx::DatatypeIdent::ConstPointer(_) => true, + witx::DatatypeIdent::Array(element) => ident_has_target_size(doc, element), + } +} diff --git a/wasi-common/wig/src/utils.rs b/wasi-common/wig/src/utils.rs new file mode 100644 index 0000000000..283cbd2d1a --- /dev/null +++ b/wasi-common/wig/src/utils.rs @@ -0,0 +1,54 @@ +use proc_macro2::{Literal, TokenStream, TokenTree}; +/// Given the input tokens to a macro invocation, return the path to the +/// witx file to process. +pub(crate) fn witx_path_from_args(args: TokenStream) -> (String, String) { + let mut strings = Vec::new(); + + for arg in args { + if let TokenTree::Literal(literal) = arg { + let parsed = parse_string_literal(literal); + + strings.push(parsed); + } else { + panic!("arguments must be string literals"); + } + } + + if strings.len() != 2 { + panic!("expected two string literals"); + } + + let phase = &strings[0]; + let id = &strings[1]; + let path = witx_path(phase, id); + + (path, phase.clone()) +} + +fn witx_path(phase: &str, id: &str) -> String { + let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or(".".into()); + format!("{}/WASI/phases/{}/witx/{}.witx", root, phase, id) +} + +// Convert a `Literal` holding a string literal into the `String`. +// +// FIXME: It feels like there should be an easier way to do this. +fn parse_string_literal(literal: Literal) -> String { + let s = literal.to_string(); + assert!( + s.starts_with('"') && s.ends_with('"'), + "string literal must be enclosed in double-quotes" + ); + + let trimmed = s[1..s.len() - 1].to_owned(); + assert!( + !trimmed.contains('"'), + "string literal must not contain embedded quotes for now" + ); + assert!( + !trimmed.contains('\\'), + "string literal must not contain embedded backslashes for now" + ); + + trimmed +} diff --git a/wasi-common/winx/Cargo.toml b/wasi-common/winx/Cargo.toml new file mode 100644 index 0000000000..82b03e111b --- /dev/null +++ b/wasi-common/winx/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "winx" +version = "0.5.0" +authors = ["Jakub Konka "] +edition = "2018" +license = "Apache-2.0 WITH LLVM-exception" +description = "Windows API helper library" + +[dependencies] +winapi = { version = "0.3", features = ["std", "errhandlingapi", "handleapi", "processthreadsapi", "securitybaseapi", "winbase", "winerror", "ws2def", "fileapi", "aclapi" ] } +bitflags = "1.0" +cvt = "0.1" diff --git a/wasi-common/winx/LICENSE b/wasi-common/winx/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/wasi-common/winx/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/wasi-common/winx/src/file.rs b/wasi-common/winx/src/file.rs new file mode 100644 index 0000000000..b937c28959 --- /dev/null +++ b/wasi-common/winx/src/file.rs @@ -0,0 +1,416 @@ +#![allow(non_camel_case_types)] +use crate::{winerror, Result}; +use cvt::cvt; +use std::ffi::{c_void, OsString}; +use std::fs::File; +use std::io; +use std::os::windows::prelude::{AsRawHandle, OsStringExt, RawHandle}; +use winapi::shared::minwindef::{self, DWORD}; +use winapi::um::{fileapi, fileapi::GetFileType, minwinbase, winbase, winnt}; + +/// Maximum total path length for Unicode in Windows. +/// [Maximum path length limitation]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation +pub const WIDE_MAX_PATH: DWORD = 0x7fff; + +#[derive(Debug, Copy, Clone)] +pub struct FileType(minwindef::DWORD); + +// possible types are: +// * FILE_TYPE_CHAR +// * FILE_TYPE_DISK +// * FILE_TYPE_PIPE +// * FILE_TYPE_REMOTE +// * FILE_TYPE_UNKNOWN +// +// FILE_TYPE_REMOTE is unused +// https://technet.microsoft.com/en-us/evalcenter/aa364960(v=vs.100) +impl FileType { + /// Returns true if character device such as LPT device or console + pub fn is_char(&self) -> bool { + self.0 == winbase::FILE_TYPE_CHAR + } + + /// Returns true if disk device such as file or dir + pub fn is_disk(&self) -> bool { + self.0 == winbase::FILE_TYPE_DISK + } + + /// Returns true if pipe device such as socket, named pipe or anonymous pipe + pub fn is_pipe(&self) -> bool { + self.0 == winbase::FILE_TYPE_PIPE + } + + /// Returns true if unknown device + pub fn is_unknown(&self) -> bool { + self.0 == winbase::FILE_TYPE_UNKNOWN + } +} + +pub unsafe fn get_file_type(handle: RawHandle) -> Result { + let file_type = FileType(GetFileType(handle)); + let err = winerror::WinError::last(); + if file_type.is_unknown() && err != winerror::WinError::ERROR_SUCCESS { + Err(err) + } else { + Ok(file_type) + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[repr(u32)] +pub enum CreationDisposition { + NO_DISPOSITION = 0, + /// Creates a new file, only if it does not already exist. + /// If the specified file exists, the function fails and the last-error code is + /// set to ERROR_FILE_EXISTS (80). + /// + /// If the specified file does not exist and is a valid path to a writable location, + /// a new file is created. + CREATE_NEW = fileapi::CREATE_NEW, + /// Creates a new file, always. + /// If the specified file exists and is writable, the function overwrites the file, + /// the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS (183). + /// + /// If the specified file does not exist and is a valid path, a new file is created, + /// the function succeeds, and the last-error code is set to zero. + CREATE_ALWAYS = fileapi::CREATE_ALWAYS, + /// Opens a file or device, only if it exists. + /// If the specified file or device does not exist, the function fails and the + /// last-error code is set to ERROR_FILE_NOT_FOUND (2). + OPEN_EXISTING = fileapi::OPEN_EXISTING, + /// Opens a file, always. + /// If the specified file exists, the function succeeds and the last-error code is + /// set to ERROR_ALREADY_EXISTS (183). + /// + /// If the specified file does not exist and is a valid path to a writable location, + /// the function creates a file and the last-error code is set to zero. + OPEN_ALWAYS = fileapi::OPEN_ALWAYS, + /// Opens a file and truncates it so that its size is zero bytes, only if it exists. + /// If the specified file does not exist, the function fails and the last-error code + /// is set to ERROR_FILE_NOT_FOUND (2). + /// + /// The calling process must open the file with the GENERIC_WRITE bit set as part + /// of the dwDesiredAccess parameter. + TRUNCATE_EXISTING = fileapi::TRUNCATE_EXISTING, +} + +impl CreationDisposition { + pub fn from_u32(disp: u32) -> Self { + use CreationDisposition::*; + match disp { + fileapi::CREATE_NEW => CREATE_NEW, + fileapi::CREATE_ALWAYS => CREATE_ALWAYS, + fileapi::OPEN_EXISTING => OPEN_EXISTING, + fileapi::OPEN_ALWAYS => OPEN_ALWAYS, + fileapi::TRUNCATE_EXISTING => TRUNCATE_EXISTING, + _ => NO_DISPOSITION, + } + } +} + +bitflags! { + pub struct Attributes: minwindef::DWORD { + /// A file or directory that is an archive file or directory. + /// Applications typically use this attribute to mark files for backup or removal. + const FILE_ATTRIBUTE_ARCHIVE = winnt::FILE_ATTRIBUTE_ARCHIVE; + /// A file or directory that is compressed. For a file, all of the data in the file is compressed. + /// For a directory, compression is the default for newly created files and subdirectories. + const FILE_ATTRIBUTE_COMPRESSED = winnt::FILE_ATTRIBUTE_COMPRESSED; + /// This value is reserved for system use. + const FILE_ATTRIBUTE_DEVICE = winnt::FILE_ATTRIBUTE_DEVICE; + /// The handle that identifies a directory. + const FILE_ATTRIBUTE_DIRECTORY = winnt::FILE_ATTRIBUTE_DIRECTORY; + /// A file or directory that is encrypted. For a file, all data streams in the file are encrypted. + /// For a directory, encryption is the default for newly created files and subdirectories. + const FILE_ATTRIBUTE_ENCRYPTED = winnt::FILE_ATTRIBUTE_ENCRYPTED; + /// The file or directory is hidden. It is not included in an ordinary directory listing. + const FILE_ATTRIBUTE_HIDDEN = winnt::FILE_ATTRIBUTE_HIDDEN; + /// The directory or user data stream is configured with integrity (only supported on ReFS volumes). + /// It is not included in an ordinary directory listing. The integrity setting persists with the file if it's renamed. + /// If a file is copied the destination file will have integrity set if either the source file or destination directory have integrity set. + const FILE_ATTRIBUTE_INTEGRITY_STREAM = winnt::FILE_ATTRIBUTE_INTEGRITY_STREAM; + /// A file that does not have other attributes set. This attribute is valid only when used alone. + const FILE_ATTRIBUTE_NORMAL = winnt::FILE_ATTRIBUTE_NORMAL; + /// The file or directory is not to be indexed by the content indexing service. + const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = winnt::FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; + /// The user data stream not to be read by the background data integrity scanner (AKA scrubber). + /// When set on a directory it only provides inheritance. This flag is only supported on Storage Spaces and ReFS volumes. + /// It is not included in an ordinary directory listing. + const FILE_ATTRIBUTE_NO_SCRUB_DATA = winnt::FILE_ATTRIBUTE_NO_SCRUB_DATA; + /// The data of a file is not available immediately. + /// This attribute indicates that the file data is physically moved to offline storage. + /// This attribute is used by Remote Storage, which is the hierarchical storage management software. + /// Applications should not arbitrarily change this attribute. + const FILE_ATTRIBUTE_OFFLINE = winnt::FILE_ATTRIBUTE_OFFLINE; + /// A file that is read-only. Applications can read the file, but cannot write to it or delete it. + /// This attribute is not honored on directories. + const FILE_ATTRIBUTE_READONLY = winnt::FILE_ATTRIBUTE_READONLY; + /// When this attribute is set, it means that the file or directory is not fully present locally. + /// For a file that means that not all of its data is on local storage (e.g. it may be sparse with some data still in remote storage). + /// For a directory it means that some of the directory contents are being virtualized from another location. + /// Reading the file / enumerating the directory will be more expensive than normal, e.g. it will cause at least some of the + /// file/directory content to be fetched from a remote store. Only kernel-mode callers can set this bit. + const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = winnt::FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; + /// This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, FILE_BOTH_DIR_INFORMATION, etc.). + /// When this attribute is set, it means that the file or directory has no physical representation on the local system; the item is virtual. + /// Opening the item will be more expensive than normal, e.g. it will cause at least some of it to be fetched from a remote store. + const FILE_ATTRIBUTE_RECALL_ON_OPEN = winnt::FILE_ATTRIBUTE_RECALL_ON_OPEN; + /// A file or directory that has an associated reparse point, or a file that is a symbolic link. + const FILE_ATTRIBUTE_REPARSE_POINT = winnt::FILE_ATTRIBUTE_REPARSE_POINT; + /// A file that is a sparse file. + const FILE_ATTRIBUTE_SPARSE_FILE = winnt::FILE_ATTRIBUTE_SPARSE_FILE; + /// A file or directory that the operating system uses a part of, or uses exclusively. + const FILE_ATTRIBUTE_SYSTEM = winnt::FILE_ATTRIBUTE_SYSTEM; + /// A file that is being used for temporary storage. + /// File systems avoid writing data back to mass storage if sufficient cache memory is available, because typically, + /// an application deletes a temporary file after the handle is closed. In that scenario, the system can entirely + /// avoid writing the data. Otherwise, the data is written after the handle is closed. + const FILE_ATTRIBUTE_TEMPORARY = winnt::FILE_ATTRIBUTE_TEMPORARY; + /// This value is reserved for system use. + const FILE_ATTRIBUTE_VIRTUAL = winnt::FILE_ATTRIBUTE_VIRTUAL; + } +} + +bitflags! { + pub struct Flags: minwindef::DWORD { + /// The file is being opened or created for a backup or restore operation. + /// The system ensures that the calling process overrides file security checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges. + /// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle. + const FILE_FLAG_BACKUP_SEMANTICS = winbase::FILE_FLAG_BACKUP_SEMANTICS; + /// The file is to be deleted immediately after all of its handles are closed, which includes the specified handle and any other open or duplicated handles. + /// If there are existing open handles to a file, the call fails unless they were all opened with the FILE_SHARE_DELETE share mode. + /// Subsequent open requests for the file fail, unless the FILE_SHARE_DELETE share mode is specified. + const FILE_FLAG_DELETE_ON_CLOSE = winbase::FILE_FLAG_DELETE_ON_CLOSE; + /// The file or device is being opened with no system caching for data reads and writes. + /// This flag does not affect hard disk caching or memory mapped files. + /// There are strict requirements for successfully working with files opened with + /// CreateFile using the FILE_FLAG_NO_BUFFERING flag. + const FILE_FLAG_NO_BUFFERING = winbase::FILE_FLAG_NO_BUFFERING; + /// The file data is requested, but it should continue to be located in remote storage. + /// It should not be transported back to local storage. This flag is for use by remote storage systems. + const FILE_FLAG_OPEN_NO_RECALL = winbase::FILE_FLAG_OPEN_NO_RECALL; + /// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. + /// When a file is opened, a file handle is returned, whether or not the filter that controls the reparse point is operational. + /// This flag cannot be used with the CREATE_ALWAYS flag. + /// If the file is not a reparse point, then this flag is ignored. + const FILE_FLAG_OPEN_REPARSE_POINT = winbase::FILE_FLAG_OPEN_REPARSE_POINT; + /// The file or device is being opened or created for asynchronous I/O. + /// When subsequent I/O operations are completed on this handle, the event specified in the OVERLAPPED structure will be set to the signaled state. + /// If this flag is specified, the file can be used for simultaneous read and write operations. + /// If this flag is not specified, then I/O operations are serialized, even if the calls to the read and write functions specify an OVERLAPPED structure. + const FILE_FLAG_OVERLAPPED = winbase::FILE_FLAG_OVERLAPPED; + /// Access will occur according to POSIX rules. This includes allowing multiple files with names, + /// differing only in case, for file systems that support that naming. Use care when using this option, + /// because files created with this flag may not be accessible by applications that are written for MS-DOS or 16-bit Windows. + const FILE_FLAG_POSIX_SEMANTICS = winbase::FILE_FLAG_POSIX_SEMANTICS; + /// Access is intended to be random. The system can use this as a hint to optimize file caching. + /// This flag has no effect if the file system does not support cached I/O and FILE_FLAG_NO_BUFFERING. + const FILE_FLAG_RANDOM_ACCESS = winbase::FILE_FLAG_RANDOM_ACCESS; + /// The file or device is being opened with session awareness. + /// If this flag is not specified, then per-session devices (such as a device using RemoteFX USB Redirection) + /// cannot be opened by processes running in session 0. This flag has no effect for callers not in session 0. + /// This flag is supported only on server editions of Windows. + const FILE_FLAG_SESSION_AWARE = winbase::FILE_FLAG_SESSION_AWARE; + /// Access is intended to be sequential from beginning to end. The system can use this as a hint to optimize file caching. + /// This flag should not be used if read-behind (that is, reverse scans) will be used. + /// This flag has no effect if the file system does not support cached I/O and FILE_FLAG_NO_BUFFERING. + const FILE_FLAG_SEQUENTIAL_SCAN = winbase::FILE_FLAG_SEQUENTIAL_SCAN; + /// Write operations will not go through any intermediate cache, they will go directly to disk. + const FILE_FLAG_WRITE_THROUGH = winbase::FILE_FLAG_WRITE_THROUGH; + } +} + +bitflags! { + /// [Access mask]: https://docs.microsoft.com/en-us/windows/desktop/SecAuthZ/access-mask + pub struct AccessMode: minwindef::DWORD { + /// For a file object, the right to read the corresponding file data. + /// For a directory object, the right to read the corresponding directory data. + const FILE_READ_DATA = winnt::FILE_READ_DATA; + const FILE_LIST_DIRECTORY = winnt::FILE_LIST_DIRECTORY; + /// For a file object, the right to write data to the file. + /// For a directory object, the right to create a file in the directory. + const FILE_WRITE_DATA = winnt::FILE_WRITE_DATA; + const FILE_ADD_FILE = winnt::FILE_ADD_FILE; + /// For a file object, the right to append data to the file. + /// (For local files, write operations will not overwrite existing data + /// if this flag is specified without FILE_WRITE_DATA.) + /// For a directory object, the right to create a subdirectory. + /// For a named pipe, the right to create a pipe. + const FILE_APPEND_DATA = winnt::FILE_APPEND_DATA; + const FILE_ADD_SUBDIRECTORY = winnt::FILE_ADD_SUBDIRECTORY; + const FILE_CREATE_PIPE_INSTANCE = winnt::FILE_CREATE_PIPE_INSTANCE; + /// The right to read extended file attributes. + const FILE_READ_EA = winnt::FILE_READ_EA; + /// The right to write extended file attributes. + const FILE_WRITE_EA = winnt::FILE_WRITE_EA; + /// For a file, the right to execute FILE_EXECUTE. + /// For a directory, the right to traverse the directory. + /// By default, users are assigned the BYPASS_TRAVERSE_CHECKING privilege, + /// which ignores the FILE_TRAVERSE access right. + const FILE_EXECUTE = winnt::FILE_EXECUTE; + const FILE_TRAVERSE = winnt::FILE_TRAVERSE; + /// For a directory, the right to delete a directory and all + /// the files it contains, including read-only files. + const FILE_DELETE_CHILD = winnt::FILE_DELETE_CHILD; + /// The right to read file attributes. + const FILE_READ_ATTRIBUTES = winnt::FILE_READ_ATTRIBUTES; + /// The right to write file attributes. + const FILE_WRITE_ATTRIBUTES = winnt::FILE_WRITE_ATTRIBUTES; + /// The right to delete the object. + const DELETE = winnt::DELETE; + /// The right to read the information in the object's security descriptor, + /// not including the information in the system access control list (SACL). + const READ_CONTROL = winnt::READ_CONTROL; + /// The right to use the object for synchronization. This enables a thread + /// to wait until the object is in the signaled state. Some object types + /// do not support this access right. + const SYNCHRONIZE = winnt::SYNCHRONIZE; + /// The right to modify the discretionary access control list (DACL) in + /// the object's security descriptor. + const WRITE_DAC = winnt::WRITE_DAC; + /// The right to change the owner in the object's security descriptor. + const WRITE_OWNER = winnt::WRITE_OWNER; + /// It is used to indicate access to a system access control list (SACL). + const ACCESS_SYSTEM_SECURITY = winnt::ACCESS_SYSTEM_SECURITY; + /// Maximum allowed. + const MAXIMUM_ALLOWED = winnt::MAXIMUM_ALLOWED; + /// Reserved + const RESERVED1 = 0x4000000; + /// Reserved + const RESERVED2 = 0x8000000; + /// Provides all possible access rights. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_ALL`] union. + const GENERIC_ALL = winnt::GENERIC_ALL; + /// Provides execute access. + const GENERIC_EXECUTE = winnt::GENERIC_EXECUTE; + /// Provides write access. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_WRITE`] union. + const GENERIC_WRITE = winnt::GENERIC_WRITE; + /// Provides read access. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_READ`] union. + const GENERIC_READ = winnt::GENERIC_READ; + /// Provides read access. + /// This flag is a union of: FILE_READ_ATTRIBUTES, FILE_READ_DATA, FILE_READ_EA, READ_CONTROL, SYNCHRONIZE + const FILE_GENERIC_READ = AccessMode::FILE_READ_ATTRIBUTES.bits + | AccessMode::FILE_READ_DATA.bits + | AccessMode::FILE_READ_EA.bits + | AccessMode::READ_CONTROL.bits + | AccessMode::SYNCHRONIZE.bits; + /// Provides write access. + /// This flag is a union of: FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, READ_CONTROL, SYNCHRONIZE + const FILE_GENERIC_WRITE = AccessMode::FILE_WRITE_ATTRIBUTES.bits + | AccessMode::FILE_WRITE_DATA.bits + | AccessMode::FILE_WRITE_EA.bits + | AccessMode::READ_CONTROL.bits + | AccessMode::SYNCHRONIZE.bits; + /// Provides execute access. + /// This flag is a union of: FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, READ_CONTROL, SYNCHRONIZE + const FILE_GENERIC_EXECUTE = AccessMode::FILE_EXECUTE.bits + | AccessMode::FILE_READ_ATTRIBUTES.bits + | AccessMode::READ_CONTROL.bits + | AccessMode::SYNCHRONIZE.bits; + /// Provides all accesses. + /// This flag is a union of: FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_GENERIC_EXECUTE + const FILE_GENERIC_ALL = AccessMode::FILE_GENERIC_READ.bits | AccessMode::FILE_GENERIC_WRITE.bits | AccessMode::FILE_GENERIC_EXECUTE.bits; + } +} + +pub unsafe fn get_file_access_mode(handle: RawHandle) -> Result { + use winapi::shared::minwindef::FALSE; + use winapi::um::accctrl; + use winapi::um::aclapi::GetSecurityInfo; + use winapi::um::securitybaseapi::{GetAce, IsValidAcl}; + let mut dacl = 0 as winnt::PACL; + let mut sec_desc = 0 as winnt::PSECURITY_DESCRIPTOR; + + let err = winerror::WinError::from_u32(GetSecurityInfo( + handle, + accctrl::SE_FILE_OBJECT, + winnt::DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut dacl, + std::ptr::null_mut(), + &mut sec_desc, + )); + + if err != winerror::WinError::ERROR_SUCCESS { + return Err(err); + } + + if IsValidAcl(dacl) == FALSE { + return Err(winerror::WinError::last()); + } + + // let count = (*dacl).AceCount; + let mut ace = 0 as winnt::PVOID; + + if GetAce(dacl, 0, &mut ace) == FALSE { + return Err(winerror::WinError::last()); + } + + // TODO: check for PACCESS_ALLOWED_ACE in Ace before accessing + // let header = (*(ace as winnt::PACCESS_ALLOWED_ACE)).Header.AceType; + Ok(AccessMode::from_bits_truncate( + (*(ace as winnt::PACCESS_ALLOWED_ACE)).Mask, + )) +} + +pub fn get_file_path(file: &File) -> Result { + use winapi::um::fileapi::GetFinalPathNameByHandleW; + + let mut raw_path: Vec = vec![0; WIDE_MAX_PATH as usize]; + + let handle = file.as_raw_handle(); + let read_len = + unsafe { GetFinalPathNameByHandleW(handle, raw_path.as_mut_ptr(), WIDE_MAX_PATH, 0) }; + + if read_len == 0 { + // failed to read + return Err(winerror::WinError::last()); + } + + // obtain a slice containing the written bytes, and check for it being too long + // (practically probably impossible) + let written_bytes = raw_path + .get(..read_len as usize) + .ok_or(winerror::WinError::ERROR_BUFFER_OVERFLOW)?; + + Ok(OsString::from_wide(written_bytes)) +} + +pub fn get_fileinfo(file: &File) -> io::Result { + use fileapi::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION}; + use std::mem; + + let handle = file.as_raw_handle(); + let info = unsafe { + let mut info: BY_HANDLE_FILE_INFORMATION = mem::zeroed(); + cvt(GetFileInformationByHandle(handle, &mut info))?; + info + }; + + Ok(info) +} + +pub fn change_time(file: &File) -> io::Result { + use fileapi::FILE_BASIC_INFO; + use minwinbase::FileBasicInfo; + use std::mem; + use winbase::GetFileInformationByHandleEx; + + let handle = file.as_raw_handle(); + let tm = unsafe { + let mut info: FILE_BASIC_INFO = mem::zeroed(); + let infosize = mem::size_of_val(&info); + cvt(GetFileInformationByHandleEx( + handle, + FileBasicInfo, + &mut info as *mut FILE_BASIC_INFO as *mut c_void, + infosize as u32, + ))?; + *info.ChangeTime.QuadPart() + }; + + Ok(tm) +} diff --git a/wasi-common/winx/src/lib.rs b/wasi-common/winx/src/lib.rs new file mode 100644 index 0000000000..44c65b44ba --- /dev/null +++ b/wasi-common/winx/src/lib.rs @@ -0,0 +1,33 @@ +#![deny( + // missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] +#![cfg(windows)] + +#[macro_use] +extern crate bitflags; + +pub mod file; +pub mod time; +pub mod winerror; + +use winerror::WinError; + +pub type Result = std::result::Result; diff --git a/wasi-common/winx/src/time.rs b/wasi-common/winx/src/time.rs new file mode 100644 index 0000000000..28803e1fb6 --- /dev/null +++ b/wasi-common/winx/src/time.rs @@ -0,0 +1,10 @@ +use cvt::cvt; +use winapi::um::{profileapi::QueryPerformanceFrequency, winnt::LARGE_INTEGER}; + +pub fn perf_counter_frequency() -> std::io::Result { + unsafe { + let mut frequency: LARGE_INTEGER = std::mem::zeroed(); + cvt(QueryPerformanceFrequency(&mut frequency))?; + Ok(*frequency.QuadPart() as u64) + } +} diff --git a/wasi-common/winx/src/winerror.rs b/wasi-common/winx/src/winerror.rs new file mode 100644 index 0000000000..bc4e39b331 --- /dev/null +++ b/wasi-common/winx/src/winerror.rs @@ -0,0 +1,127 @@ +#![allow(non_camel_case_types)] +use winapi::shared::winerror; +use winapi::um::errhandlingapi::GetLastError; + +macro_rules! win_error_expand { + { + $( + #[doc=$doc:literal] + $error:ident, + )* + } => { + /// Wraps WINAPI error code as enum. + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + #[repr(u32)] + pub enum WinError { + /// Unknown error occurred. + UnknownError = std::u32::MAX, + $( + #[doc=$doc] + $error = winerror::$error, + )* + } + + fn desc(err: WinError) -> &'static str { + use WinError::*; + match err { + UnknownError => r" Unknown error occurred.", + $($error => $doc,)* + } + } + + fn from_u32(err: u32) -> WinError { + use WinError::*; + match err { + $(winerror::$error => $error,)* + _ => UnknownError, + } + } + } +} + +win_error_expand! { + /// The operation completed successfully. + ERROR_SUCCESS, + /// The system cannot find the file specified. + ERROR_FILE_NOT_FOUND, + /// The system cannot find the path specified. + ERROR_PATH_NOT_FOUND, + /// The system cannot open the file. + ERROR_TOO_MANY_OPEN_FILES, + /// Access is denied. + ERROR_ACCESS_DENIED, + /// The handle is invalid. + ERROR_INVALID_HANDLE, + /// Not enough storage is available to process this command. + ERROR_NOT_ENOUGH_MEMORY, + /// The environment is incorrect. + ERROR_BAD_ENVIRONMENT, + /// Not enough storage is available to complete this operation. + ERROR_OUTOFMEMORY, + /// The device is not ready. + ERROR_NOT_READY, + /// The request is not supported. + ERROR_NOT_SUPPORTED, + /// The file exists. + ERROR_FILE_EXISTS, + /// The pipe has been ended. + ERROR_BROKEN_PIPE, + /// The file name is too long. + ERROR_BUFFER_OVERFLOW, + /// The directory is not empty. + ERROR_DIR_NOT_EMPTY, + /// The volume label you entered exceeds the label character limit of the destination file system. + ERROR_LABEL_TOO_LONG, + /// The requested resource is in use. + ERROR_BUSY, + /// The file name, directory name, or volume label syntax is incorrect. + ERROR_INVALID_NAME, + /// The process cannot access the file because it is being used by another process. + ERROR_SHARING_VIOLATION, + /// A required privilege is not held by the client. + ERROR_PRIVILEGE_NOT_HELD, + /// The file or directory is not a reparse point. + ERROR_NOT_A_REPARSE_POINT, + /// An attempt was made to move the file pointer before the beginning of the file. + ERROR_NEGATIVE_SEEK, + /// The directory name is invalid. + ERROR_DIRECTORY, + /// Cannot create a file when that file already exists. + ERROR_ALREADY_EXISTS, +} + +impl WinError { + /// Returns the last error as WinError. + pub fn last() -> Self { + Self::from_u32(unsafe { GetLastError() }) + } + + /// Constructs WinError from error code. + pub fn from_u32(err: u32) -> Self { + from_u32(err) + } + + /// Returns error's description string. This description matches + /// the docs for the error. + pub fn desc(self) -> &'static str { + desc(self) + } +} + +impl std::error::Error for WinError { + fn description(&self) -> &str { + self.desc() + } +} + +impl std::fmt::Display for WinError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}: {}", self, self.desc()) + } +} + +impl From for std::io::Error { + fn from(err: WinError) -> Self { + Self::from_raw_os_error(err as i32) + } +} diff --git a/wasmtime-api/Cargo.toml b/wasmtime-api/Cargo.toml index 8975cb2ba7..76ad9c5da6 100644 --- a/wasmtime-api/Cargo.toml +++ b/wasmtime-api/Cargo.toml @@ -35,7 +35,7 @@ core = ["hashbrown/nightly", "cranelift-codegen/core", "cranelift-wasm/core", "w [dev-dependencies] # for wasmtime.rs -wasi-common = { git = "https://github.com/CraneStation/wasi-common", rev = "2fe3530"} +wasi-common = { path = "../wasi-common" } docopt = "1.0.1" serde = { "version" = "1.0.94", features = ["derive"] } pretty_env_logger = "0.3.0" diff --git a/wasmtime-wasi/Cargo.toml b/wasmtime-wasi/Cargo.toml index 8cd647ec34..5e87c26e54 100644 --- a/wasmtime-wasi/Cargo.toml +++ b/wasmtime-wasi/Cargo.toml @@ -13,7 +13,7 @@ edition = "2018" wasmtime-runtime = { path = "../wasmtime-runtime" } wasmtime-environ = { path = "../wasmtime-environ" } wasmtime-jit = { path = "../wasmtime-jit" } -wasi-common = { git = "https://github.com/CraneStation/wasi-common", rev = "2fe3530"} +wasi-common = { path = "../wasi-common" } cranelift-codegen = { version = "0.49", features = ["enable-serde"] } cranelift-entity = { version = "0.49", features = ["enable-serde"] } cranelift-wasm = { version = "0.49", features = ["enable-serde"] }