commit d37320c3d89727a60e62ee738da95104c6663304 Author: Dan Gohman Date: Mon Sep 14 13:26:29 2020 -0700 Initial checkin. diff --git a/.github/actions/install-rust/README.md b/.github/actions/install-rust/README.md new file mode 100644 index 00000000..e90a00f4 --- /dev/null +++ b/.github/actions/install-rust/README.md @@ -0,0 +1,20 @@ +# install-rust + +Author: Alex Crichton + +A small github action to install `rustup` and a Rust toolchain. This is +generally expressed inline, but it was repeated enough in this repository it +seemed worthwhile to extract. + +Some gotchas: + +* Can't `--self-update` on Windows due to permission errors (a bug in Github + Actions) +* `rustup` isn't installed on macOS (a bug in Github Actions) + +When the above are fixed we should delete this action and just use this inline: + +```yml +- run: rustup update $toolchain && rustup default $toolchain + shell: bash +``` \ No newline at end of file diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml new file mode 100644 index 00000000..d82fe182 --- /dev/null +++ b/.github/actions/install-rust/action.yml @@ -0,0 +1,13 @@ +# Author: Alex Crichton +name: 'Install Rust toolchain' +description: 'Install both `rustup` and a Rust toolchain' + +inputs: + toolchain: + description: 'Default toolchan to install' + required: false + default: 'stable' + +runs: + using: node12 + main: 'main.js' \ No newline at end of file diff --git a/.github/actions/install-rust/main.js b/.github/actions/install-rust/main.js new file mode 100644 index 00000000..f464bbab --- /dev/null +++ b/.github/actions/install-rust/main.js @@ -0,0 +1,28 @@ +// Author: Alex Crichton +const child_process = require('child_process'); +const toolchain = process.env.INPUT_TOOLCHAIN; + +if (process.platform === 'darwin') { + child_process.execSync(`curl https://sh.rustup.rs | sh -s -- -y --default-toolchain=none --profile=minimal`); + const bindir = `${process.env.HOME}/.cargo/bin`; + console.log(`::add-path::${bindir}`); + process.env.PATH = `${process.env.PATH}:${bindir}`; +} + +child_process.execFileSync('rustup', ['set', 'profile', 'minimal']); +child_process.execFileSync('rustup', ['update', toolchain, '--no-self-update']); +child_process.execFileSync('rustup', ['default', toolchain]); + +// Deny warnings on CI to keep our code warning-free as it lands in-tree. Don't +// do this on nightly though since there's a fair amount of warning churn there. +if (!toolchain.startsWith('nightly')) { + console.log(`::set-env name=RUSTFLAGS::-D warnings`); +} + +// Save disk space by avoiding incremental compilation, and also we don't use +// any caching so incremental wouldn't help anyway. +console.log(`::set-env name=CARGO_INCREMENTAL::0`); + +// Turn down debuginfo from 2 to 1 to help save disk space +console.log(`::set-env name=CARGO_PROFILE_DEV_DEBUG::1`); +console.log(`::set-env name=CARGO_PROFILE_TEST_DEBUG::1`); \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..52efb88e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: stable + - run: cargo fmt --all -- --check + + check: + name: Check + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [stable, beta, nightly] + include: + - build: stable + os: ubuntu-latest + rust: stable + - build: beta + os: ubuntu-latest + rust: beta + - build: nightly + os: ubuntu-latest + rust: nightly + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: ${{ matrix.rust }} + - run: > + rustup target add + x86_64-unknown-linux-musl + x86_64-unknown-linux-gnux32 + x86_64-linux-android + x86_64-apple-darwin + x86_64-unknown-freebsd + x86_64-unknown-netbsd + x86_64-fuchsia + i686-unknown-linux-gnu + i686-unknown-linux-musl + wasm32-unknown-emscripten + - run: cargo check --workspace --release -vv + - run: cargo check --workspace --release -vv --target=x86_64-unknown-linux-musl + - run: cargo check --workspace --release -vv --target=x86_64-unknown-linux-gnux32 + - run: cargo check --workspace --release -vv --target=x86_64-linux-android + - run: cargo check --workspace --release -vv --target=x86_64-apple-darwin + - run: cargo check --workspace --release -vv --target=x86_64-unknown-freebsd + - run: cargo check --workspace --release -vv --target=x86_64-unknown-netbsd + - run: cargo check --workspace --release -vv --target=x86_64-fuchsia + - run: cargo check --workspace --release -vv --target=i686-unknown-linux-gnu + - run: cargo check --workspace --release -vv --target=i686-unknown-linux-musl + - run: cargo check --workspace --release -vv --target=wasm32-unknown-emscripten diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..8148fc63 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +# This file tells tools we use rustfmt. We use the default settings. diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 00000000..2de3db6e --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,29 @@ +Short version for non-lawyers: + +`posish` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `posish` project are retained by their contributors. +No copyright assignment is required to contribute to the `posish` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `posish` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..38a7c510 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "posish" +version = "0.0.0" +authors = [ + "Dan Gohman ", + "Jakub Konka ", +] +description = "Safe Rust bindings to POSIX-ish libc APIs" +documentation = "https://docs.rs/posish" +license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" +repository = "https://github.com/sunfishcode/posish" +edition = "2018" +keywords = ["api", "file", "network"] +categories = ["filesystem", "network-programming"] +readme = "README.md" + +[dependencies] +bitflags = "1.2.1" +cfg-if = "0.1.10" +errno = "0.2.6" +libc = { version = "0.2.77", features = ["extra_traits"] } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + 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. diff --git a/LICENSE-Apache-2.0_WITH_LLVM-exception b/LICENSE-Apache-2.0_WITH_LLVM-exception new file mode 100644 index 00000000..f9d81955 --- /dev/null +++ b/LICENSE-Apache-2.0_WITH_LLVM-exception @@ -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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..638e2367 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +
+

posish

+ +

+ Safe Rust bindings to POSIX-ish libc APIs +

+ +

+ Github Actions CI Status + crates.io page + docs.rs docs +

+
+ +`posish` provides safe wrappers to POSIX-ish `libc` APIs. + +`posish` is focused on functionality that isn't already provided by [`std`] +or [`getrandom`]. It has a philosophy of considering raw file descriptors to be +equivalent to pointers in terms of making APIs unsafe, in the spirit of +[`std::os::unix::io::FromRawFd::from_raw_fd`] being unsafe. + +`posish` is relatively low-level and does not support Windows; for higher-level +and portable APIs to this functionality, see the [`system-interface`] and +[`cap-std`] crates. + +[`std`]: https://doc.rust-lang.org/std/ +[`getrandom`]: https://crates.io/crates/getrandom +[`std::os::unix::io::FromRawFd::from_raw_fd`]: https://doc.rust-lang.org/std/os/unix/io/trait.FromRawFd.html#tymethod.from_raw_fd +[`system-interface`]: https://crates.io/crates/system-interface +[`cap-std`]: https://crates.io/crates/cap-std diff --git a/src/fs/android.rs b/src/fs/android.rs new file mode 100644 index 00000000..7556efb9 --- /dev/null +++ b/src/fs/android.rs @@ -0,0 +1,23 @@ +//! Work around Bionic (Android's libc) lacking `telldir` and `seekdir`. See +//! +//! https://kernel.googlesource.com/pub/scm/fs/ext2/xfstests-bld/+/HEAD/android-compat/telldir.c +//! https://kernel.googlesource.com/pub/scm/fs/ext2/xfstests-bld/+/7ca4e52401a5e3376ee285cbb973f84559c12572/android-compat/telldir.c +//! +//! for details. + +use crate::zero_ok; + +#[repr(C)] +struct DIR_INTERNALS { + fd: libc::c_int, +} + +pub(crate) unsafe fn telldir(dir: *mut libc::DIR) -> libc::c_long { + let dir = &*(dir as *mut DIR_INTERNALS); + libc::lseek(dir.fd, 0, libc::SEEK_CUR) +} + +pub(crate) unsafe fn seekdir(dir: *mut libc::DIR, loc: libc::c_long) { + let dir = &*(dir as *mut DIR_INTERNALS); + zero_ok(libc::lseek(dir.fd, loc, libc::SEEK_SET)).unwrap() +} diff --git a/src/fs/at.rs b/src/fs/at.rs new file mode 100644 index 00000000..2cb0cb74 --- /dev/null +++ b/src/fs/at.rs @@ -0,0 +1,348 @@ +//! POSIX-style `*at` functions. + +use crate::{ + fs::{Access, AtFlags, LibcStat, Mode, OFlags, PathArg}, + negone_err, zero_ok, +}; +#[cfg(not(any(target_os = "linux", target_os = "emscripten", target_os = "l4re")))] +use libc::{fstatat as libc_fstatat, openat as libc_openat}; +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] +use libc::{fstatat64 as libc_fstatat, openat64 as libc_openat}; +#[cfg(unix)] +use std::os::unix::{ + ffi::OsStrExt, + io::{AsRawFd, FromRawFd, RawFd}, +}; +#[cfg(target_os = "wasi")] +use std::os::wasi::{ + ffi::OsStringExt, + io::{AsRawFd, FromRawFd, RawFd}, +}; +use std::{ + convert::TryInto, + ffi::{CStr, OsString}, + fs, io, + mem::MaybeUninit, +}; +#[cfg(not(target_os = "wasi"))] +use std::{ffi::OsStr, mem::ManuallyDrop}; + +/// Return a "file" which holds a handle which refers to the process current +/// directory (`AT_FDCWD`). It is a `ManuallyDrop`, however the caller should +/// not drop it explicitly, as it refers to an ambient authority rather than +/// an open resource. +#[cfg(not(target_os = "wasi"))] +pub fn cwd() -> ManuallyDrop { + ManuallyDrop::new(unsafe { fs::File::from_raw_fd(libc::AT_FDCWD) }) +} + +/// `openat(dirfd, path, oflags, mode)` +#[inline] +pub fn openat( + dirfd: &Fd, + path: P, + oflags: OFlags, + mode: Mode, +) -> io::Result { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _openat(dirfd, &path, oflags, mode) } +} + +unsafe fn _openat(dirfd: RawFd, path: &CStr, oflags: OFlags, mode: Mode) -> io::Result { + #[allow(clippy::useless_conversion)] + let fd = negone_err(libc_openat( + dirfd as libc::c_int, + path.as_ptr(), + oflags.bits(), + libc::c_uint::from(mode.bits()), + ))?; + + Ok(fs::File::from_raw_fd(fd as RawFd)) +} + +/// `readlinkat(fd, path)` +#[inline] +pub fn readlinkat(dirfd: &Fd, path: P) -> io::Result { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _readlinkat(dirfd, &path) } +} + +/// Implement `readlinkat` on platforms which have `PATH_MAX` that we can rely on. +#[cfg(not(target_os = "wasi"))] +unsafe fn _readlinkat(dirfd: RawFd, path: &CStr) -> io::Result { + let buffer = &mut [0_u8; libc::PATH_MAX as usize + 1]; + let nread = negone_err(libc::readlinkat( + dirfd, + path.as_ptr(), + buffer.as_mut_ptr() as *mut libc::c_char, + buffer.len(), + ))?; + + let nread = nread.try_into().unwrap(); + let link = OsStr::from_bytes(&buffer[0..nread]); + Ok(link.into()) +} + +/// Implemented `readlinkat` on platforms where we dyamically allocate the buffer instead. +#[cfg(target_os = "wasi")] +unsafe fn _readlinkat(dirfd: RawFd, path: &CStr) -> io::Result { + // Start with a buffer big enough for the vast majority of paths. + let mut buffer = Vec::with_capacity(256); + loop { + let nread = negone_err(unsafe { + libc::readlinkat( + dirfd as libc::c_int, + path.as_ptr(), + buffer.as_mut_ptr() as *mut libc::c_char, + buffer.capacity(), + ) + })?; + + let nread = nread.try_into().unwrap(); + buffer.set_len(nread); + if nread < buffer.capacity() { + return Ok(OsString::from_vec(buffer)); + } + + // This would be a good candidate for `try_reserve`. + // https://github.com/rust-lang/rust/issues/48043 + buffer.reserve(1); + } +} + +/// `mkdirat(fd, path, mode)` +#[inline] +pub fn mkdirat(dirfd: &Fd, path: P, mode: Mode) -> io::Result<()> { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _mkdirat(dirfd, &path, mode) } +} + +unsafe fn _mkdirat(dirfd: RawFd, path: &CStr, mode: Mode) -> io::Result<()> { + zero_ok(libc::mkdirat( + dirfd as libc::c_int, + path.as_ptr(), + mode.bits(), + )) +} + +/// `linkat(old_dirfd, old_path, new_dirfd, new_path, flags)` +#[inline] +pub fn linkat( + old_dirfd: &PFd, + old_path: P, + new_dirfd: &QFd, + new_path: Q, + flags: AtFlags, +) -> io::Result<()> { + let old_dirfd = old_dirfd.as_raw_fd(); + let new_dirfd = new_dirfd.as_raw_fd(); + let old_path = old_path.as_cstr()?; + let new_path = new_path.as_cstr()?; + unsafe { _linkat(old_dirfd, &old_path, new_dirfd, &new_path, flags) } +} + +unsafe fn _linkat( + old_dirfd: RawFd, + old_path: &CStr, + new_dirfd: RawFd, + new_path: &CStr, + flags: AtFlags, +) -> io::Result<()> { + zero_ok(libc::linkat( + old_dirfd as libc::c_int, + old_path.as_ptr(), + new_dirfd as libc::c_int, + new_path.as_ptr(), + flags.bits(), + )) +} + +/// `unlinkat(fd, path, flags)` +#[inline] +pub fn unlinkat(dirfd: &Fd, path: P, flags: AtFlags) -> io::Result<()> { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _unlinkat(dirfd, &path, flags) } +} + +unsafe fn _unlinkat(dirfd: RawFd, path: &CStr, flags: AtFlags) -> io::Result<()> { + zero_ok(libc::unlinkat( + dirfd as libc::c_int, + path.as_ptr(), + flags.bits(), + )) +} + +/// `renameat(old_dirfd, old_path, new_dirfd, new_path)` +#[inline] +pub fn renameat( + old_dirfd: &PFd, + old_path: P, + new_dirfd: &QFd, + new_path: Q, +) -> io::Result<()> { + let old_dirfd = old_dirfd.as_raw_fd(); + let new_dirfd = new_dirfd.as_raw_fd(); + let old_path = old_path.as_cstr()?; + let new_path = new_path.as_cstr()?; + unsafe { _renameat(old_dirfd, &old_path, new_dirfd, &new_path) } +} + +unsafe fn _renameat( + old_dirfd: RawFd, + old_path: &CStr, + new_dirfd: RawFd, + new_path: &CStr, +) -> io::Result<()> { + zero_ok(libc::renameat( + old_dirfd as libc::c_int, + old_path.as_ptr(), + new_dirfd as libc::c_int, + new_path.as_ptr(), + )) +} + +/// `symlinkat(old_dirfd, old_path, new_dirfd, new_path)` +#[inline] +pub fn symlinkat( + old_path: P, + new_dirfd: &Fd, + new_path: Q, +) -> io::Result<()> { + let new_dirfd = new_dirfd.as_raw_fd(); + let old_path = old_path.as_cstr()?; + let new_path = new_path.as_cstr()?; + unsafe { _symlinkat(&old_path, new_dirfd, &new_path) } +} + +unsafe fn _symlinkat(old_path: &CStr, new_dirfd: RawFd, new_path: &CStr) -> io::Result<()> { + zero_ok(libc::symlinkat( + old_path.as_ptr(), + new_dirfd as libc::c_int, + new_path.as_ptr(), + )) +} + +/// `fstatat(dirfd, path, flags)` +#[inline] +pub fn statat( + dirfd: &Fd, + path: P, + flags: AtFlags, +) -> io::Result { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _statat(dirfd, &path, flags) } +} + +unsafe fn _statat(dirfd: RawFd, path: &CStr, flags: AtFlags) -> io::Result { + let mut stat = MaybeUninit::::uninit(); + zero_ok(libc_fstatat( + dirfd as libc::c_int, + path.as_ptr(), + stat.as_mut_ptr(), + flags.bits(), + ))?; + Ok(stat.assume_init()) +} + +/// `faccessat(dirfd, path, access, flags)` +#[inline] +pub fn accessat( + dirfd: &Fd, + path: P, + access: Access, + flags: AtFlags, +) -> io::Result<()> { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _accessat(dirfd, &path, access, flags) } +} + +#[cfg(not(target_os = "emscripten"))] +unsafe fn _accessat(dirfd: RawFd, path: &CStr, access: Access, flags: AtFlags) -> io::Result<()> { + zero_ok(libc::faccessat( + dirfd as libc::c_int, + path.as_ptr(), + access.bits(), + flags.bits(), + )) +} + +// Temporarily disable on Emscripten until https://github.com/rust-lang/libc/pull/1836 +// is available. +#[cfg(target_os = "emscripten")] +unsafe fn _accessat( + _dirfd: RawFd, + _path: &CStr, + _access: Access, + _flags: AtFlags, +) -> io::Result<()> { + Ok(()) +} + +/// `utimensat(dirfd, path, times, flags)` +pub fn utimensat( + dirfd: &Fd, + path: P, + times: &[libc::timespec; 2], + flags: AtFlags, +) -> io::Result<()> { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _utimensat(dirfd, &path, times, flags) } +} + +unsafe fn _utimensat( + dirfd: RawFd, + path: &CStr, + times: &[libc::timespec; 2], + flags: AtFlags, +) -> io::Result<()> { + zero_ok(libc::utimensat( + dirfd as libc::c_int, + path.as_ptr(), + times.as_ptr(), + flags.bits(), + )) +} + +/// `fchmodat(dirfd, path, mode, 0)` +/// +/// The flags argument is fixed to 0, so `AT_SYMLINK_NOFOLLOW` is not supported. +///
+/// Platform support for this flag varies widely. +///
+/// +/// Note that this implementation does not support `O_PATH` file descriptors, +/// even on platforms where the host libc emulates it. +#[cfg(not(target_os = "wasi"))] +pub fn chmodat(dirfd: &Fd, path: P, mode: Mode) -> io::Result<()> { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _chmodat(dirfd, &path, mode) } +} + +#[cfg(not(any(target_os = "linux", target_os = "wasi")))] +unsafe fn _chmodat(dirfd: RawFd, path: &CStr, mode: Mode) -> io::Result<()> { + zero_ok(libc::fchmodat( + dirfd as libc::c_int, + path.as_ptr(), + mode.bits(), + 0, + )) +} + +#[cfg(target_os = "linux")] +unsafe fn _chmodat(dirfd: RawFd, path: &CStr, mode: Mode) -> io::Result<()> { + // Note that Linux's `fchmodat` does not have a flags argument. + zero_ok(libc::syscall( + libc::SYS_fchmodat, + dirfd as libc::c_int, + path.as_ptr(), + mode.bits(), + )) +} diff --git a/src/fs/constants.rs b/src/fs/constants.rs new file mode 100644 index 00000000..897643ea --- /dev/null +++ b/src/fs/constants.rs @@ -0,0 +1,245 @@ +//! Libc filesystem constants, translated into `bitflags` constants. + +use bitflags::bitflags; +use cfg_if::cfg_if; + +bitflags! { + /// `FD_*` constants. + pub struct FdFlags: libc::c_int { + /// `FD_CLOEXEC` + const CLOEXEC = libc::FD_CLOEXEC; + } +} + +bitflags! { + /// `*_OK` constants for use with [`accessat`]. + /// + /// [`accessat`]: fn.accessat.html + pub struct Access: libc::c_int { + /// `R_OK` + const READ_OK = libc::R_OK; + + /// `W_OK` + const WRITE_OK = libc::W_OK; + + /// `X_OK` + const EXEC_OK = libc::X_OK; + + /// `F_OK` + const EXISTS = libc::F_OK; + } +} + +bitflags! { + /// `AT_*` constants. + pub struct AtFlags: libc::c_int { + /// `AT_REMOVEDIR` + const REMOVEDIR = libc::AT_REMOVEDIR; + + /// `AT_SYMLINK_FOLLOW` + const SYMLINK_FOLLOW = libc::AT_SYMLINK_FOLLOW; + + /// `AT_SYMLINK_NOFOLLOW` + const SYMLINK_NOFOLLOW = libc::AT_SYMLINK_NOFOLLOW; + + /// `AT_EMPTY_PATH` + #[cfg(any(target_os = "android", + target_os = "fuchsia", + target_os = "linux"))] + const EMPTY_PATH = libc::AT_EMPTY_PATH; + + /// `AT_EACCESS` + // Temporarily disable on Emscripten until https://github.com/rust-lang/libc/pull/1836 + // is available. + #[cfg(not(any(target_os = "emscripten", target_os = "android")))] + const EACCESS = libc::AT_EACCESS; + } +} + +#[cfg(not(target_os = "wasi"))] +bitflags! { + /// `S_I*` constants. + pub struct Mode: libc::mode_t { + /// `S_IRWXU` + const IRWXU = libc::S_IRWXU; + + /// `S_IRUSR` + const IRUSR = libc::S_IRUSR; + + /// `S_IWUSR` + const IWUSR = libc::S_IWUSR; + + /// `S_IXUSR` + const IXUSR = libc::S_IXUSR; + + /// `S_IRWXG` + const IRWXG = libc::S_IRWXG; + + /// `S_IRGRP` + const IRGRP = libc::S_IRGRP; + + /// `S_IWGRP` + const IWGRP = libc::S_IWGRP; + + /// `S_IXGRP` + const IXGRP = libc::S_IXGRP; + + /// `S_IRWXO` + const IRWXO = libc::S_IRWXO; + + /// `S_IROTH` + const IROTH = libc::S_IROTH; + + /// `S_IWOTH` + const IWOTH = libc::S_IWOTH; + + /// `S_IXOTH` + const IXOTH = libc::S_IXOTH; + + /// `S_ISUID` + const ISUID = libc::S_ISUID as libc::mode_t; + + /// `S_ISGID` + const ISGID = libc::S_ISGID as libc::mode_t; + + /// `S_ISVTX` + const ISVTX = libc::S_ISVTX as libc::mode_t; + } +} + +#[cfg(target_os = "wasi")] +pub struct Mode {} + +#[cfg(target_os = "wasi")] +impl Mode { + pub fn bits(&self) -> u32 { + 0 + } +} + +bitflags! { + /// `O_*` constants. + pub struct OFlags: libc::c_int { + /// `O_ACCMODE` + const ACCMODE = libc::O_ACCMODE; + + /// Similar to `ACCMODE`, but just includes the read/write flags, and + /// no other flags. + /// + /// Some libc implementations include `O_PATH` in `O_ACCMODE`, which + /// sometimes we really just want the read/write bits. Caution is + /// indicated, as the presence of `O_PATH` may mean that the read/write + /// bits don't have their usual meaning. + const RWMODE = libc::O_RDONLY | libc::O_WRONLY | libc::O_RDWR; + + /// `O_APPEND` + const APPEND = libc::O_APPEND; + + /// `O_CREAT` + const CREATE = libc::O_CREAT; + + /// `O_DIRECTORY` + const DIRECTORY = libc::O_DIRECTORY; + + /// `O_DSYNC` + const DSYNC = { + cfg_if! { + if #[cfg(any(target_os = "android", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "wasi"))] { + libc::O_DSYNC + } else if #[cfg(target_os = "freebsd")] { + // FreeBSD lacks `O_DSYNC`; emulate it with `O_SYNC`, which + // is correct, though conservative. + // https://reviews.freebsd.org/D19407#inline-118670 + libc::O_SYNC + } + } + }; + + /// `O_EXCL` + const EXCL = libc::O_EXCL; + + /// `O_FSYNC` + #[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "ios", + all(target_os = "linux", not(target_env = "musl")), + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] + const FSYNC = libc::O_FSYNC; + + /// `O_NOFOLLOW` + const NOFOLLOW = libc::O_NOFOLLOW; + + /// `O_NONBLOCK` + const NONBLOCK = libc::O_NONBLOCK; + + /// `O_RDONLY` + const RDONLY = libc::O_RDONLY; + + /// `O_WRONLY` + const WRONLY = libc::O_WRONLY; + + /// `O_RDWR` + const RDWR = libc::O_RDWR; + + /// `O_NOCTTY` + const NOCTTY = libc::O_NOCTTY; + + /// `O_RSYNC` + #[cfg(any(target_os = "emscripten", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", + target_os = "wasi", + ))] + const RSYNC = libc::O_RSYNC; + + /// `O_SYNC` + const SYNC = libc::O_SYNC; + + /// `O_TRUNC` + const TRUNC = libc::O_TRUNC; + + /// `O_PATH` + #[cfg(any(target_os = "android", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "linux", + target_os = "redox"))] + const PATH = libc::O_PATH; + + /// `O_CLOEXEC` + #[cfg(any(target_os = "android", + target_os = "dragonfly", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "freebsd", + target_os = "haiku", + target_os = "hermit", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "redox", + target_os = "solaris", + target_os = "vxworks"))] + const CLOEXEC = libc::O_CLOEXEC; + + /// `O_TMPFILE` + #[cfg(any(target_os = "android", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "linux"))] + const TMPFILE = libc::O_TMPFILE; + } +} diff --git a/src/fs/dir.rs b/src/fs/dir.rs new file mode 100644 index 00000000..14c26179 --- /dev/null +++ b/src/fs/dir.rs @@ -0,0 +1,197 @@ +//! `Dir`, `Entry`, and `SeekLoc`. + +#[cfg(target_os = "android")] +use crate::fs::android::{seekdir as libc_seekdir, telldir as libc_telldir}; +use crate::fs::FileType; +use errno::{set_errno, Errno}; +#[cfg(not(any( + target_os = "android", + target_os = "emscripten", + target_os = "l4re", + target_os = "linux" +)))] +use libc::{dirent as libc_dirent, readdir as libc_readdir}; +#[cfg(any( + target_os = "android", + target_os = "emscripten", + target_os = "l4re", + target_os = "linux" +))] +use libc::{dirent64 as libc_dirent, readdir64 as libc_readdir}; +#[cfg(not(target_os = "android"))] +use libc::{seekdir as libc_seekdir, telldir as libc_telldir}; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, IntoRawFd, RawFd}; +use std::{convert::TryInto, ffi::CStr, io, ptr}; + +/// `DIR*` +pub struct Dir(ptr::NonNull); + +impl Dir { + /// Construct a `Dir`, assuming ownership of the file descriptor. + #[inline] + pub fn from(fd: F) -> io::Result { + let fd = fd.into_raw_fd(); + unsafe { Self::_from(fd) } + } + + unsafe fn _from(fd: RawFd) -> io::Result { + let d = libc::fdopendir(fd as libc::c_int); + if let Some(d) = ptr::NonNull::new(d) { + Ok(Self(d)) + } else { + let e = io::Error::last_os_error(); + libc::close(fd as libc::c_int); + Err(e) + } + } + + /// `seekdir(self, loc)` + #[inline] + pub fn seek(&self, loc: SeekLoc) { + unsafe { libc_seekdir(self.0.as_ptr(), loc.0) } + } + + /// `telldir(self)` + #[inline] + pub fn tell(&self) -> SeekLoc { + SeekLoc(unsafe { libc_telldir(self.0.as_ptr()) }) + } + + /// `rewinddir(self)` + #[inline] + pub fn rewind(&self) { + unsafe { libc::rewinddir(self.0.as_ptr()) } + } + + /// `readdir(self)`, where `None` means the end of the directory. + pub fn read(&self) -> Option> { + set_errno(Errno(0)); + let dirent = unsafe { libc_readdir(self.0.as_ptr()) }; + if dirent.is_null() { + let curr_errno = io::Error::last_os_error(); + if curr_errno.raw_os_error() == Some(0) { + // We successfully reached the end of the stream. + None + } else { + // `errno` is unknown or non-zero, so an error occurred. + Some(Err(curr_errno)) + } + } else { + // We successfully read an entry. + Some(Ok(Entry { + dirent: unsafe { *dirent }, + })) + } + } +} + +unsafe impl Send for Dir {} +unsafe impl Sync for Dir {} + +impl AsRawFd for Dir { + #[inline] + fn as_raw_fd(&self) -> RawFd { + unsafe { libc::dirfd(self.0.as_ptr()) as RawFd } + } +} + +impl Drop for Dir { + #[inline] + fn drop(&mut self) { + unsafe { libc::closedir(self.0.as_ptr()) }; + } +} + +impl Iterator for Dir { + type Item = io::Result; + + #[inline] + fn next(&mut self) -> Option { + Self::read(self) + } +} + +/// `struct dirent` +#[derive(Debug)] +pub struct Entry { + dirent: libc_dirent, +} + +impl Entry { + /// Returns the file name of this directory entry. + #[inline] + pub fn file_name(&self) -> &CStr { + unsafe { CStr::from_ptr(self.dirent.d_name.as_ptr()) } + } + + /// Returns the type of this directory entry. + #[inline] + pub fn file_type(&self) -> FileType { + FileType::from_dirent_d_type(self.dirent.d_type) + } + + /// Return the inode number of this directory entry. + #[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))] + #[inline] + pub fn ino(&self) -> u64 { + self.dirent.d_ino + } + + /// Return the inode number of this directory entry. + #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] + #[inline] + pub fn ino(&self) -> u64 { + self.dirent.d_fileno.into() + } + + /// Return a cookie indicating the location of this entry, for use with [`Dir::seek`]. + /// + /// [`Dir::seek`]: struct.Dir.html#method.seek + #[cfg(not(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + )))] + #[inline] + pub fn seek_loc(&self) -> io::Result { + let off_i64: i64 = self.dirent.d_off; + unsafe { SeekLoc::from_raw(off_i64 as u64) } + } +} + +/// A location for use with [`Dir::seek`]. +/// +/// [`Dir::seek`]: struct.Dir.html#method.seek +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct SeekLoc(libc::c_long); + +impl SeekLoc { + /// Return the location encoded as a `u64`. Note that this value is meant to + /// be opaque, and applications shouldn't do anything with it except call + /// `SeekLoc::from_raw`. + #[allow(clippy::useless_conversion)] + #[inline] + pub fn to_raw(&self) -> u64 { + i64::from(self.0) as u64 + } + + /// Construct a new `SeekLoc` from a value returned by `SeekLoc::to_raw`. + /// + /// # Safety + /// + /// The passed-in `loc` value must be a value returned from + /// `SeekLoc::to_raw`. + #[inline] + pub unsafe fn from_raw(loc: u64) -> io::Result { + Ok(Self( + loc.try_into() + .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?, + )) + } +} diff --git a/src/fs/fadvise.rs b/src/fs/fadvise.rs new file mode 100644 index 00000000..0d4aa6aa --- /dev/null +++ b/src/fs/fadvise.rs @@ -0,0 +1,55 @@ +use crate::zero_ok; +#[cfg(not(any(target_os = "linux", target_os = "emscripten", target_os = "l4re")))] +use libc::posix_fadvise as libc_posix_fadvise; +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] +use libc::posix_fadvise64 as libc_posix_fadvise; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{convert::TryInto, io}; + +/// `POSIX_FADV_*` constants. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[repr(i32)] +pub enum Advice { + /// `POSIX_FADV_NORMAL` + Normal = libc::POSIX_FADV_NORMAL, + + /// `POSIX_FADV_SEQUENTIAL` + Sequential = libc::POSIX_FADV_SEQUENTIAL, + + /// `POSIX_FADV_RANDOM` + Random = libc::POSIX_FADV_RANDOM, + + /// `POSIX_FADV_NOREUSE` + NoReuse = libc::POSIX_FADV_NOREUSE, + + /// `POSIX_FADV_WILLNEED` + WillNeed = libc::POSIX_FADV_WILLNEED, + + /// `POSIX_FADV_DONTNEED` + DontNeed = libc::POSIX_FADV_DONTNEED, +} + +/// `posix_fadvise(fd, offset, len, advice)` +#[inline] +pub fn fadvise(fd: &Fd, offset: u64, len: u64, advice: Advice) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _fadvise(fd, offset, len, advice) } +} + +unsafe fn _fadvise(fd: RawFd, offset: u64, len: u64, advice: Advice) -> io::Result<()> { + if let (Ok(offset), Ok(len)) = (offset.try_into(), len.try_into()) { + zero_ok(libc_posix_fadvise( + fd as libc::c_int, + offset, + len, + advice as libc::c_int, + ))?; + } + + // If the offset or length can't be converted, ignore the advice, as it + // isn't likely to be useful in that case. + Ok(()) +} diff --git a/src/fs/fcntl.rs b/src/fs/fcntl.rs new file mode 100644 index 00000000..18588b31 --- /dev/null +++ b/src/fs/fcntl.rs @@ -0,0 +1,53 @@ +use crate::{ + fs::{FdFlags, OFlags}, + negone_err, zero_ok, +}; +use std::io; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; + +/// `fcntl(fd, F_GETFD)` +#[inline] +pub fn getfd(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _getfd(fd) } +} + +unsafe fn _getfd(fd: RawFd) -> io::Result { + negone_err(libc::fcntl(fd as libc::c_int, libc::F_GETFD)).map(FdFlags::from_bits_truncate) +} + +/// `fcntl(fd, F_SETFD, flags)` +#[inline] +pub fn setfd(fd: &Fd, flags: FdFlags) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _setfd(fd, flags) } +} + +unsafe fn _setfd(fd: RawFd, flags: FdFlags) -> io::Result<()> { + zero_ok(libc::fcntl(fd as libc::c_int, libc::F_SETFD, flags.bits())) +} + +/// `fcntl(fd, F_GETFL)` +#[inline] +pub fn getfl(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _getfl(fd) } +} + +unsafe fn _getfl(fd: RawFd) -> io::Result { + negone_err(libc::fcntl(fd as libc::c_int, libc::F_GETFL)).map(OFlags::from_bits_truncate) +} + +/// `fcntl(fd, F_SETFL, flags)` +#[inline] +pub fn setfl(fd: &Fd, flags: OFlags) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _setfl(fd, flags) } +} + +unsafe fn _setfl(fd: RawFd, flags: OFlags) -> io::Result<()> { + zero_ok(libc::fcntl(fd as libc::c_int, libc::F_SETFL, flags.bits())) +} diff --git a/src/fs/fd.rs b/src/fs/fd.rs new file mode 100644 index 00000000..0cf8b8c1 --- /dev/null +++ b/src/fs/fd.rs @@ -0,0 +1,134 @@ +//! Functions which operate on file descriptors. + +#[cfg(not(target_os = "wasi"))] +use crate::fs::Mode; +use crate::{negone_err, zero_ok}; +#[cfg(not(any( + target_os = "linux", + target_os = "emscripten", + target_os = "l4re", + target_os = "netbsd", + target_os = "wasi" +)))] +use libc::fstatfs as libc_fstatfs; +#[cfg(not(any(target_os = "linux", target_os = "emscripten", target_os = "l4re")))] +use libc::lseek as libc_lseek; +#[cfg(not(any( + target_os = "ios", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "redox", + target_os = "wasi", +)))] +use libc::off64_t as libc_off_t; +#[cfg(any( + target_os = "ios", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "redox", + target_os = "wasi", +))] +use libc::off_t as libc_off_t; +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] +use libc::{fstatfs64 as libc_fstatfs, lseek64 as libc_lseek}; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{ + convert::TryInto, + io::{self, SeekFrom}, +}; +#[cfg(not(any(target_os = "netbsd", target_os = "wasi")))] +// not implemented in libc for netbsd yet +use {crate::fs::LibcStatFs, std::mem::MaybeUninit}; + +/// `lseek(fd, offset, whence)` +#[inline] +pub fn seek(fd: &Fd, pos: SeekFrom) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _seek(fd, pos) } +} + +unsafe fn _seek(fd: RawFd, pos: SeekFrom) -> io::Result { + let (whence, offset): (libc::c_int, libc_off_t) = match pos { + SeekFrom::Start(pos) => ( + libc::SEEK_SET, + pos.try_into() + .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?, + ), + SeekFrom::End(offset) => (libc::SEEK_END, offset), + SeekFrom::Current(offset) => (libc::SEEK_CUR, offset), + }; + let offset = negone_err(libc_lseek(fd as libc::c_int, offset, whence))?; + Ok(offset.try_into().unwrap()) +} + +/// `lseek(fd, 0, SEEK_CUR)` +#[inline] +pub fn tell(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _tell(fd) } +} + +unsafe fn _tell(fd: RawFd) -> io::Result { + let offset = negone_err(libc_lseek(fd as libc::c_int, 0, libc::SEEK_CUR))?; + Ok(offset.try_into().unwrap()) +} + +/// `fchmod(fd)`. +/// +/// Note that this implementation does not support `O_PATH` file descriptors, +/// even on platforms where the host libc emulates it. +#[cfg(not(target_os = "wasi"))] +#[inline] +pub fn fchmod(fd: &Fd, mode: Mode) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _fchmod(fd, mode) } +} + +#[cfg(not(any(target_os = "linux", target_os = "wasi")))] +unsafe fn _fchmod(fd: RawFd, mode: Mode) -> io::Result<()> { + zero_ok(libc::fchmod(fd as libc::c_int, mode.bits())) +} + +#[cfg(target_os = "linux")] +unsafe fn _fchmod(fd: RawFd, mode: Mode) -> io::Result<()> { + // Use `libc::syscall` rather than `libc::fchmod` because some libc + // implementations, such as musl, add extra logic to `fchmod` to emulate + // support for `O_PATH`, which uses `/proc` outside our control and + // interferes with our own use of `O_PATH`. + zero_ok(libc::syscall( + libc::SYS_fchmod, + fd as libc::c_int, + mode.bits(), + )) +} + +/// `fstatfs(fd)` +#[cfg(not(any(target_os = "netbsd", target_os = "wasi")))] // not implemented in libc for netbsd yet +#[inline] +pub fn fstatfs(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _fstatfs(fd) } +} + +#[cfg(not(any(target_os = "netbsd", target_os = "wasi")))] // not implemented in libc for netbsd yet +unsafe fn _fstatfs(fd: RawFd) -> io::Result { + let mut statfs = MaybeUninit::::uninit(); + zero_ok(libc_fstatfs(fd as libc::c_int, statfs.as_mut_ptr()))?; + Ok(statfs.assume_init()) +} + +/// `futimens(fd, times)` +#[inline] +pub fn futimens(fd: &Fd, times: &[libc::timespec; 2]) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _futimens(fd, times) } +} + +unsafe fn _futimens(fd: RawFd, times: &[libc::timespec; 2]) -> io::Result<()> { + zero_ok(libc::futimens(fd as libc::c_int, times.as_ptr())) +} diff --git a/src/fs/file_type.rs b/src/fs/file_type.rs new file mode 100644 index 00000000..be0d3717 --- /dev/null +++ b/src/fs/file_type.rs @@ -0,0 +1,61 @@ +/// `S_IF*` constants. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FileType { + /// `S_IFREG` + RegularFile, + + /// `S_IFDIR` + Directory, + + /// `S_IFLNK` + Symlink, + + /// `S_IFIFO` + Fifo, + + /// `S_IFSOCK` + Socket, + + /// `S_IFCHR` + CharacterDevice, + + /// `S_IFBLK` + BlockDevice, + + /// An unknown filesystem object. + Unknown, +} + +impl FileType { + /// Construct a `FileType` from the `st_mode` field of a `LibcStat`. + pub fn from_libc_stat_st_mode(st_mode: libc::mode_t) -> Self { + match st_mode & libc::S_IFMT { + libc::S_IFREG => Self::RegularFile, + libc::S_IFDIR => Self::Directory, + libc::S_IFLNK => Self::Symlink, + #[cfg(not(target_os = "wasi"))] // TODO: Use WASI's `S_IFIFO`. + libc::S_IFIFO => Self::Fifo, + #[cfg(not(target_os = "wasi"))] // TODO: Use WASI's `S_IFSOCK`. + libc::S_IFSOCK => Self::Socket, + libc::S_IFCHR => Self::CharacterDevice, + libc::S_IFBLK => Self::BlockDevice, + _ => Self::Unknown, + } + } + + /// Construct a `FileType` from the `d_type` field of a `libc::dirent`. + pub(crate) fn from_dirent_d_type(d_type: u8) -> Self { + match d_type { + libc::DT_REG => Self::RegularFile, + libc::DT_DIR => Self::Directory, + libc::DT_LNK => Self::Symlink, + #[cfg(not(target_os = "wasi"))] // TODO: Use WASI's `DT_SOCK`. + libc::DT_SOCK => Self::Socket, + #[cfg(not(target_os = "wasi"))] // TODO: Use WASI's `DT_FIFO`. + libc::DT_FIFO => Self::Fifo, + libc::DT_CHR => Self::CharacterDevice, + libc::DT_BLK => Self::BlockDevice, + /* libc::DT_UNKNOWN | */ _ => Self::Unknown, + } + } +} diff --git a/src/fs/getpath.rs b/src/fs/getpath.rs new file mode 100644 index 00000000..2972d58d --- /dev/null +++ b/src/fs/getpath.rs @@ -0,0 +1,21 @@ +use crate::zero_ok; +use std::{ + io, + os::unix::io::{AsRawFd, RawFd}, +}; + +/// `fcntl(fd, F_GETPATH)` +pub fn getpath(fd: &Fd, buf: &mut [u8]) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _getpath(fd, buf) } +} + +unsafe fn _getpath(fd: RawFd, buf: &mut [u8]) -> io::Result<()> { + // From the macOS `fcntl` man page: + // `F_GETPATH` - Get the path of the file descriptor `Fildes`. The argument + // must be a buffer of size `MAXPATHLEN` or greater. + // + // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html + assert!(buf.len() >= libc::PATH_MAX as usize); + zero_ok(libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr())) +} diff --git a/src/fs/makedev.rs b/src/fs/makedev.rs new file mode 100644 index 00000000..05b796e5 --- /dev/null +++ b/src/fs/makedev.rs @@ -0,0 +1,22 @@ +/// `makedev(maj, min)` +#[cfg(not(any(target_os = "android", target_os = "emscripten", target_os = "wasi")))] +#[inline] +pub fn makedev(maj: u32, min: u32) -> u64 { + unsafe { libc::makedev(maj, min) } +} + +/// `makedev(maj, min)` +#[cfg(target_os = "android")] +#[inline] +pub fn makedev(maj: u32, min: u32) -> u64 { + // Android's `makedev` oddly has signed argument types. + unsafe { libc::makedev(maj as i32, min as i32) } +} + +/// `makedev(maj, min)` +#[cfg(target_os = "emscripten")] +#[inline] +pub fn makedev(maj: u32, min: u32) -> u64 { + // Emscripten's `makedev` has a 32-bit return value. + u64::from(unsafe { libc::makedev(maj, min) }) +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 00000000..cd9338d2 --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,91 @@ +//! Filesystem operations. + +#[cfg(target_os = "android")] +mod android; +mod at; +mod constants; +mod dir; +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "netbsd")))] +mod fadvise; +mod fcntl; +mod fd; +mod file_type; +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod getpath; +#[cfg(not(any( + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "freebsd" +)))] +mod makedev; +mod path_arg; +#[cfg(not(any(target_os = "ios", target_os = "macos")))] +// Most Modern OS's have `preadv`/`pwritev`. +mod pv; +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod rdadvise; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +mod statx; + +pub use at::{ + accessat, linkat, mkdirat, openat, readlinkat, renameat, statat, symlinkat, unlinkat, utimensat, +}; +#[cfg(not(target_os = "wasi"))] +pub use at::{chmodat, cwd}; +pub use constants::{Access, AtFlags, FdFlags, Mode, OFlags}; +pub use dir::{Dir, Entry, SeekLoc}; +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "netbsd")))] +pub use fadvise::{fadvise, Advice}; +pub use fcntl::{getfd, getfl, setfd, setfl}; +#[cfg(not(target_os = "wasi"))] +pub use fd::fchmod; +#[cfg(not(any(target_os = "netbsd", target_os = "wasi")))] +// not implemented in libc for netbsd yet +pub use fd::fstatfs; +pub use fd::{futimens, seek, tell}; +pub use file_type::FileType; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub use getpath::getpath; +#[cfg(not(any( + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "wasi", +)))] +pub use makedev::makedev; +pub use path_arg::PathArg; +#[cfg(not(any(target_os = "ios", target_os = "macos")))] +pub use pv::{preadv, pwritev}; +#[cfg(any(target_os = "ios", target_os = "macos"))] +pub use rdadvise::rdadvise; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +pub use statx::statx; + +/// Re-export `libc::stat` (or `libc::stat64` where applicable). +#[cfg(not(any(target_os = "linux", target_os = "emscripten", target_os = "l4re")))] +pub type LibcStat = libc::stat; + +/// Re-export `libc::statfs` (or `libc::statfs64` where applicable). +#[cfg(not(any( + target_os = "linux", + target_os = "emscripten", + target_os = "l4re", + target_os = "netbsd", + target_os = "wasi", +)))] +#[allow(clippy::module_name_repetitions)] +pub type LibcStatFs = libc::statfs; + +/// Re-export `libc::stat` (or `libc::stat64` where applicable). +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] +pub type LibcStat = libc::stat64; + +/// Re-export `libc::statfs` (or `libc::statfs64` where applicable). +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "l4re"))] +pub type LibcStatFs = libc::statfs64; + +/// Re-export `libc::statx`. Only available on Linux with GLIBC for now. +#[cfg(all(target_os = "linux", target_env = "gnu"))] +pub type LibcStatx = libc::statx; diff --git a/src/fs/path_arg.rs b/src/fs/path_arg.rs new file mode 100644 index 00000000..cb519f39 --- /dev/null +++ b/src/fs/path_arg.rs @@ -0,0 +1,446 @@ +#[cfg(target_os = "hermit")] +use std::os::hermit::ext::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(target_os = "vxworks")] +use std::os::vxworks::ext::ffi::OsStrExt; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::OsStrExt; +use std::{ + borrow::Cow, + ffi::{CStr, CString, OsStr, OsString}, + io, + ops::Deref, + path::{Component, Components, Iter, Path, PathBuf}, + str, +}; + +/// A trait for passing path arguments. This is similar to +/// [`AsRef`]`<`[`Path`]`>`, but is implemented for more kinds of strings and +/// can convert into more kinds of strings. +/// +/// [`AsRef`]: https://doc.rust-lang.org/std/convert/trait.AsRef.html +/// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html +pub trait PathArg { + /// Return a view of this string as a string slice. + fn as_str(&self) -> io::Result<&str>; + + /// Return a potentially-lossy rendering of this string as a `Cow`. + fn to_string_lossy(&self) -> Cow; + + /// Return a view of this string as a maybe-owend [`CStr`]. + /// + /// [`CStr`]: https://doc.rust-lang.org/std/ffi/struct.CStr.html + #[cfg(not(windows))] + fn as_cstr(&self) -> io::Result>; + + /// Return a view of this string as a byte slice. + #[cfg(not(windows))] + fn as_utf8_bytes(&self) -> &[u8]; + + /// Return a view of this string as a maybe-owend [`OsStr`]. + /// + /// [`OsStr`]: https://doc.rust-lang.org/std/ffi/struct.OsStr.html + #[cfg(windows)] + fn as_os_str(&self) -> io::Result>; +} + +impl PathArg for &str { + #[inline] + fn as_str(&self) -> io::Result<&str> { + Ok(self) + } + + fn to_string_lossy(&self) -> Cow { + Cow::Borrowed(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for String { + #[inline] + fn as_str(&self) -> io::Result<&str> { + Ok(self) + } + + fn to_string_lossy(&self) -> Cow { + Cow::Borrowed(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for &OsStr { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + OsStr::to_string_lossy(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_utf8_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for OsString { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_os_str().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.as_os_str().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_utf8_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for &Path { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_os_str().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + Path::to_string_lossy(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_os_str().as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_os_str().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for PathBuf { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_os_str().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.as_os_str().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_os_str().as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_os_str().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for &CStr { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.to_str().map_err(|_| utf8_error()) + } + + fn to_string_lossy(&self) -> Cow { + CStr::to_string_lossy(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Borrowed(self)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.to_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for CString { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_c_str().to_str().map_err(|_| utf8_error()) + } + + fn to_string_lossy(&self) -> Cow { + self.as_c_str().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Borrowed(self.as_c_str())) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl<'a> PathArg for Cow<'a, OsStr> { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.deref().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.deref().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.deref().as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.deref().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl<'a> PathArg for Component<'a> { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_os_str().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.as_os_str().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.as_os_str().as_bytes())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_os_str().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl<'a> PathArg for Components<'a> { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_path().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.as_path().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new( + self.as_path().as_os_str().as_bytes(), + )?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_path().as_os_str().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl<'a> PathArg for Iter<'a> { + #[inline] + fn as_str(&self) -> io::Result<&str> { + self.as_path().to_str().ok_or_else(utf8_error) + } + + fn to_string_lossy(&self) -> Cow { + self.as_path().to_string_lossy() + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new( + self.as_path().as_os_str().as_bytes(), + )?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self.as_path().as_os_str().as_bytes() + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for &[u8] { + #[inline] + fn as_str(&self) -> io::Result<&str> { + str::from_utf8(self).map_err(|_| utf8_error()) + } + + fn to_string_lossy(&self) -> Cow { + String::from_utf8_lossy(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(*self)?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +impl PathArg for Vec { + #[inline] + fn as_str(&self) -> io::Result<&str> { + str::from_utf8(self).map_err(|_| utf8_error()) + } + + fn to_string_lossy(&self) -> Cow { + String::from_utf8_lossy(self) + } + + #[cfg(not(windows))] + #[inline] + fn as_cstr(&self) -> io::Result> { + Ok(Cow::Owned(CString::new(self.clone())?)) + } + + #[cfg(not(windows))] + #[inline] + fn as_utf8_bytes(&self) -> &[u8] { + self + } + + #[cfg(windows)] + fn as_os_str(&self) -> io::Result> { + self.as_ref() + } +} + +fn utf8_error() -> io::Error { + io::Error::from_raw_os_error(libc::EILSEQ) +} diff --git a/src/fs/pv.rs b/src/fs/pv.rs new file mode 100644 index 00000000..a0ba8524 --- /dev/null +++ b/src/fs/pv.rs @@ -0,0 +1,81 @@ +//! Positioned *and* vectored I/O: `preadv` and `pwritev`. + +use crate::negone_err; +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "emscripten")))] +use libc::{preadv as libc_preadv, pwritev as libc_pwritev}; +#[cfg(any(target_os = "linux", target_os = "android", target_os = "emscripten"))] +use libc::{preadv64 as libc_preadv, pwritev64 as libc_pwritev}; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{ + cmp, + convert::TryInto, + io::{self, IoSlice, IoSliceMut}, + sync::atomic::{AtomicUsize, Ordering}, +}; + +/// `preadv(fd, bufs.as_ptr(), bufs.len(), offset)` +#[inline] +pub fn preadv(fd: &Fd, bufs: &[IoSliceMut], offset: u64) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _preadv(fd, bufs, offset) } +} + +unsafe fn _preadv(fd: RawFd, bufs: &[IoSliceMut], offset: u64) -> io::Result { + let offset = offset + .try_into() + .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?; + let nread = negone_err(libc_preadv( + fd as libc::c_int, + bufs.as_ptr() as *const libc::iovec, + cmp::min(bufs.len(), max_iov()).try_into().unwrap(), + offset, + ))?; + Ok(nread.try_into().unwrap()) +} + +/// `pwritev(fd, bufs.as_ptr(), bufs.len(), offset)` +#[inline] +pub fn pwritev(fd: &Fd, bufs: &[IoSlice], offset: u64) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _pwritev(fd, bufs, offset) } +} + +unsafe fn _pwritev(fd: RawFd, bufs: &[IoSlice], offset: u64) -> io::Result { + let offset = offset + .try_into() + .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?; + let nwritten = negone_err(libc_pwritev( + fd as libc::c_int, + bufs.as_ptr() as *const libc::iovec, + cmp::min(bufs.len(), max_iov()).try_into().unwrap(), + offset, + ))?; + Ok(nwritten.try_into().unwrap()) +} + +// These functions are derived from Rust's library/std/src/sys/unix/fd.rs at +// revision 108e90ca78f052c0c1c49c42a22c85620be19712. + +#[cfg(not(any(target_os = "redox", target_env = "newlib")))] +fn max_iov() -> usize { + static LIM: AtomicUsize = AtomicUsize::new(0); + + let mut lim = LIM.load(Ordering::Relaxed); + if lim == 0 { + let ret = unsafe { libc::sysconf(libc::_SC_IOV_MAX) }; + + // 16 is the minimum value required by POSIX. + lim = if ret > 0 { ret as usize } else { 16 }; + LIM.store(lim, Ordering::Relaxed); + } + + lim +} + +#[cfg(any(target_os = "redox", target_env = "newlib"))] +fn max_iov() -> usize { + 16 // The minimum value required by POSIX. +} diff --git a/src/fs/rdadvise.rs b/src/fs/rdadvise.rs new file mode 100644 index 00000000..a2dbeb13 --- /dev/null +++ b/src/fs/rdadvise.rs @@ -0,0 +1,46 @@ +use crate::zero_ok; +use std::{ + convert::TryInto, + io, + os::unix::io::{AsRawFd, RawFd}, +}; + +/// `fcntl(fd, F_RDADVISE, radvisory { offset, len })` +pub fn rdadvise(fd: &Fd, offset: u64, len: u64) -> io::Result<()> { + let fd = fd.as_raw_fd(); + unsafe { _rdadvise(fd, offset, len) } +} + +unsafe fn _rdadvise(fd: RawFd, offset: u64, len: u64) -> io::Result<()> { + // From the macOS `fcntl` man page: + // `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 */ + // }; + // ``` + // + // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html + let ra_offset = match offset.try_into() { + Ok(len) => len, + // If this conversion fails, the user is providing an offset outside + // any possible file extent, so just ignore it. + Err(_) => return Ok(()), + }; + let ra_count = match len.try_into() { + Ok(len) => len, + // If this conversion fails, the user is providing a dubiously large + // hint which is unlikely to improve performance. + Err(_) => return Ok(()), + }; + let radvisory = libc::radvisory { + ra_offset, + ra_count, + }; + zero_ok(libc::fcntl(fd, libc::F_RDADVISE, &radvisory)) +} diff --git a/src/fs/statx.rs b/src/fs/statx.rs new file mode 100644 index 00000000..5ae73f45 --- /dev/null +++ b/src/fs/statx.rs @@ -0,0 +1,116 @@ +//! Linux `statx`. + +use crate::{ + fs::{AtFlags, LibcStatx, PathArg}, + zero_ok, +}; +use bitflags::bitflags; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{ffi::CStr, io, mem::MaybeUninit}; + +bitflags! { + /// `STATX_*` constants. + pub struct StatxFlags: u32 { + /// `STATX_TYPE` + const TYPE = libc::STATX_TYPE; + + /// `STATX_MODE` + const MODE = libc::STATX_MODE; + + /// `STATX_NLINK` + const NLINK = libc::STATX_NLINK; + + /// `STATX_UID` + const UID = libc::STATX_UID; + + /// `STATX_GID` + const GID = libc::STATX_GID; + + /// `STATX_ATIME` + const ATIME = libc::STATX_ATIME; + + /// `STATX_MTIME` + const MTIME = libc::STATX_MTIME; + + /// `STATX_CTIME` + const CTIME = libc::STATX_CTIME; + + /// `STATX_INO` + const INO = libc::STATX_INO; + + /// `STATX_SIZE` + const SIZE = libc::STATX_SIZE; + + /// `STATX_BLOCKS` + const BLOCKS = libc::STATX_BLOCKS; + + /// `STATX_BASIC_STATS` + const BASIC_STATS = libc::STATX_BASIC_STATS; + + /// `STATX_BTIME` + const BTIME = libc::STATX_BTIME; + + /// `STATX_ALL` + const ALL = libc::STATX_ALL; + } +} + +/// `statx(dirfd, path, flags, mask, statxbuf)`. Note that this isn't available +/// on older Linux; returns `ENOSYS` in that case. +#[inline] +pub fn statx( + dirfd: &Fd, + path: P, + flags: AtFlags, + mask: StatxFlags, +) -> io::Result { + let dirfd = dirfd.as_raw_fd(); + let path = path.as_cstr()?; + unsafe { _statx(dirfd, &path, flags, mask) } +} + +macro_rules! syscall { + (fn $name:ident($($arg_name:ident: $t:ty),*) -> $ret:ty) => ( + unsafe fn $name($($arg_name: $t),*) -> $ret { + weak! { fn $name($($t),*) -> $ret } + + if let Some(fun) = $name.get() { + fun($($arg_name),*) + } else { + errno::set_errno(errno::Errno(libc::ENOSYS)); + -1 + } + } + ) +} + +#[allow(non_upper_case_globals)] +unsafe fn _statx( + dirfd: RawFd, + path: &CStr, + flags: AtFlags, + mask: StatxFlags, +) -> io::Result { + syscall! { + fn statx( + dirfd: libc::c_int, + path: *const libc::c_char, + flags: libc::c_int, + mask: libc::c_uint, + buf: *mut LibcStatx + ) -> libc::c_int + } + + let mut statx_buf = MaybeUninit::::uninit(); + zero_ok(statx( + dirfd as libc::c_int, + path.as_ptr(), + flags.bits(), + mask.bits(), + statx_buf.as_mut_ptr(), + ))?; + Ok(statx_buf.assume_init()) +} diff --git a/src/io/fd.rs b/src/io/fd.rs new file mode 100644 index 00000000..69dd0862 --- /dev/null +++ b/src/io/fd.rs @@ -0,0 +1,78 @@ +//! Functions which operate on file descriptors. + +use crate::zero_ok; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{convert::TryInto, io, mem::MaybeUninit}; + +/// `ioctl(fd, FIONREAD)`. +#[inline] +pub fn fionread(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _fionread(fd) } +} + +unsafe fn _fionread(fd: RawFd) -> io::Result { + let mut nread = MaybeUninit::::uninit(); + zero_ok(libc::ioctl( + fd as libc::c_int, + libc::FIONREAD, + nread.as_mut_ptr(), + ))?; + Ok(nread.assume_init().try_into().unwrap()) +} + +/// `isatty(fd)` +#[inline] +pub fn isatty(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _isatty(fd) } +} + +unsafe fn _isatty(fd: RawFd) -> io::Result { + let res = libc::isatty(fd as libc::c_int); + if res == 0 { + let err = io::Error::last_os_error(); + match err.raw_os_error() { + #[cfg(not(target_os = "linux"))] + Some(libc::ENOTTY) => Ok(false), + + // Old Linux versions reportedly return `EINVAL`. + // https://man7.org/linux/man-pages/man3/isatty.3.html#ERRORS + #[cfg(target_os = "linux")] + Some(libc::ENOTTY) | Some(libc::EINVAL) => Ok(false), + + _ => Err(err), + } + } else { + Ok(true) + } +} + +/// `fcntl(fd, F_GETFL) & O_ACCMODE`. Returns a pair of booleans indicating +/// whether the file descriptor is readable and/or writeable, respectively. +pub fn is_read_write(fd: &Fd) -> io::Result<(bool, bool)> { + let mode = crate::fs::getfl(fd)?; + + // Check for `O_PATH`. + #[cfg(any( + target_os = "android", + target_os = "fuchsia", + target_os = "linux", + target_os = "emscripten" + ))] + if mode.contains(crate::fs::OFlags::PATH) { + return Ok((false, false)); + } + + // Use `RWMODE` rather than `ACCMODE` as `ACCMODE` may include `O_PATH`. + // We handled `O_PATH` above. + match mode & crate::fs::OFlags::RWMODE { + crate::fs::OFlags::RDONLY => Ok((true, false)), + crate::fs::OFlags::RDWR => Ok((true, true)), + crate::fs::OFlags::WRONLY => Ok((false, true)), + _ => unreachable!(), + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs new file mode 100644 index 00000000..9266129e --- /dev/null +++ b/src/io/mod.rs @@ -0,0 +1,7 @@ +//! I/O operations. + +mod fd; +mod poll; + +pub use fd::{fionread, is_read_write, isatty}; +pub use poll::{PollFd, PollFdVec}; diff --git a/src/io/poll.rs b/src/io/poll.rs new file mode 100644 index 00000000..ca78a720 --- /dev/null +++ b/src/io/poll.rs @@ -0,0 +1,74 @@ +use crate::negone_err; +use bitflags::bitflags; +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; +#[cfg(target_os = "wasi")] +use std::os::wasi::io::{AsRawFd, RawFd}; +use std::{convert::TryInto, io}; + +bitflags! { + pub struct PollFlags: libc::c_short { + const POLLIN = libc::POLLIN; + const POLLPRI = libc::POLLPRI; + const POLLOUT = libc::POLLOUT; + const POLLRDNORM = libc::POLLRDNORM; + const POLLWRNORM = libc::POLLWRNORM; + const POLLRDBAND = libc::POLLRDBAND; + const POLLWRBAND = libc::POLLWRBAND; + const POLLERR = libc::POLLERR; + const POLLHUP = libc::POLLHUP; + const POLLNVAL = libc::POLLNVAL; + } +} + +/// `pollfd` +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[repr(C)] +pub struct PollFd(libc::pollfd); + +impl PollFd { + /// # Safety + /// + /// `PollFd` does not take ownership of the file descriptors passed in here, + /// and they need to live at least through the `PollFdVec::poll` call. + pub unsafe fn new(fd: &Fd, events: PollFlags) -> Self { + let fd = fd.as_raw_fd(); + Self::_new(fd, events) + } + + unsafe fn _new(fd: RawFd, events: PollFlags) -> Self { + Self(libc::pollfd { + fd: fd as libc::c_int, + events: events.bits(), + revents: PollFlags::empty().bits(), + }) + } + + /// Return the ready events. + pub fn revents(self) -> PollFlags { + PollFlags::from_bits(self.0.revents).unwrap() + } +} + +/// A [`Vec`] of `libc::pollfd`. +/// +/// [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct PollFdVec { + fds: Vec, +} + +impl PollFdVec { + /// `poll(self.fds.as_mut_ptr(), self.fds.len(), timeout)` + pub fn poll(&mut self, timeout: libc::c_int) -> io::Result { + let nfds = self + .fds + .len() + .try_into() + .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; + + let nready = negone_err(unsafe { libc::poll(self.fds.as_mut_ptr(), nfds, timeout) })?; + + Ok(nready.try_into().unwrap()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..66f23f18 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,103 @@ +//! `posish` provides safe wrappers to `libc` functions. The current focus is +//! on the functionality needed by [`cap-std`] and [`system-interface`] that +//! isn't provided by [`std`] or [`getrandom`]. +//! +//! The wrappers perform the following tasks: +//! - Error values are translated to `Result`s. +//! - Out-parameters are translated to return values. +//! - Path arguments can by any kind of string type. +//! - File descriptors are passed in through arguments implementing +//! [`AsRawFd`] instead of as bare integers and returned as +//! [`std::fs::File`]s. +//! - Constants use `enum`s and [`bitflags`] types. +//! - Multiplexed functions (eg. `fcntl`, `ioctl`, etc.) are de-multiplexed. +//! - Variadic functions (eg. `openat`, etc.) are presented as non-variadic. +//! - Functions and types which need `64` suffixes to enable large-file +//! support are used automatically. +//! - Behaviors that depend on the sizes of C types like `long` are hidden. +//! - File offsets and sizes are presented as `i64` and `u64` rather than +//! `off_t`. +//! - In some places, more human-friendly and less historical-accident names +//! are used. +//! +//! Things they don't do include: +//! - Emulating functions that aren't natively supported on a platform. +//! - Detecting whether functions are supported at runtime. +//! - Hiding significant differences between platforms. +//! - Hiding ambient authorities. +//! - Imposing sandboxing features such as filesystem path or network address +//! sandboxing. +//! +//! See [`cap-std`] and [`system-interface`] for libraries which do hide +//! ambient authorities and perform sandboxing. +//! +//! # Safety +//! +//! This library follows [`std`] in considering dynamic integer values that +//! have no meaning outside of OS APIs to be similar to raw pointers, from a +//! safety perspective. For example, +//! [`std::os::unix::io::FromRawFd::from_raw_fd`] is unsafe since it takes a +//! raw file descriptor. In this library, raw file descriptors and raw +//! directory seek locations are considered to be similar to raw pointers in +//! terms of safety. +//! +//! [`cap-std`]: https://crates.io/crates/cap-std +//! [`system-interface`]: https://crates.io/crates/system-interface +//! [`std`]: https://doc.rust-lang.org/std/ +//! [`getrandom`]: https://crates.io/crates/getrandom +//! [`AsRawFd`]: https://doc.rust-lang.org/std/os/unix/io/trait.AsRawFd.html +//! [`std::fs::File`]: https://doc.rust-lang.org/std/fs/struct.File.html +//! [`bitflags`]: https://crates.io/crates/bitflags +//! [`std::os::unix::io::FromRawFd::from_raw_fd`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.from_raw_fd + +#![deny(missing_docs)] +#![cfg_attr(target_os = "wasi", feature(wasi_ext))] + +#[macro_use] +mod weak; + +pub mod fs; +pub mod io; +#[cfg(not(target_os = "wasi"))] // WASI doesn't support `net` yet. +pub mod net; +pub mod process; +pub mod time; + +/// Given a `libc` return value, translate `0` into `Ok(())` and any other value +/// to an `Err` with the error from `errno`. +fn zero_ok(t: T) -> std::io::Result<()> { + if t.is_zero() { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } +} + +/// Given a `libc` return value, translate `-1` into an `Err` with the error +/// from `errno`, and any other value to an `Ok` containing the value. +fn negone_err(t: T) -> std::io::Result { + if t.is_negone() { + Err(std::io::Error::last_os_error()) + } else { + Ok(t) + } +} + +trait LibcResult { + fn is_zero(&self) -> bool; + fn is_negone(&self) -> bool; +} + +macro_rules! is_impls { + ($($t:ident)*) => ($(impl LibcResult for $t { + fn is_zero(&self) -> bool { + *self == 0 + } + + fn is_negone(&self) -> bool { + *self == -1 + } + })*) +} + +is_impls! { i32 i64 isize } diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 00000000..01ba7c5b --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,5 @@ +//! Network-related operations. + +mod socket; + +pub use socket::{socket_type, SocketType}; diff --git a/src/net/socket.rs b/src/net/socket.rs new file mode 100644 index 00000000..9c80a0c9 --- /dev/null +++ b/src/net/socket.rs @@ -0,0 +1,50 @@ +use crate::zero_ok; +use std::{ + io, + mem::{size_of, MaybeUninit}, + os::unix::io::{AsRawFd, RawFd}, +}; + +/// `SOCK_*` constants. +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub enum SocketType { + /// `SOCK_STREAM`. + Stream = libc::SOCK_STREAM, + + /// `SOCK_DGRAM`. + Datagram = libc::SOCK_DGRAM, + + /// `SOCK_SEQPACKET`. + SeqPacket = libc::SOCK_SEQPACKET, + + /// `SOCK_RAW`. + Raw = libc::SOCK_RAW, + + /// `SOCK_RDM`. + Rdm = libc::SOCK_RDM, +} + +/// `getsockopt(fd, SOL_SOCKET, SO_TYPE)` +pub fn socket_type(fd: &Fd) -> io::Result { + let fd = fd.as_raw_fd(); + unsafe { _socket_type(fd) } +} + +unsafe fn _socket_type(fd: RawFd) -> io::Result { + let mut buffer = MaybeUninit::::uninit(); + let mut out_len = size_of::() as libc::socklen_t; + zero_ok(libc::getsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_TYPE, + buffer.as_mut_ptr() as *mut libc::c_void, + &mut out_len, + ))?; + assert_eq!( + out_len as usize, + size_of::(), + "unexpected SocketType size" + ); + Ok(buffer.assume_init()) +} diff --git a/src/process/id.rs b/src/process/id.rs new file mode 100644 index 00000000..24c7deea --- /dev/null +++ b/src/process/id.rs @@ -0,0 +1,18 @@ +/// `getuid()` +#[inline] +pub fn getuid() -> u32 { + unsafe { libc::getuid() } +} + +/// `getgid()` +#[inline] +pub fn getgid() -> u32 { + unsafe { libc::getgid() } +} + +/// `getpid()` +#[inline] +pub fn getpid() -> u32 { + let pid: i32 = unsafe { libc::getpid() }; + pid as u32 +} diff --git a/src/process/mod.rs b/src/process/mod.rs new file mode 100644 index 00000000..2a88b005 --- /dev/null +++ b/src/process/mod.rs @@ -0,0 +1,9 @@ +//! Process-associated operations. + +#[cfg(not(target_os = "wasi"))] // WASI doesn't have get[gpu]id. +mod id; +mod sched; + +#[cfg(not(target_os = "wasi"))] +pub use id::{getgid, getpid, getuid}; +pub use sched::sched_yield; diff --git a/src/process/sched.rs b/src/process/sched.rs new file mode 100644 index 00000000..c43dba9d --- /dev/null +++ b/src/process/sched.rs @@ -0,0 +1,7 @@ +use crate::zero_ok; + +/// `sched_yield()` +#[inline] +pub fn sched_yield() { + zero_ok(unsafe { libc::sched_yield() }).unwrap() +} diff --git a/src/time/clock.rs b/src/time/clock.rs new file mode 100644 index 00000000..f778229f --- /dev/null +++ b/src/time/clock.rs @@ -0,0 +1,56 @@ +use crate::zero_ok; +use std::mem::MaybeUninit; + +pub use libc::{timespec, UTIME_NOW, UTIME_OMIT}; + +/// `clockid_t` +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(i32)] +pub enum ClockId { + /// `CLOCK_REALTIME` + Realtime = libc::CLOCK_REALTIME, + + /// `CLOCK_MONOTONIC` + Monotonic = libc::CLOCK_MONOTONIC, + + /// `CLOCK_PROCESS_CPUTIME_ID` + #[cfg(not(target_os = "netbsd"))] + ProcessCPUTime = libc::CLOCK_PROCESS_CPUTIME_ID, + + /// `CLOCK_THREAD_CPUTIME_ID` + #[cfg(not(target_os = "netbsd"))] + ThreadCPUTime = libc::CLOCK_THREAD_CPUTIME_ID, +} + +/// `clockid_t` +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(u32)] +pub enum ClockId { + /// `CLOCK_REALTIME` + Realtime = libc::CLOCK_REALTIME, + + /// `CLOCK_MONOTONIC` + Monotonic = libc::CLOCK_MONOTONIC, + + /// `CLOCK_PROCESS_CPUTIME_ID` + ProcessCPUTime = libc::CLOCK_PROCESS_CPUTIME_ID, + + /// `CLOCK_THREAD_CPUTIME_ID` + ThreadCPUTime = libc::CLOCK_THREAD_CPUTIME_ID, +} + +/// `clock_getres(id)` +pub fn clock_getres(id: ClockId) -> timespec { + let mut timespec = MaybeUninit::::uninit(); + zero_ok(unsafe { libc::clock_getres(id as libc::clockid_t, timespec.as_mut_ptr()) }).unwrap(); + unsafe { timespec.assume_init() } +} + +/// `clock_gettime(id)` +pub fn clock_gettime(id: ClockId) -> timespec { + let mut timespec = MaybeUninit::::uninit(); + zero_ok(unsafe { libc::clock_gettime(id as libc::clockid_t, timespec.as_mut_ptr()) }).unwrap(); + unsafe { timespec.assume_init() } +} diff --git a/src/time/mod.rs b/src/time/mod.rs new file mode 100644 index 00000000..35ca960c --- /dev/null +++ b/src/time/mod.rs @@ -0,0 +1,7 @@ +//! Time-related operations. + +#[cfg(not(target_os = "wasi"))] // not implmented in libc for WASI yet +mod clock; + +#[cfg(not(target_os = "wasi"))] // not implmented in libc for WASI yet +pub use clock::{clock_getres, clock_gettime, timespec, ClockId, UTIME_NOW, UTIME_OMIT}; diff --git a/src/weak.rs b/src/weak.rs new file mode 100644 index 00000000..5aa23de0 --- /dev/null +++ b/src/weak.rs @@ -0,0 +1,111 @@ +// Implementation derived from `weak` in Rust's +// library/std/src/sys/unix/weak.rs at revision +// 108e90ca78f052c0c1c49c42a22c85620be19712. + +//! Support for "weak linkage" to symbols on Unix +//! +//! Some I/O operations we do in libstd require newer versions of OSes but we +//! need to maintain binary compatibility with older releases for now. In order +//! to use the new functionality when available we use this module for +//! detection. +//! +//! One option to use here is weak linkage, but that is unfortunately only +//! really workable on Linux. Hence, use dlsym to get the symbol value at +//! runtime. This is also done for compatibility with older versions of glibc, +//! and to avoid creating dependencies on `GLIBC_PRIVATE` symbols. It assumes that +//! we've been dynamically linked to the library the symbol comes from, but that +//! is currently always the case for things like libpthread/libc. +//! +//! A long time ago this used weak linkage for the `__pthread_get_minstack` +//! symbol, but that caused Debian to detect an unnecessarily strict versioned +//! dependency on libc6 (#23628). + +// There are a variety of `#[cfg]`s controlling which targets are involved in +// each instance of `weak!` and `syscall!`. Rather than trying to unify all of +// that, we'll just allow that some unix targets don't use this module at all. +#![allow(dead_code, unused_macros)] +#![allow(clippy::doc_markdown)] + +use std::{ + ffi::CStr, + marker, mem, + sync::atomic::{AtomicUsize, Ordering}, +}; + +macro_rules! weak { + (fn $name:ident($($t:ty),*) -> $ret:ty) => ( + static $name: crate::weak::Weak $ret> = + crate::weak::Weak::new(concat!(stringify!($name), '\0')); + ) +} + +pub(crate) struct Weak { + name: &'static str, + addr: AtomicUsize, + _marker: marker::PhantomData, +} + +impl Weak { + pub(crate) const fn new(name: &'static str) -> Self { + Self { + name, + addr: AtomicUsize::new(1), + _marker: marker::PhantomData, + } + } + + pub(crate) fn get(&self) -> Option { + assert_eq!(mem::size_of::(), mem::size_of::()); + unsafe { + if self.addr.load(Ordering::SeqCst) == 1 { + self.addr.store(fetch(self.name), Ordering::SeqCst); + } + match self.addr.load(Ordering::SeqCst) { + 0 => None, + addr => Some(mem::transmute_copy::(&addr)), + } + } + } +} + +unsafe fn fetch(name: &str) -> usize { + let name = match CStr::from_bytes_with_nul(name.as_bytes()) { + Ok(cstr) => cstr, + Err(..) => return 0, + }; + libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr()) as usize +} + +#[cfg(not(target_os = "linux"))] +macro_rules! syscall { + (fn $name:ident($($arg_name:ident: $t:ty),*) -> $ret:ty) => ( + unsafe fn $name($($arg_name: $t),*) -> $ret { + use super::os; + + weak! { fn $name($($t),*) -> $ret } + + if let Some(fun) = $name.get() { + fun($($arg_name),*) + } else { + os::set_errno(libc::ENOSYS); + -1 + } + } + ) +} + +#[cfg(target_os = "linux")] +macro_rules! syscall { + (fn $name:ident($($arg_name:ident: $t:ty),*) -> $ret:ty) => ( + unsafe fn $name($($arg_name:$t),*) -> $ret { + // This looks like a hack, but concat_idents only accepts idents + // (not paths). + use libc::*; + + syscall( + concat_idents!(SYS_, $name), + $($arg_name as c_long),* + ) as $ret + } + ) +}