fs_at-0.1.10/.cargo_vcs_info.json0000644000000001360000000000100122070ustar { "git": { "sha1": "c82030df79768beaf080e1dbbdb0046414283a11" }, "path_in_vcs": "" }fs_at-0.1.10/.github/CODEOWNERS000064400000000000000000000000151046102023000137260ustar 00000000000000* @rbtcollinsfs_at-0.1.10/.github/renovate.json000064400000000000000000000013561046102023000150620ustar 00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ], "git-submodules": { "enabled": true }, "labels": [ "dependencies" ], "prCreation": "immediate", "stabilityDays": 3, "lockFileMaintenance": { "automerge": true, "enabled": true }, "packageRules": [ { "matchUpdateTypes": [ "minor", "patch" ], "matchCurrentVersion": "!/^0/", "automerge": true }, { "matchUpdateTypes": [ "patch" ], "matchCurrentVersion": "/^0\\./", "automerge": true } ] }fs_at-0.1.10/.github/workflows/publish.yaml000064400000000000000000000021211046102023000167220ustar 00000000000000name: Publish to Cargo on: push: branches: [main, master] jobs: publish: runs-on: ubuntu-latest name: "publish" # Reference your environment variables environment: cargo steps: - name: checkout uses: actions/checkout@master # Use caching to speed up your build - name: Cache publish-action bin id: cache-publish-action uses: actions/cache@v3 env: cache-name: cache-publish-action with: path: ~/.cargo key: ${{ runner.os }}-build-${{ env.cache-name }}-v0.1.13 # install publish-action by cargo in github action - name: Install publish-action if: steps.cache-publish-action.outputs.cache-hit != 'true' run: cargo install publish-action --version=0.1.13 - name: Publish to cargo run: publish-action env: # This can help you tagging the github repository GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This can help you publish to crates.io CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}fs_at-0.1.10/.github/workflows/rust.yml000064400000000000000000000174161046102023000161250ustar 00000000000000name: Rust permissions: contents: read # to fetch code (actions/checkout) on: # test renovate branches even without a PR push: branches: - main - renovate/* pull_request: branches: [main] jobs: build-docker: runs-on: ubuntu-latest continue-on-error: ${{ matrix.channel == 'nightly' }} strategy: fail-fast: true matrix: channel: ["1.63", stable, nightly] target: [x86_64-unknown-freebsd] include: - target: x86_64-unknown-freebsd run_tests: NO env: RUST_BACKTRACE: 1 CROSS_CONFIG: .github/cross.toml steps: - uses: actions/checkout@v3 - name: Set environment variables appropriately for the build run: | echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: setup cache uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" - name: Install Rustup run: | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=none --profile=minimal -y - name: Install cross run: | cargo install cross --git https://github.com/cross-rs/cross - name: Build within docker run: | cross build --all-targets --target ${{matrix.target}} - name: Test within docker if: ${{matrix.run_tests == 'YES'}} run: | cross test --target ${{matrix.target}} build-unix: runs-on: ${{ matrix.os }}-latest continue-on-error: ${{ matrix.channel == 'nightly' }} strategy: fail-fast: true matrix: channel: ["1.63", stable, nightly] os: [ubuntu, macos] steps: - name: Checkout code uses: actions/checkout@v3 - name: rustup default run: rustup default ${{ matrix.channel }} - name: rustup components run: rustup component add clippy - name: setup cache uses: Swatinem/rust-cache@v2 with: # The prefix cache key, this can be changed to start a new cache manually. # default: "v0-rust" # prefix-key: "" # A cache key that is used instead of the automatic `job`-based key, # and is stable over multiple jobs. # default: empty # shared-key: "" # An additional cache key that is added alongside the automatic `job`-based # cache key and can be used to further differentiate jobs. # default: empty # key: "" # A whitespace separated list of env-var *prefixes* who's value contributes # to the environment cache key. # The env-vars are matched by *prefix*, so the default `RUST` var will # match all of `RUSTC`, `RUSTUP_*`, `RUSTFLAGS`, `RUSTDOC_*`, etc. # default: "CARGO CC CFLAGS CXX CMAKE RUST" # env-vars: "" # The cargo workspaces and target directory configuration. # These entries are separated by newlines and have the form # `$workspace -> $target`. The `$target` part is treated as a directory # relative to the `$workspace` and defaults to "target" if not explicitly given. # default: ". -> target" # workspaces: "" # Additional non workspace directories to be cached, separated by newlines. # cache-directories: "" # Determines whether workspace `target` directories are cached. # If `false`, only the cargo registry will be cached. # default: "true" # cache-targets: "" # Determines if the cache should be saved even when the workflow has failed. # default: "false" cache-on-failure: "true" # Determiners whether the cache should be saved. # If `false`, the cache is only restored. # Useful for jobs where the matrix is additive e.g. additional Cargo features. # default: "true" # save-if: "" - name: build run: cargo build --verbose --all-targets - name: test run: cargo test - name: Lint # unknown-lints permits fixing lints on nightly without breaking stable run: cargo clippy --all-targets --no-deps -- -D warnings -A unknown-lints build-windows: runs-on: windows-latest continue-on-error: ${{ matrix.channel == 'nightly' }} strategy: fail-fast: true matrix: arch: [i686, x86_64, aarch64] # "gnu" on windows isn't particularly interesting/different but very slow in # CI. Skip unless/until we have a bug report where it matters. variant: [msvc] channel: ["1.63", stable, nightly] exclude: - arch: aarch64 variant: gnu - arch: i686 variant: gnu steps: - name: Checkout code uses: actions/checkout@v3 - name: install msys2 run: choco install msys2 if: matrix.variant == 'gnu' - name: rustup default run: rustup default ${{ matrix.channel }} - name: add target run: rustup target add ${{ matrix.arch }}-pc-windows-${{ matrix.variant }} - name: rustup components run: rustup component add clippy - name: setup cache uses: Swatinem/rust-cache@v2 with: # The prefix cache key, this can be changed to start a new cache manually. # default: "v0-rust" # prefix-key: "" # A cache key that is used instead of the automatic `job`-based key, # and is stable over multiple jobs. # default: empty # shared-key: "" # An additional cache key that is added alongside the automatic `job`-based # cache key and can be used to further differentiate jobs. # default: empty # key: "" # A whitespace separated list of env-var *prefixes* who's value contributes # to the environment cache key. # The env-vars are matched by *prefix*, so the default `RUST` var will # match all of `RUSTC`, `RUSTUP_*`, `RUSTFLAGS`, `RUSTDOC_*`, etc. # default: "CARGO CC CFLAGS CXX CMAKE RUST" # env-vars: "" # The cargo workspaces and target directory configuration. # These entries are separated by newlines and have the form # `$workspace -> $target`. The `$target` part is treated as a directory # relative to the `$workspace` and defaults to "target" if not explicitly given. # default: ". -> target" # workspaces: "" # Additional non workspace directories to be cached, separated by newlines. # cache-directories: "" # Determines whether workspace `target` directories are cached. # If `false`, only the cargo registry will be cached. # default: "true" # cache-targets: "" # Determines if the cache should be saved even when the workflow has failed. # default: "false" cache-on-failure: "true" # Determiners whether the cache should be saved. # If `false`, the cache is only restored. # Useful for jobs where the matrix is additive e.g. additional Cargo features. # default: "true" # save-if: "" - name: Build run: cargo build --verbose --target ${{ matrix.arch }}-pc-windows-${{ matrix.variant }} - name: Run tests (nightly) if: (matrix.arch != 'aarch64') && (matrix.channel == 'nightly') run: cargo test --verbose --target ${{ matrix.arch }}-pc-windows-${{ matrix.variant }} - name: Run tests if: (matrix.arch != 'aarch64') && (matrix.channel != 'nightly') run: cargo test --verbose --target ${{ matrix.arch }}-pc-windows-${{ matrix.variant }} - name: Lint # unknown-lints permits fixing lints on nightly without breaking stable run: cargo clippy --all-targets --no-deps -- -D warnings -A unknown-lints fs_at-0.1.10/.gitignore000064400000000000000000000000231046102023000127620ustar 00000000000000/target Cargo.lock fs_at-0.1.10/.vscode/settings.json000064400000000000000000000001071046102023000150710ustar 00000000000000{ "rust-analyzer.linkedProjects": [ ".\\Cargo.toml" ] }fs_at-0.1.10/CODE_OF_CONDUCT.md000064400000000000000000000121551046102023000136020ustar 00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at robert.collins@cognite.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. fs_at-0.1.10/Cargo.toml0000644000000035760000000000100102200ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.63.0" name = "fs_at" version = "0.1.10" authors = ["Robert Collins "] description = "Implementation of 'at' functions for various platforms" readme = "README.md" categories = [ "filesystem", "os", ] license = "Apache-2.0" repository = "https://github.com/rbtcollins/fs_at.git" [dependencies.cfg-if] version = "1.0.0" [dependencies.cvt] version = "0.1.1" [dependencies.log] version = "0.4.17" optional = true [dev-dependencies.env_logger] version = "0.10.0" [dev-dependencies.fs-set-times] version = "0.20.0" [dev-dependencies.rayon] version = "1.6.1" [dev-dependencies.tempfile] version = "3.8.0" [dev-dependencies.test-log] version = "0.2.11" [features] default = [] log = ["dep:log"] workaround-procmon = ["dep:once_cell"] [target."cfg(not(windows))".dependencies.libc] version = "0.2.147" [target."cfg(not(windows))".dependencies.nix] version = "0.26.2" features = ["dir"] default-features = false [target."cfg(windows)".dependencies.aligned] version = "0.4.1" [target."cfg(windows)".dependencies.once_cell] version = "1.18.0" optional = true [target."cfg(windows)".dependencies.windows-sys] version = "0.48.0" features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_SystemServices", "Win32_System_WindowsProgramming", "Win32_Security", "Win32_System_Kernel", "Win32_System_IO", "Win32_System_Ioctl", ] fs_at-0.1.10/Cargo.toml.orig000064400000000000000000000026701046102023000136730ustar 00000000000000[package] authors = ["Robert Collins "] categories = ["filesystem", "os"] description = "Implementation of 'at' functions for various platforms" edition = "2021" license = "Apache-2.0" name = "fs_at" readme = "README.md" repository = "https://github.com/rbtcollins/fs_at.git" rust-version = "1.63.0" version = "0.1.10" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = [] log = ["dep:log"] "workaround-procmon" = ["dep:once_cell"] [dependencies] cfg-if = "1.0.0" cvt = "0.1.1" log = { version = "0.4.17", optional = true } [dev-dependencies] env_logger = "0.10.0" fs-set-times = "0.20.0" rayon = "1.6.1" tempfile = "3.8.0" test-log = "0.2.11" [target.'cfg(not(windows))'.dependencies] libc = "0.2.147" # Saves nontrivial unsafe and platform specific code (Darwin vs other Unixes, # MAX_PATH and more : consider it weak and something we can remove if expedient # later. nix = { version = "0.26.2", default-features = false, features = ["dir"] } [target.'cfg(windows)'.dependencies] aligned = "0.4.1" once_cell = { optional = true, version = "1.18.0" } [target.'cfg(windows)'.dependencies.windows-sys] features = [ "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_SystemServices", "Win32_System_WindowsProgramming", "Win32_Security", "Win32_System_Kernel", "Win32_System_IO", "Win32_System_Ioctl", ] version = "0.48.0" fs_at-0.1.10/README.md000064400000000000000000000070001046102023000122530ustar 00000000000000# `*_at` syscalls for Rust (*nix and Windows) The Rust standard library does not (yet) offer at-style filesystem calls as a core feature. For instance `mkdirat`. These calls are essential for writing race-free filesystem code, since otherwise the state of the filesystem path that operations are executed against can change silently, leading to TOC-TOU race conditions. For Unix these calls are readily available in the libc crate, but for Windows some more plumbing is needed. This crate provides a unified Rust-y and safe interface to these calls. Not all platforms behave identically in their underlying syscalls, and this crate doesn't abstract over fundamental differences, but it does attempt to provide consistent errors for key scenarios. As a concrete example creating a directory at the path of an existing link with follow disabled errors with AlreadyExists. On Linux this is achieved by reading back the path that was requested, as atomic mkdir isn't yet available. `mkdirat` is used so the parent directory is reliable, but the presence of a link pointing to another part of the file system cannot be precluded. On Windows this same scenario will either result in `fs_at` receiving a `NotADirectory` error from `NtCreateFile`, or the open succeeding but a race-free detection of the presence of the link is done using `DeviceIoControl`. Both cases are reported as `AlreadyExists`. The two codepaths exist because on Windows symlinks can themselves be files or directories, and the kernel type-checks some operations such as creating a directory or truncating a file at both the link target and the link source. Truncate+nofollow also varies by platform: See OpenOptions::truncate. ## MSRV policy I'll keep this compiling against older rusts as long as it is easy, but not at the expense of a lot of code golf, or past CVEs in old releases of dependencies. Currently MSRV is 1.63. If there is a lot of interest in older versions I'm open to patches. ## Usage See the crate [docs](https://docs.rs/fs_at). But in short: use `fs_at::OpenOptions`, similar to `std::fs::OpenOptions`. ## vs other crates ### openat [openat](https://docs.rs/openat) is a nice wrapper around the Unix *at facilities. It doesn't offer Windows support, and it also requires adoption of a new Dir struct which owns the fd - which adds friction for interop with the rest of std. ### cap_std [cap_std](https://docs.rs/cap-std) is a lovely rethink of many system interactions as operations on capabilities. Even more than openat, it steps away from the familiar std APIs and instead provides its own comprehensive ecosystem. Unfortunately that doesn't use the full capabilities of the underlying OS - it layers on top of Rust's own IO stack in some cases (e.g. Windows, some non-Linux), leading to TOCTOU concerns. That is obviously fixable over time - if you want a high level API that will make insecure usage hard, I think cap-std is perfect. The goal of fs_at isn't to reframe how we do IO though - but just to surface these important calls in an ergonomic way. Perhaps cap_std could layer on fs_at when it is finished. ## Contributing PR's as normal on Github. Coverage - consider grcov. ```rust export RUSTFLAGS="-Cinstrument-coverage" export LLVM_PROFILE_FILE="fs-at-%p-%m.profraw" cargo test && grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/debug/cove rage/ ``` ## Code of conduct Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. fs_at-0.1.10/src/lib.rs000064400000000000000000001430611046102023000127070ustar 00000000000000//! Extension for operations that manipulate the file system relative to an open //! directory, rather than the global namespace. //! //! NB: If a missing capability or platform is found, I will happily add / //! accept patches : features are being added as needed, rather than //! speculatively. //! //! The Rust standard library does not (yet) offer at-style filesystem calls as //! a core feature. For instance `mkdirat`. These calls are essential for //! writing race-free filesystem code, since otherwise the state of the //! filesystem path that operations are executed against can change silently, //! leading to TOC-TOU race conditions. For Unix these calls are readily //! available in the libc crate, but for Windows some more plumbing is needed. //! This crate provides a unified Rust-y interface to these calls. //! //! Not all platforms behave identically in their underlying syscalls, and this //! crate doesn't abstract over fundamental differences, but it does attempt to //! provide consistent errors for key scenarios. As a concrete example creating //! a directory at the path of an existing link with follow disabled errors with //! AlreadyExists. In general platform documentation should be consulted to //! understand the underlying behaviour. //! //! On Linux this is achieved by reading back the path that was requested, as //! atomic mkdir isn't yet available. `mkdirat` is used so the parent directory //! is reliable, but the presence of a link pointing to another part of the file //! system cannot be precluded. //! //! On Windows this same scenario will either result in `fs_at` receiving a //! `NotADirectory` error from `NtCreateFile`, or the open succeeding but a //! race-free detection of the presence of the link is done using //! `DeviceIoControl`. Both cases are reported as `AlreadyExists`. The two //! codepaths exist because on Windows symlinks can themselves be files or //! directories, and the kernel type-checks some operations such as creating a //! directory or truncating a file at both the link target and the link source. //! //! Truncate+nofollow also varies by platform: See OpenOptions::truncate. //! //! //! Caveats: //! - On windows, procmon will cause the symlink resolution check to receive an //! incorrect error code. Enabling the workaround-procmon feature and setting //! FS_AT_WORKAROUND_PROCMON will treat ACCESS_DENIED as //! ERROR_NOT_REPARSE_POINT. //! https://twitter.com/rbtcollins/status/1617211985384407044 //! //! Feature flags: //! - workaround-procmon: enables the FS_AT_WORKAROUND_PROCMON environment //! variable. //! - log: enables trace log messages for debugging use std::{ ffi::OsStr, fs::File, io::{Error, ErrorKind, Result}, path::Path, }; cfg_if::cfg_if! { if #[cfg(windows)] { mod win; use win::{OpenOptionsImpl, ReadDirImpl, DirEntryImpl}; } else { mod unix; use unix::{OpenOptionsImpl, ReadDirImpl, DirEntryImpl}; } } /// Similar to [`std::fs::OpenOptions`], this struct is used to parameterise the /// various at functions, which are then called on the struct itself. Typical /// use is to create a struct via [`Default::default`] or /// [`OpenOptions::default()`], and then customise it as desired (e.g. setting /// security descriptors on windows, or mode on unix) using an appropriate /// platform specific trait, finishing up with the desired manipulation e.g. /// `mkdirat`. /// /// A note on the manipulations: they take a directory handle as &File. This is /// believed safe but if you have reason to disagree please file a bug. /// /// - Rust's borrow checker ensures that File::drop() will not be called /// concurrently with a manipulation, thus the file will still be open (in the /// absence of unsafe Rust or non-Rust libraries) /// - the openat family of functions do not document any state changes to the /// base fd that names are resolved against. Only `read_dir` is documented as /// changing state. /// - similarly on Windows, NtCreateFile is not documented as changing any state /// when creating a file relative to the handle. #[derive(Default, Debug)] #[non_exhaustive] pub struct OpenOptions { _impl: OpenOptionsImpl, } /// Controls the way writes to an opened file are performed. Write modes do not /// affect how the file is opened - creating the file or truncating it require /// separate options. #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, PartialOrd)] #[non_exhaustive] pub enum OpenOptionsWriteMode { /// No writing permitted. Allows opening files where the process lacks write permissions, and attempts to write will fail. #[default] None, /// Writes permitted. The file location pointer tracked by the OS determines /// where writes in the file will take place. Write, /// Writes permitted. The OS will place each write at the current end of the /// file. These may still change the file location pointer, so if reads are /// being used as well, be sure to seek to the desired location before /// reading. One way to do this is to use seek to save the file location /// pointer (`seek(SeekFrom::Current(0))`) and then apply the result before /// the next read. /// /// Most OSes and filesystems make these writes atomically, such that /// different threads or even processes can collaborate safely on a single /// file, as long as each write call provides a full unit of data (e.g. a /// line, or a binary struct etc). This can be done by building up the data /// to write, or using a buffered writer that is large enough and calling /// flush after each unit is complete. /// /// In particular NFS on Linux is documented as not providing atomic appends. /// /// ```no_compile /// use std::fs::OpenOptions; /// /// let file = OpenOptions::new().write(OpenOptionsWriteMode::Append).open_at(&mut parent, "foo.txt"); /// ``` Append, } impl OpenOptions { /// Sets the option for read access. /// /// This option, when true, will indicate that the file should be read-able if opened. /// /// ```no_compile /// use fs_at::OpenOptions; /// /// let file = OpenOptions::default().read(true).open_at(&mut parent, "foo"); /// ``` pub fn read(&mut self, read: bool) -> &mut Self { self._impl.read(read); self } /// Sets the option for write access. /// /// See [`OpenOptionsWriteMode`] for the details of each mode. /// /// This option on its own is not enough to create a new file. /// /// ```no_compile /// use fs_at::OpenOptions; /// /// let file = OpenOptions::default().write(OpenOptionsWriteMode::Write).open_at(&mut parent, "foo.txt"); /// ``` pub fn write(&mut self, write: OpenOptionsWriteMode) -> &mut Self { self._impl.write(write); self } /// Sets the option for truncating a previous file. /// /// If a file is successfully opened with this option set it will truncate /// the file to 0 length if it already exists. /// /// The file must be opened with write access for truncate to work. /// /// Behaviour of truncate on directories and symlink files is unspecified. /// /// On Windows a file-symlink from A to B when truncated with no-follow /// `(.write(true).truncate(true).follow(false) )` will convert the target /// from a symlink to an empty file. The Windows behaviour is compatible /// with the definition of O_TRUNC on Unix - this case is unspecified. This /// cannot be made race-free, however it seems like a race will at most /// destroy a link, not permit elevation of privileges, so this can be /// handled by the caller by doing a readlink first, treating a success as /// an EEXISTS error, and then actually performing the no-follow truncation. /// /// On Unix platforms EEXISTS tends to be returned instead. /// /// ```no_compile /// use std::fs::OpenOptions; /// /// let file = OpenOptions::new().write(OpenOptionsWriteMode::Append).truncate(true).open_at(&mut parent, "foo.txt"); /// ``` pub fn truncate(&mut self, truncate: bool) -> &mut Self { self._impl.truncate(truncate); self } /// Set the option to create a new file when missing, while still opening /// existing files. Unlike the Rust stdlib, an options with write set to /// [`OpenOptionsWriteMode::None`] can still be used to create a new file. /// /// Platform specific: /// - on Windows, safely opens existing directories or makes new ones. /// - on Linux, consumes EEXIST when making a directory and returns an /// existing directory at that path if it exists. pub fn create(&mut self, create: bool) -> &mut Self { self._impl.create(create); self } /// Set the option to create a new file, rejecting existing entries at the /// pathname, whether links or directories. /// /// This is requested from the OS as an atomic operation, to provide safety /// against TOCTOU conditions. Whether this will occur as an atomic /// operation depends on the OS and filesystem in use. In particular NFS /// versions below 3 do not support the needed operations for atomicity. /// /// Unlike the Rust stdlib, an options with write set to /// [`OpenOptionsWriteMode::None`] can still be used to create a new file. /// /// ```no_compile /// use fs_at::OpenOptions; /// /// let file = OpenOptions::default().write(OpenOptionsWriteMode::Write) /// .create_new(true) /// .open_at(&mut parent, "foo.txt"); /// let f = OpenOptions::default() /// .open_at(&mut parent, "foo.txt").unwrap_err(); /// ``` pub fn create_new(&mut self, create_new: bool) -> &mut Self { self._impl.create_new(create_new); self } /// Set the option to follow symlinks /// /// This defaults to true, matching the behaviour of syscalls and most /// command line utilities - except for mkdir /// /// Unix: This corresponds to O_NOFOLLOW, which disables symlink resolution /// only for the last element of a path. /// /// Windows: This corresponds to controlling FILE_FLAG_OPEN_REPARSE_POINT, /// which behaves similarly. pub fn follow(&mut self, follow: bool) -> &mut Self { self._impl.follow(follow); self } /// Create a directory relative to an open directory. Errors if a rooted /// path is provided. /// /// Returns a [`File`] opened on the created directory. /// /// Platform specific: /// - on Windows, atomically creates a new directory (two syscalls: one to /// create the directory with link following disabled, and one to probe /// whether the opened directory is itself a link). /// - on Unix, treats EEXIST as an error, but on success requires a separate /// `openat` syscall to open the created directory. This limitation may be /// lifted in future if the mooted mkdirat2 call gets created.. The mode /// of the new directory defaults to 0o777. pub fn mkdir_at>(&self, d: &File, p: P) -> Result { self._impl .mkdir_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } /// Opens a file at the path p relative to the directory d. /// /// This will honour the options set for creation/append etc, but will only /// operate relative to d. To open a file with an absolute path, use the /// stdlib fs::OpenOptions. /// /// Platform specific: /// /// Windows: Backed by /// [NTCreateFile](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile). /// This function does not perform file name separator translations. If /// passing a path containing a separator, it must be a platform native one. /// e.g. `foo\\bar` on Windows, vs `foo/bar` on most other OS's. This /// function cannot open the parent directory (e.g. open_at(&d, "..")). It /// is possible for callers to determine the [path of a /// handle](https://learn.microsoft.com/en-us/windows/win32/memory/obtaining-a-file-name-from-a-file-handle), /// and then open that using normal stdlib functions. /// /// Unix: Backed by openat(2). pub fn open_at>(&self, d: &File, p: P) -> Result { self._impl .open_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } /// Opens a directory. /// /// This is a thin layer over [open_at] which handles the platform specific /// variation involved in opening a directory. Follow handling defaults off. /// /// As with [open_at], extension methods can be used to override the /// underlying behaviour. /// /// Before 0.1.6 follow was always disabled. /// /// Platform specific: /// /// Windows: sets FILE_FLAG_OPEN_REPARSE_POINT for createOptions when follow /// is disabled, and for dwAccessFlag adds in FILE_LIST_DIRECTORY and /// FILE_TRAVERSE. Further, read and write requests are translated to /// FILE_READ_ATTRIBUTES, and FILE_WRITE_ATTRIBUTES|DELETE respectively. /// /// Unix: sets O_NOFOLLOW depending on the but honours `follow`. Also /// O_PATH on platforms that define it. pub fn open_dir_at>(&self, d: &File, p: P) -> Result { self._impl .open_dir_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } /// Opens a path. /// /// This will open a file that refers to a path which could be normally /// unopenable. This is useful for inspecting files in a race-free fashion /// without requiring full permissions to them. /// /// Platform specific: /// /// Windows: sets FILE_FLAG_OPEN_REPARSE_POINT for createOptions. The /// windows extension trait can be used to set dwAccessFlags. All /// normal operations can be performed on a file opened in this way /// (assuming appropriate access flags). /// /// Unix: sets O_NOFOLLOW | O_PATH. Many operations on the file handle are /// restricted. /// /// AIX, DragonFlyBSD, iOS, MacOSX, NetBSD, OpenBSD, and illumos: Not /// implemented as O_PATH is not defined. #[cfg(not(any( target_os = "aix", target_os = "dragonfly", target_os = "ios", target_os = "macos", target_os = "netbsd", target_os = "openbsd", target_os = "illumos" )))] pub fn open_path_at>(&self, d: &File, p: P) -> Result { self._impl .open_path_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } /// Creates a symlink at the path linkname pointing to target. /// /// This will fail if the path linkname is already used. /// /// Unlike [`open_at`] this doesn't return a File object: opening symlink /// files directly is not portable. /// /// Note: on Windows two syscalls are required to create a symlink. The /// creation of the backing file is atomic and safe, but it is possible if /// the process is interrupted that it will remain as a an blank /// [`LinkEntryType`] rather than being converted to a symlink. /// https://github.com/rbtcollins/fs_at/issues/10 /// /// The target may be an absolute or relative path, and will be inspected to /// determine that before creation - but as with [`open_at`] native OS path /// separators must be used, and minimal processing is done - to use /// absolute paths, canonicalise them first. /// /// The `entry_type` is unused on *nix OS's; if writing *nix only software, /// just pass in LinkEntryType::default(). Similarly if writing portable /// software where the only consumers will be symlink aware. But if humans /// using a UI are expected to interact with the link, choose an appropriate /// type based on how the UI should behave when viewing the parent. /// /// Stability: it isn't clear whether entry_type should be exposed, or the /// default should be just a file(or dir) always and then fine grained /// control via an extension trait. pub fn symlink_at( &self, d: &File, linkname: P, entry_type: LinkEntryType, target: Q, ) -> Result<()> where P: AsRef, Q: AsRef, { self._impl.symlink_at( d, OpenOptions::ensure_rootless(linkname.as_ref())?, entry_type, target.as_ref(), ) } /// Unlink a non-directory at a path relative to d. /// /// If the path referred to is a symbolic link, the link itself is removed. /// /// Platform specific: some platforms treat unlink and rmdir as equivalent. /// Others such as Mac OSX do not, and rmdir must be used when deleting a /// directory. pub fn unlink_at

(&self, d: &File, p: P) -> Result<()> where P: AsRef, { self._impl .unlink_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } /// Remove a directory at a path relative to d. /// /// Platform specific: some platforms treat unlink and rmdir as equivalent. /// Others such as Mac OSX do not, and rmdir must be used when deleting a /// directory. pub fn rmdir_at

(&self, d: &File, p: P) -> Result<()> where P: AsRef, { self._impl .rmdir_at(d, OpenOptions::ensure_rootless(p.as_ref())?) } fn ensure_rootless(p: &Path) -> Result<&Path> { if p.has_root() { return Err(Error::new( ErrorKind::Other, format!("Rooted file path {p:?}"), )); } Ok(p) } } /// Iterate over the contents of a directory. Created by calling read_dir() on /// an opened directory. Each item yielded by the iterator is an io::Result to /// allow communication of io errors as the iterator is advanced. /// /// To the greatest extent possible the underlying OS semantics are preserved. /// That means that `.` and `..` entries are exposed, and that no sort order is /// guaranteed by the iterator. /// /// On both unix and Windows directory iteration affects shared mutable state, /// thus this iterator holds an &mut File for the lifetime of the iterator. The /// workaround - opening a new file - can be performed by users of the library /// if desired. /// /// (On Unix fdopendir is used to obtain a directory stream, but as closedir /// closes the file descriptor the original descriptor is dup2'd first. But as /// dup2 duplicated descriptors share the open file description, the position in /// readdir() is shared: permitting other concurrent readdir iterations to be /// started concurrently might be memory safe, but its clearly not safe safe. /// /// On Windows a similar situation applies with FileIdBothDirectoryInfo / /// FileIdBothDirectoryRestartInfo and DuplicateHandle: DuplicateHandle aliases /// into kernel state rather than creating an entirely separate accounting. #[derive(Debug)] pub struct ReadDir<'a> { _impl: ReadDirImpl<'a>, } impl<'a> ReadDir<'a> { pub fn new(d: &'a mut File) -> Result { Ok(ReadDir { _impl: ReadDirImpl::new(d)?, }) } } impl Iterator for ReadDir<'_> { type Item = Result; fn next(&mut self) -> Option> { self._impl .next() .map(|entry| entry.map(|_impl| DirEntry { _impl })) } } /// The returned type for each entry found by [`read_dir`]. /// /// Each entry represents a single entry inside the directory. Platforms that /// provide rich metadata may in future expose this through methods or extension /// traits on DirEntry. /// /// For now however, only the [`name()`] is exposed. This does not imply any /// additional IO for most workloads: metadata returned from a directory listing /// is inherently racy: presuming that what was a dir, or symlink etc when the /// directory was listed, will still be the same when opened is fallible. /// Instead, use open_at to open the contents, and then process based on the /// type of content found. #[derive(Debug)] pub struct DirEntry { _impl: DirEntryImpl, } impl DirEntry { pub fn name(&self) -> &OsStr { self._impl.name() } } /// Read the children of the directory d. /// /// See [`ReadDir`] and [`DirEntry`] for details. pub fn read_dir(d: &mut File) -> Result { ReadDir::new(d) } /// File kind indicator /// /// On Windows symlinks are implemented an actual directory or file, with /// reparse data stored in a single global index; the kind of the actual /// directory or file leaks through to the operations one can perform on the /// symlink (e.g. cannot chdir from a CMD prompt to a file-backed symlink). #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum LinkEntryType { #[default] File, Dir, Other, } pub mod os { cfg_if::cfg_if! { if #[cfg(windows)] { pub use crate::win::exports as windows; } else { pub use crate::unix::exports as unix; } } } #[cfg(test)] pub mod testsupport; #[cfg(test)] mod tests { #[cfg(not(any( target_os = "macos", target_os = "ios", target_os = "netbsd", target_os = "illumos" )))] use std::path::Path; use std::{ ffi::OsStr, fs::{rename, File}, io::{Error, ErrorKind, Result, Seek, SeekFrom, Write}, path::PathBuf, time::{Duration, SystemTime}, }; use rayon::prelude::*; use tempfile::TempDir; use test_log::test; use crate::{ read_dir, testsupport::open_dir, DirEntry, LinkEntryType, OpenOptions, OpenOptionsWriteMode, }; // Can be inlined when more_io_errors stablises cfg_if::cfg_if! { if #[cfg(windows)] { use windows_sys::Win32::Foundation::{ERROR_CANT_RESOLVE_FILENAME, ERROR_DIRECTORY}; #[allow(non_snake_case)] fn FileSystemLoopError() -> Error { Error::from_raw_os_error( ERROR_CANT_RESOLVE_FILENAME as i32)} #[allow(non_snake_case)] fn NotADirectory() -> Error { Error::from_raw_os_error( ERROR_DIRECTORY as i32 )} } else { #[allow(non_snake_case)] fn FileSystemLoopError() -> Error { Error::from_raw_os_error(libc::ELOOP)} #[allow(non_snake_case)] fn NotADirectory() -> Error { Error::from_raw_os_error(libc::ENOTDIR)} } } /// Create a directory parent, open it, then rename it to renamed-parent and /// create another directory in its place. returns the file handle and the /// final path. fn setup() -> Result<(TempDir, File, PathBuf)> { let tmp = TempDir::new()?; let parent = tmp.path().join("parent"); let renamed_parent = tmp.path().join("renamed-parent"); std::fs::create_dir(&parent)?; let parent_file = open_dir(&parent)?; rename(parent, &renamed_parent)?; Ok((tmp, parent_file, renamed_parent)) } #[derive(Default, Debug, Clone, PartialEq, PartialOrd)] enum Op { // Perform a mkdirat call #[default] MkDir, // perform an open call on a file OpenFile, // perform an open call on a dir ? [should this be extension only?] #[allow(unused)] OpenDir, // perform an unlink of a non-dir Unlink, // perform a rmdir of a dir RmDir, } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd)] enum SymlinkMode { // no symlink present #[default] None, // operate on paths that are the target of a symlink e.g. foo/ LinkIsTarget, // operate on paths that are found through a symlink e.g. /foo LinkIsParent, } #[derive(Default, Debug, Clone)] struct Test { pub create: bool, pub create_new: bool, pub read: bool, pub write: OpenOptionsWriteMode, pub truncate: bool, pub op: Op, pub symlink_mode: SymlinkMode, pub symlink_entry_type: LinkEntryType, pub follow: Option, } impl Test { fn create(mut self, create: bool) -> Self { self.create = create; self } fn create_new(mut self, create_new: bool) -> Self { self.create_new = create_new; self } fn read(mut self, read: bool) -> Self { self.read = read; self } fn write(mut self, write: OpenOptionsWriteMode) -> Self { self.write = write; self } fn truncate(mut self, truncate: bool) -> Self { self.truncate = truncate; self } fn op(mut self, op: Op) -> Self { self.op = op; self } fn symlink_mode(mut self, symlink_mode: SymlinkMode) -> Self { self.symlink_mode = symlink_mode; self } fn symlink_entry_type(mut self, symlink_entry_type: LinkEntryType) -> Self { self.symlink_entry_type = symlink_entry_type; self } fn follow(mut self, follow: Option) -> Self { self.follow = follow; self } } fn _check_behaviour( test: Test, create_in_advance: bool, err: Option<&Error>, counter: &mut u32, ) -> Result<()> { eprintln!( "testing idx: {counter}, op: {test:?} create_in_advance: {create_in_advance}, err: {err:?}" ); *counter += 1; let (_tmp, parent_file, renamed_parent) = setup()?; let mut options = OpenOptions::default(); let (actual_child, child_name) = if test.symlink_mode == SymlinkMode::None { ("child", PathBuf::from("child")) } else if test.symlink_mode == SymlinkMode::LinkIsTarget { ("link_child", PathBuf::from("child")) } else { /* LinkIsParent */ ("link_child", PathBuf::from("link_dir").join("link_child")) }; if test.create { options.create(true); } if test.create_new { options.create_new(true); } if test.read { options.read(true); } options.write(test.write); if test.truncate { options.truncate(true); } if let Some(follow) = test.follow { options.follow(follow); } if create_in_advance { match test.op { Op::MkDir | Op::RmDir => { options.mkdir_at(&parent_file, actual_child)?; } Op::OpenDir => (), Op::OpenFile | Op::Unlink => { let mut first_file = OpenOptions::default() .create(true) .write(OpenOptionsWriteMode::Write) .open_at(&parent_file, actual_child)?; assert_eq!(16, first_file.write(b"existing content")?); first_file.flush()?; } } } match test.symlink_mode { SymlinkMode::None => {} SymlinkMode::LinkIsParent => { OpenOptions::default().create(true).symlink_at( &parent_file, "link_dir", test.symlink_entry_type, ".", )?; } SymlinkMode::LinkIsTarget => { OpenOptions::default().create(true).symlink_at( &parent_file, &child_name, test.symlink_entry_type, actual_child, )?; } } if matches!(test.op, Op::MkDir | Op::OpenDir | Op::OpenFile) { // functions that return a file handle let res = match test.op { Op::MkDir => options.mkdir_at(&parent_file, &child_name), Op::OpenDir => unimplemented!(), Op::OpenFile => options.open_at(&parent_file, &child_name), _ => unreachable!(), }; let mut child = match (res, err) { (Ok(child), None) => child, (Ok(_), Some(e)) => panic!("unexpected success {e:?}"), (Err(e), None) => panic!("unexpected error {e:?}"), (Err(e), Some(expected_e)) => { assert_eq!(e.kind(), expected_e.kind(), "{e:?} != {expected_e:?}"); return Ok(()); } }; let expected = renamed_parent.join(actual_child); let metadata = expected.symlink_metadata()?; match test.op { Op::MkDir => assert!(metadata.is_dir()), Op::OpenDir => (), Op::OpenFile => { assert!(metadata.is_file()); // If the file was truncated, it will be 0-length. // If the file is new it will be 0-length. let initial_length = metadata.len(); if test.truncate || !create_in_advance { assert_eq!(initial_length, 0); } else { assert_eq!(initial_length, 16); } if test.write != OpenOptionsWriteMode::None { child.seek(SeekFrom::Start(10))?; assert_eq!(10, child.write(b"some data\n")?); if test.write == OpenOptionsWriteMode::Write { assert_eq!(expected.symlink_metadata()?.len(), 20); } else { // The write location is ignored in append mode assert_eq!(expected.symlink_metadata()?.len(), initial_length + 10); } } // } _ => unreachable!(), } } else { // Functions that delete something let res = match test.op { Op::RmDir => options.rmdir_at(&parent_file, &child_name), Op::Unlink => options.unlink_at(&parent_file, &child_name), _ => unreachable!(), }; match (res, err) { (Ok(()), None) => (), (Ok(_), Some(e)) => panic!("unexpected success {e:?}"), (Err(e), None) => panic!("unexpected error {e:?}"), (Err(e), Some(expected_e)) => { assert_eq!(e.kind(), expected_e.kind(), "{e:?} != {expected_e:?}"); return Ok(()); } }; // in the non-error case child_name should have been removed. let expected = renamed_parent.join(&child_name); match expected.symlink_metadata() { Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), Err(e) => Err(e), Ok(_) => panic!("{child_name:?} not deleted"), }?; } Ok(()) } // basic property based framework. Performs a specific combination of // options with a file-or-dir opening call, and verifies the resulting // object can be used as expected. Note that this cannot be used to create // actual races - but the library depends on the OS behaviour for race // safety: what we are checking for here is that we're passing the right // semantics down for when races do occur (e.g. O_EXCL is supplied when // requested...) // // Some combinations are illegal on some platforms and they get filtered // out. For instance file operations through a LinkEntryType::Dir link will // error on windows, and directory operations through a LinkEntryType::File // link will error likewise. fn check_behaviour(test: Test, counter: &mut u32) -> Result<()> { if cfg!(windows) && (matches!(test.op, Op::MkDir | Op::OpenDir) && matches!(test.symlink_entry_type, LinkEntryType::File)) || (matches!(test.op, Op::OpenFile) && matches!(test.symlink_entry_type, LinkEntryType::Dir)) { // Windows doesn't support dir operations on a file typed link or vice versa. return Ok(()); } if cfg!(windows) && test.symlink_mode == SymlinkMode::LinkIsTarget && test.follow == Some(false) && test.symlink_entry_type == LinkEntryType::File && test.op == Op::OpenFile && test.truncate { // Windows truncates the *symlink itself* on a truncate operation on // a LinkEntryType::File truncated with no-follow. Just skip the test entirely. return Ok(()); } let err = if test.symlink_mode == SymlinkMode::LinkIsTarget && (test.op == Op::MkDir || test.create_new) { // mkdirat is specified as failing with EEXIST if pathname exists - // including a dangling symlink. Force those scenarios to errors. // similarly openat with O_EXCL + O_CREAT == create_new. Some(Error::from(ErrorKind::AlreadyExists)) } else if test.symlink_mode == SymlinkMode::LinkIsTarget && test.follow == Some(false) { // follow(false) causes every openat to fail ELOOP when the path as given resolves to a link itself. Some(FileSystemLoopError()) } else if test.symlink_mode == SymlinkMode::LinkIsTarget && (test.op == Op::RmDir) { #[cfg(windows)] { if test.symlink_entry_type == LinkEntryType::Dir { // on windows symlinks can be directories None } else { // or they can be files Some(NotADirectory()) } } #[cfg(not(windows))] { // can't rmdir a symlink on unix ... Some(NotADirectory()) } } else { None }; if test.create_new { // run three tests: one that creates the path, and one that expects // an error operating on the existing path, and one that expects an // error likewise operating on an existing symlink _check_behaviour(test.clone(), false, err.as_ref(), counter)?; let err = Error::from(ErrorKind::AlreadyExists); _check_behaviour(test, true, Some(&err), counter) } else if test.create || test.truncate { // run two tests: one that creates the path, and once that opens // the existing path _check_behaviour(test.clone(), true, err.as_ref(), counter)?; _check_behaviour(test, false, err.as_ref(), counter) } else if matches!(test.op, Op::MkDir) { // run two tests: one that creates the path where it didn't exist // and one that precreates the path and expects an error _check_behaviour(test.clone(), false, err.as_ref(), counter)?; let err = Error::from(ErrorKind::AlreadyExists); _check_behaviour(test, true, Some(&err), counter) } else if matches!(test.op, Op::RmDir) { // run two tests: one that unlinks a missing path and expects an error // and one that creates the path and expects success when operating on a dir // or NotADirectory when operating on a symlink let missing_err = if test.symlink_mode == SymlinkMode::LinkIsTarget { // On Windows, the link itself may be a dir, which can then be // rmdired. Or the link may be a file, where rmdir is wrong, but seems to succeed. Thats a kernel concern!. #[cfg(windows)] { if test.symlink_entry_type == LinkEntryType::File { Some(NotADirectory()) } else { None } } #[cfg(not(windows))] { // when we rmdir a symlink (at least on linux) Some(NotADirectory()) } } else { // when we rmdir a missing path we get NotFound. Some(Error::from(ErrorKind::NotFound)) }; _check_behaviour(test.clone(), false, missing_err.as_ref(), counter)?; _check_behaviour(test, true, err.as_ref(), counter) } else if matches!(test.op, Op::Unlink) { // run two tests: one that unlinks a missing path and expects an error // except when operating on a symlink. // and one that creates the path and expects success. let missing_err = if test.symlink_mode == SymlinkMode::LinkIsTarget { None } else { Some(Error::from(ErrorKind::NotFound)) }; _check_behaviour(test.clone(), false, missing_err.as_ref(), counter)?; _check_behaviour(test, true, err.as_ref(), counter) } else { Ok(()) } } #[test] fn all_mkdir() -> Result<()> { let mut counter = 0; for create in [false, true] { for create_new in [false, true] { for read in [false, true] { for write in [ OpenOptionsWriteMode::None, OpenOptionsWriteMode::Write, OpenOptionsWriteMode::Append, ] { for symlink_mode in [ SymlinkMode::None, SymlinkMode::LinkIsParent, SymlinkMode::LinkIsTarget, ] { for symlink_entry_type in [LinkEntryType::Dir, LinkEntryType::File] { check_behaviour( Test::default() .create(create) .create_new(create_new) .read(read) .write(write) .symlink_mode(symlink_mode) .symlink_entry_type(symlink_entry_type) .op(Op::MkDir), &mut counter, )?; } } } } } } Ok(()) } #[test] fn all_rmdir() -> Result<()> { let mut counter = 0; for symlink_mode in [ SymlinkMode::None, SymlinkMode::LinkIsParent, SymlinkMode::LinkIsTarget, ] { for symlink_entry_type in [LinkEntryType::Dir, LinkEntryType::File] { check_behaviour( Test::default() .symlink_mode(symlink_mode) .symlink_entry_type(symlink_entry_type) .op(Op::RmDir), &mut counter, )?; } } Ok(()) } #[test] fn all_unlink() -> Result<()> { let mut counter = 0; for symlink_mode in [ SymlinkMode::None, SymlinkMode::LinkIsParent, SymlinkMode::LinkIsTarget, ] { for symlink_entry_type in [LinkEntryType::Dir, LinkEntryType::File] { check_behaviour( Test::default() .symlink_mode(symlink_mode) .symlink_entry_type(symlink_entry_type) .op(Op::Unlink), &mut counter, )?; } } Ok(()) } #[test] fn all_open_file() -> Result<()> { let mut counter = 0; for create in [false, true] { for create_new in [false, true] { for read in [false, true] { for write in [ OpenOptionsWriteMode::None, OpenOptionsWriteMode::Write, OpenOptionsWriteMode::Append, ] { for truncate in [false, true] { // Filter for open: without one of read/write/append all // calls will fail if !read && write == OpenOptionsWriteMode::None { continue; } for symlink_mode in [ SymlinkMode::None, SymlinkMode::LinkIsParent, SymlinkMode::LinkIsTarget, ] { for symlink_entry_type in [LinkEntryType::Dir, LinkEntryType::File] { for follow in [None, Some(true), Some(false)] { check_behaviour( Test::default() .create(create) .create_new(create_new) .read(read) .write(write) .truncate(truncate) .symlink_mode(symlink_mode) .symlink_entry_type(symlink_entry_type) .follow(follow) .op(Op::OpenFile), &mut counter, )?; } } } } } } } } Ok(()) } #[test] fn readdir_sync_send() -> Result<()> { let (_tmp, mut parent_dir, _pathname) = setup()?; let dirstream = read_dir(&mut parent_dir)?; dirstream .par_bridge() .try_for_each(|dir_entry| -> Result<()> { dir_entry?; Ok(()) })?; Ok(()) } #[test] fn readdir() -> Result<()> { let (_tmp, mut parent_dir, _pathname) = setup()?; assert_eq!( 2, // . and .. read_dir(&mut parent_dir)? .collect::>>()? .len() ); let dir_present = |children: &Vec, name: &OsStr| children.iter().any(|e| e.name() == name); let mut options = OpenOptions::default(); options.create_new(true).write(OpenOptionsWriteMode::Write); options.open_at(&parent_dir, "1")?; options.open_at(&parent_dir, "2")?; options.open_at(&options.mkdir_at(&parent_dir, "child")?, "3")?; let children = read_dir(&mut parent_dir)?.collect::>>()?; assert_eq!( 5, children.len(), "directory contains 5 entries (., .., 1, 2, child)" ); assert!(dir_present(&children, OsStr::new("1")), "{children:?}"); assert!(dir_present(&children, OsStr::new("2")), "{children:?}"); assert!(dir_present(&children, OsStr::new("child")), "{children:?}"); { let mut child = OpenOptions::default() .read(true) .open_at(&parent_dir, "child")?; let children = read_dir(&mut child)?.collect::>>()?; assert_eq!(3, children.len(), "{children:?}"); assert!(dir_present(&children, OsStr::new("3")), "{children:?}"); } Ok(()) } #[test] fn symlink_at() -> Result<()> { let (_tmp, mut parent_dir, _pathname) = setup()?; OpenOptions::default().symlink_at( &parent_dir, "linkname1", crate::LinkEntryType::Dir, "target", )?; OpenOptions::default().symlink_at( &parent_dir, "linkname2", crate::LinkEntryType::File, "target", )?; let children = read_dir(&mut parent_dir)?.collect::>>()?; assert_eq!( 4, // . and .. and the two links children.len() ); assert!(children.iter().any(|e| e.name() == "linkname1")); assert!(children.iter().any(|e| e.name() == "linkname2")); Ok(()) } #[test] fn open_dir_at() -> Result<()> { let (_tmp, parent_dir, _pathname) = setup()?; // setup { let dir = OpenOptions::default().mkdir_at(&parent_dir, "dir")?; OpenOptions::default() .create_new(true) .write(OpenOptionsWriteMode::Write) .open_at(&dir, "file")?; OpenOptions::default().symlink_at( &parent_dir, "linkname", LinkEntryType::Dir, "dir", )?; } // case 1: no options -> error { OpenOptions::default() .open_dir_at(&parent_dir, "dir") .unwrap_err(); } // case 2: write - can we write the dir's date let reference_time = SystemTime::UNIX_EPOCH + Duration::from_secs(10); { let dir = OpenOptions::default() .write(OpenOptionsWriteMode::Write) .open_dir_at(&parent_dir, "dir")?; fs_set_times::SetTimes::set_times( &dir, None, Some(fs_set_times::SystemTimeSpec::Absolute(reference_time)), )?; } // case 3: read - can we read the dir's date { let dir = OpenOptions::default() .read(true) .open_dir_at(&parent_dir, "dir")?; assert_eq!(reference_time, dir.metadata()?.modified()?); } // case 4: can we traverse the directory { let mut dir = OpenOptions::default() .read(true) .open_dir_at(&parent_dir, "dir")?; OpenOptions::default().read(true).open_at(&dir, "file")?; let children = super::read_dir(&mut dir)?.map(|dir_entry| dir_entry.unwrap().name().to_owned()); assert_eq!(3, children.count()); } // case 5: we cannot open a directory via a symlink by default { OpenOptions::default() .read(true) .open_dir_at(&parent_dir, "linkname") .unwrap_err(); } // case 6: but we can if we enable follow { let dir = OpenOptions::default() .read(true) .follow(true) .open_dir_at(&parent_dir, "linkname")?; assert_eq!(reference_time, dir.metadata()?.modified()?); } Ok(()) } #[cfg(not(any( target_os = "aix", target_os = "dragonfly", target_os = "ios", target_os = "macos", target_os = "netbsd", target_os = "openbsd", target_os = "illumos" )))] #[test] fn open_path_at() -> Result<()> { let (_tmp, parent_dir, _pathname) = setup()?; // setup { let dir = OpenOptions::default().mkdir_at(&parent_dir, "dir")?; OpenOptions::default() .create_new(true) .write(OpenOptionsWriteMode::Write) .open_at(&dir, "file")?; OpenOptions::default().symlink_at(&dir, "linkname", LinkEntryType::File, "target")?; } // case 1: open a dir { OpenOptions::default().open_path_at(&parent_dir, "dir")?; } // case 2: open a file { OpenOptions::default().open_path_at(&parent_dir, Path::new("dir").join("file"))?; } // case 3: open a link { OpenOptions::default().open_path_at(&parent_dir, Path::new("dir").join("linkname"))?; } // case 4: can we open-and-delete on windows #[cfg(windows)] { use windows_sys::Win32::Storage::FileSystem::DELETE; use super::os::windows::{FileExt, OpenOptionsExt}; let f = OpenOptions::default() .desired_access(DELETE) .open_path_at(&parent_dir, "dir\\linkname")?; f.delete_by_handle().map_err(|(_, e)| e)?; } // case 4: can we traverse a directory on windows #[cfg(windows)] { use windows_sys::Win32::Storage::FileSystem::FILE_LIST_DIRECTORY; use super::os::windows::OpenOptionsExt; let mut dir = OpenOptions::default() .desired_access(FILE_LIST_DIRECTORY) .open_path_at(&parent_dir, "dir")?; let children = super::read_dir(&mut dir)?.map(|dir_entry| dir_entry.unwrap().name().to_owned()); assert_eq!(3, children.count()); } Ok(()) } #[test] fn check_eloop_raw_os_value() -> Result<()> { let (_tmp, parent_dir, _pathname) = setup()?; OpenOptions::default().symlink_at( &parent_dir, "linkname1", crate::LinkEntryType::Dir, "linkname2", )?; OpenOptions::default().symlink_at( &parent_dir, "linkname2", crate::LinkEntryType::Dir, "linkname1", )?; let e = OpenOptions::default() .read(true) .open_at(&parent_dir, "linkname1") .unwrap_err(); assert_eq!(e.raw_os_error(), FileSystemLoopError().raw_os_error()); Ok(()) } } fs_at-0.1.10/src/testsupport.rs000064400000000000000000000014541046102023000145540ustar 00000000000000use std::{ fs::{File, OpenOptions}, io::Result, path::Path, }; cfg_if::cfg_if! { if #[cfg(windows)] { pub fn open_dir(p:&Path) -> Result { use std::os::windows::fs::OpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; let mut options = OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); options.open(p) } } else { pub fn open_dir(p:&Path) -> Result { use std::os::unix::fs::OpenOptionsExt; use libc; let mut options = OpenOptions::new(); options.read(true); options.custom_flags(libc::O_NOFOLLOW); options.open(p) } } } fs_at-0.1.10/src/unix.rs000064400000000000000000000370541046102023000131300ustar 00000000000000use std::{ ffi::{CString, OsStr, OsString}, fs::File, io::Result, marker::PhantomData, os::unix::prelude::{AsRawFd, FromRawFd, OsStrExt}, path::Path, ptr, }; // This will probably take a few iterations to get right. The idea: always use // an openat64, and import the right variant for the platform. See File::open_c in [`std::sys::unix::fs`]. cfg_if::cfg_if! { if #[cfg(any(target_os = "aix", target_os = "macos", target_os = "dragonfly", target_os = "freebsd", target_os = "ios", target_os = "netbsd", target_os = "openbsd", target_os = "illumos"))] { use libc::openat as openat64; } else { use libc::openat64; } } use cvt::cvt_r; use libc::{c_int, mkdirat, mode_t}; use crate::{LinkEntryType, OpenOptions, OpenOptionsWriteMode}; pub mod exports { pub use super::OpenOptionsExt; #[doc(no_inline)] pub use libc::mode_t; } trait PathFFI { fn as_cstring(&self) -> Result; } impl PathFFI for Path { fn as_cstring(&self) -> Result { std::ffi::CString::new(self.as_os_str().as_bytes()) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) } } #[derive(Debug, Default)] pub(crate) struct OpenOptionsImpl { read: bool, write: OpenOptionsWriteMode, truncate: bool, create: bool, create_new: bool, follow: Option, mode: Option, } impl OpenOptionsImpl { pub fn read(&mut self, read: bool) { self.read = read; } pub fn write(&mut self, write: OpenOptionsWriteMode) { self.write = write; } pub fn truncate(&mut self, truncate: bool) { self.truncate = truncate; } pub fn create(&mut self, create: bool) { self.create = create; } pub fn create_new(&mut self, create_new: bool) { self.create_new = create_new; } pub fn follow(&mut self, follow: bool) { self.follow = Some(follow) } fn get_flags(&self) -> Result { let data_flags = match (self.read, self.write) { (false, OpenOptionsWriteMode::Write) => Ok(libc::O_WRONLY), (false, OpenOptionsWriteMode::Append) => Ok(libc::O_WRONLY | libc::O_APPEND), (false, OpenOptionsWriteMode::None) => { Err(std::io::Error::from_raw_os_error(libc::EINVAL)) } (true, OpenOptionsWriteMode::None) => Ok(libc::O_RDONLY), (true, OpenOptionsWriteMode::Write) => Ok(libc::O_RDWR), (true, OpenOptionsWriteMode::Append) => Ok(libc::O_RDWR | libc::O_APPEND), }?; let create_flags = if self.create_new { libc::O_EXCL | libc::O_CREAT } else if self.truncate { libc::O_CREAT | libc::O_TRUNC } else if self.create { libc::O_CREAT } else { 0 }; let no_follow_flag = match self.follow { None | Some(true) => 0, Some(false) => libc::O_NOFOLLOW, }; // Some / all of these need to become OpenOptions controls. let common_flags = libc::O_CLOEXEC | libc::O_NOCTTY; // We should add an extension to suppport libc::O_PATH as NtCreateFile // has a matching capability. // Similarly O_TMPFILE Ok(data_flags | create_flags | common_flags | no_follow_flag) } pub fn open_at(&self, d: &File, path: &Path) -> Result { let flags = self.get_flags()?; self._open_at(d, path, flags) } pub fn open_dir_at(&self, d: &File, path: &Path) -> Result { if matches!((self.read, self.write), (false, OpenOptionsWriteMode::None)) { return Err(std::io::Error::from_raw_os_error(libc::EINVAL)); } let no_follow_flag = match self.follow { None | Some(false) => libc::O_NOFOLLOW, Some(true) => 0, }; let flags = libc::O_RDONLY | no_follow_flag | libc::O_CLOEXEC | libc::O_NOCTTY; self._open_at(d, path, flags) } #[cfg(not(any( target_os = "aix", target_os = "dragonfly", target_os = "ios", target_os = "macos", target_os = "netbsd", target_os = "openbsd", target_os = "illumos" )))] pub fn open_path_at(&self, d: &File, path: &Path) -> Result { let flags = libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_PATH | libc::O_CLOEXEC | libc::O_NOCTTY; self._open_at(d, path, flags) } fn _open_at(&self, d: &File, path: &Path, flags: i32) -> Result { let path = path.as_cstring()?; let mode = self.mode.unwrap_or(0o777); // TODO // Consider using openat2 on Linux... though that requires direct // syscall usage today. https://man7.org/linux/man-pages/man2/openat2.2.html let fd = cvt_r(|| unsafe { openat64(d.as_raw_fd(), path.as_ptr(), flags, mode as c_int) })?; Ok(unsafe { File::from_raw_fd(fd) }) } /// One note: as the widespread unix interfaces don't offer atomic /// create-and-open, there is a race condition here (which is bad as this /// crate exists to target race conditions). Possibly addressable by a /// create-random + atomic move into place, but it isn't clear that this /// interface should hide that. mkdirat2 does not exist yet, though patch /// sets have been proposed. The second wart is that a non-O_EXCL mode /// doesn't exist: mkdir_at() fails if the target exists and is a dir /// already. pub fn mkdir_at(&self, d: &File, path: &Path) -> Result { let path = path.as_cstring()?; let mode = self.mode.unwrap_or(0o777); let mut mkdir_e = None; // create match cvt_r(|| unsafe { mkdirat(d.as_raw_fd(), path.as_ptr(), mode) }) { Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { // IFF exclusive wasn't requested (currently the default), then // proceed to open-as-dir and return existing dir. if self.create && !self.create_new { // save the error as we may be doing mkdir on top of a // non-dir mkdir_e = Some(e); Ok(()) } else { Err(e) } } Err(e) => Err(e), Ok(_) => Ok(()), }?; // Consider using openat2 on Linux... though that requires direct // syscall usage today. https://man7.org/linux/man-pages/man2/openat2.2.html let flags = libc::O_CLOEXEC | libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_DIRECTORY; // and open to return it let fd = cvt_r(|| unsafe { openat64(d.as_raw_fd(), path.as_ptr(), flags, mode as c_int) }); match fd { Err(fd_e) => { if let (Some(libc::ENOTDIR), Some(mkdir_e)) = (fd_e.raw_os_error(), mkdir_e) { Err(mkdir_e) } else { Err(fd_e) } } Ok(fd) => Ok(unsafe { File::from_raw_fd(fd) }), } } pub fn symlink_at( &self, d: &File, linkname: &Path, _entry_type: LinkEntryType, target: &Path, ) -> Result<()> { let linkname = linkname.as_cstring()?; let target = target.as_cstring()?; cvt_r(|| unsafe { libc::symlinkat(target.as_ptr(), d.as_raw_fd(), linkname.as_ptr()) }) .map(|_| ()) } pub fn rmdir_at(&self, d: &File, p: &Path) -> Result<()> { self.unlinkat(d, p, libc::AT_REMOVEDIR) } pub fn unlink_at(&self, d: &File, p: &Path) -> Result<()> { self.unlinkat(d, p, 0) } fn unlinkat(&self, d: &File, p: &Path, flags: c_int) -> Result<()> { let path = p.as_cstring()?; cvt_r(|| unsafe { libc::unlinkat(d.as_raw_fd(), path.as_ptr(), flags) }).map(|_| ()) } } pub trait OpenOptionsExt { /*! Set mode bits for new inode creation. This is masked out by umask as well. ```no_run use std::{fs, os::unix::fs::OpenOptionsExt as StdOpenOptionsExt}; use fs_at::OpenOptions; use fs_at::os::unix::OpenOptionsExt; use libc; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(libc::O_NOFOLLOW); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.mode(0o700); // Only permit the euid to access the directory. let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn mode(&mut self, mode: mode_t) -> &mut Self; } impl OpenOptionsExt for OpenOptions { fn mode(&mut self, mode: mode_t) -> &mut Self { self._impl.mode = Some(mode); self } } #[derive(Debug)] pub(crate) struct ReadDirImpl<'a> { // Since we clone the FD, the original FD is now separate. In theory. // // In practice they may share the same file offset, and some OS's use that // for managing dirstream state; thus both the original and the cloned fd // that the DIR takes ownership of must not be used concurrently. The // PhantomData here causes the borrow checker to consider the mut borrow // required for read_dir(&mut d) to extend to the life of the ReadDirImpl // struct. _phantom: PhantomData<&'a mut File>, // Set to None after closedir is called on the pointed at struct. // Perhaps we should we impl Send and Sync // because the data referenced is owned by libc ? dir: Option>, } // Safety: DIR is aligned correctly as it is returned by libc. Initialized by // libc and dereferencable. The original FD which might share library state is // mutably borrowed for the lifetime of the ReadDirImpl, enforced by the borrow // checker, granting us sole access to the dirstream unless other unsafe code is // used (e.g. cloning the fd before constructing a ReadDirImpl). Its possible // that on some platforms DIR is radically different, so we depend on Box<> to // figure out Send-ability of DIR. unsafe impl<'a> Send for ReadDirImpl<'a> where Box: Send {} // Safety: As above, DIR is aligned etc; further all mutation uses mutable // borrows. There is no way to go from ReadDirImpl to &Dir, and synchronisation // is dependent on the MT-Safe behaviour of readdir. // https://www.gnu.org/software/libc/manual/html_node/Reading_002fClosing-Directory.html // says "Because of this, it is not safe to share a DIR object among multiple // threads, unless you use your own locking to ensure that no thread calls // readdir while another thread is still using the data from the previous call. // In the GNU C Library, it is safe to call readdir from multiple threads as // long as each thread uses its own DIR object. POSIX.1-2008 does not require // this to be safe, but we are not aware of any operating systems where it does // not work." We have a unique DIR object, and the borrow checker will not // permit concurrent calls to next/close/drop because of the unique &mut // constraint. POSIX does not require memory barriers, merely that no thread is // using the data returned by a different call. next() takes care to copy out // data to an OsString before returning, meeting that requirement. unsafe impl<'a> Sync for ReadDirImpl<'a> where Box: Sync {} impl<'a> ReadDirImpl<'a> { // The code doesn't use the mutable value, but that is due to the value // being a simple int passed to the kernel. The kernel does change global // state, so the mutable ref is entirely appropriate. #[allow(clippy::needless_pass_by_ref_mut)] pub fn new(dir_file: &'a mut File) -> Result { // closedir closes the FD; make a new one that we can close when done with. let new_fd = cvt_r(|| unsafe { libc::fcntl(dir_file.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) })?; let mut dir = Some( ptr::NonNull::new(unsafe { libc::fdopendir(new_fd) }).ok_or_else(|| { let _droppable = unsafe { File::from_raw_fd(new_fd) }; std::io::Error::last_os_error() })?, ); // If dir_file has had operations on it - such as open_at - its pointer // might not be at the start of the dir, and fdopendir is documented // (e.g. BSD man pages) to not rewind the fd - and our cloned fd // inherits the pointer. if let Some(d) = dir.as_mut() { unsafe { libc::rewinddir(d.as_mut()) }; } Ok(ReadDirImpl { _phantom: PhantomData, dir, }) } fn close_dir(&mut self) -> Result<()> { if let Some(ref mut dir) = self.dir { let result = unsafe { libc::closedir(dir.as_mut()) }; // call made, clear state self.dir = None; cvt_r(|| result)?; } Ok(()) } } impl Drop for ReadDirImpl<'_> { fn drop(&mut self) { // like the stdlib, we eat errors occuring during drop, as there is no // way to get error handling. let _ = self.close_dir(); } } impl Iterator for ReadDirImpl<'_> { type Item = Result; fn next(&mut self) -> Option { let dir = unsafe { self.dir?.as_mut() }; // the readdir result is only guaranteed valid within the same thread // and until other calls are made on the same dir stream. Thus we // perform the required work inside next, allowing the next call to // readdir to be managed by the single mutable borrower rule in Rust. // readdir requires errno set to zero. nix::Error::clear(); ptr::NonNull::new(unsafe { libc::readdir(dir) }) .map(|e| { Ok(DirEntryImpl { name: unsafe { // Step one: C pointer to CStr - referenced data, length not known. let c_str = std::ffi::CStr::from_ptr(e.as_ref().d_name.as_ptr()); // Step two: OsStr: referenced data, length calcu;ated let os_str = OsStr::from_bytes(c_str.to_bytes()); // Step three: owned copy os_str.to_os_string() }, }) }) .or_else(|| { // NULL result, an error IFF errno has been set. let err = std::io::Error::last_os_error(); if err.raw_os_error() == Some(0) { None } else { Some(Err(err)) } }) } } #[derive(Debug)] pub(crate) struct DirEntryImpl { name: OsString, } impl DirEntryImpl { pub fn name(&self) -> &OsStr { &self.name } } #[cfg(test)] mod tests { use std::{ fs::{rename, File}, io::Result, os::unix::prelude::MetadataExt, }; use tempfile::TempDir; use test_log::test; use crate::{os::unix::OpenOptionsExt, testsupport::open_dir, OpenOptions}; #[test] fn mkdirat_mode() -> Result<()> { let tmp = TempDir::new()?; let parent = tmp.path().join("parent"); let renamed_parent = tmp.path().join("renamed-parent"); std::fs::create_dir(&parent)?; let parent_file = open_dir(&parent)?; rename(parent, &renamed_parent)?; let mut create_opt = OpenOptions::default(); create_opt.mode(0o700); let child: File = create_opt.mkdir_at(&parent_file, "child")?; let expected = renamed_parent.join("child"); let metadata = expected.symlink_metadata()?; assert!(metadata.is_dir()); assert_eq!(child.metadata()?.mode() & 0o777, 0o700); Ok(()) } } fs_at-0.1.10/src/win/sugar.rs000064400000000000000000000034361046102023000140600ustar 00000000000000use std::{fmt, mem::MaybeUninit}; use windows_sys::Win32::Foundation::{RtlNtStatusToDosError, NTSTATUS, UNICODE_STRING}; use super::windows_sys_gap_defs::init_unicode_string; pub struct NTStatusError { pub status: NTSTATUS, } /// Mimics the behavior of the NT_SUCCESS macro from Microsoft C headers fn nt_success(status: NTSTATUS) -> bool { status >= 0 } impl NTStatusError { pub fn from(status: NTSTATUS) -> std::result::Result<(), NTStatusError> { if nt_success(status) { Ok(()) } else { Err(NTStatusError { status }) } } } impl fmt::Display for NTStatusError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let code = unsafe { RtlNtStatusToDosError(self.status) }; let err = std::io::Error::from_raw_os_error(code as i32); err.fmt(f) } } impl From for std::io::Error { fn from(status: NTStatusError) -> Self { let code: i32 = unsafe { RtlNtStatusToDosError(status.status) as i32 }; std::io::Error::from_raw_os_error(code) } } // This does not have (or need) Drop: the vec retains the content, and we // don't call RtlAnsiStringToUnicodeString pub struct OSUnicodeString { _content: Vec, pub inner: UNICODE_STRING, } impl TryFrom> for OSUnicodeString { type Error = NTStatusError; fn try_from(content: Vec) -> std::result::Result { let mut content = content; content.push(0); let mut inner = MaybeUninit::uninit(); unsafe { NTStatusError::from(init_unicode_string(inner.as_mut_ptr(), &mut content)) }?; let winapi_string = unsafe { inner.assume_init() }; Ok(OSUnicodeString { _content: content, inner: winapi_string, }) } } fs_at-0.1.10/src/win.rs000064400000000000000000001537601046102023000127450ustar 00000000000000mod sugar; use std::{ ffi::{c_void, OsStr, OsString}, fmt, fs::File, io::{self, ErrorKind, Result}, mem::{self, size_of, zeroed, MaybeUninit}, os::windows::prelude::{AsRawHandle, FromRawHandle, MetadataExt, OsStrExt, OsStringExt}, path::Path, ptr::{self, null_mut}, slice, }; use aligned::{Aligned, A8}; use sugar::{NTStatusError, OSUnicodeString}; use windows_sys::Win32::{ Foundation::{ ERROR_CANT_RESOLVE_FILENAME, ERROR_DIRECTORY, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_A_REPARSE_POINT, ERROR_NOT_SUPPORTED, ERROR_NO_MORE_FILES, HANDLE, TRUE, }, Security::{SECURITY_DESCRIPTOR, SECURITY_QUALITY_OF_SERVICE}, Storage::FileSystem::{ FileBasicInfo, FileDispositionInfo, FileDispositionInfoEx, FileIdBothDirectoryInfo, FileIdBothDirectoryRestartInfo, GetFileInformationByHandleEx, NtCreateFile, SetFileInformationByHandle, DELETE, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_READONLY, FILE_BASIC_INFO, FILE_CREATE, FILE_DISPOSITION_INFO, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_ID_BOTH_DIR_INFO, FILE_INFO_BY_HANDLE_CLASS, FILE_LIST_DIRECTORY, FILE_OPEN, FILE_OPEN_IF, FILE_OVERWRITE_IF, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_TRAVERSE, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, SYNCHRONIZE, }, System::{ Ioctl::{FSCTL_GET_REPARSE_POINT, FSCTL_SET_REPARSE_POINT}, Kernel::OBJ_CASE_INSENSITIVE, SystemServices::{IO_REPARSE_TAG_MOUNT_POINT, IO_REPARSE_TAG_SYMLINK}, WindowsProgramming::{ FILE_CREATED, FILE_DIRECTORY_FILE, FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_DOES_NOT_EXIST, FILE_EXISTS, FILE_OPENED, FILE_OPEN_REPARSE_POINT, FILE_OVERWRITTEN, FILE_SUPERSEDED, FILE_SYNCHRONOUS_IO_NONALERT, OBJECT_ATTRIBUTES, }, IO::DeviceIoControl, }, }; use crate::{LinkEntryType, OpenOptions, OpenOptionsWriteMode}; use exports::SECURITY_CONTEXT_TRACKING_MODE; use self::windows_sys_gap_defs::{ reparse_definitions::{REPARSE_DATA_BUFFER_u_SymbolicLinkReparseBuffer, REPARSE_DATA_BUFFER}, SYMLINK_FLAG_RELATIVE, }; pub mod exports { pub use super::{FileExt, OpenOptionsExt}; pub use super::windows_sys_gap_defs::SECURITY_CONTEXT_TRACKING_MODE; #[doc(no_inline)] pub use windows_sys::Win32::Security::SECURITY_DESCRIPTOR; } // These definitions should come from windows_sys, but don't exist right now. pub(crate) mod windows_sys_gap_defs { use windows_sys::Win32::{ Foundation::{NTSTATUS, STATUS_INVALID_PARAMETER, STATUS_SUCCESS, UNICODE_STRING}, System::{ SystemServices::UNICODE_STRING_MAX_CHARS, WindowsProgramming::RtlInitUnicodeString, }, }; // RtlInitUnicodeStringEx isn't available in windows_sys at this time, and // won't be (see https://github.com/microsoft/win32metadata/issues/1461) so // we're going to roll our own. We'll rely on RtlInitUnicodeString to do // this, and just make sure we don't pass it information that would induce // an error. pub unsafe fn init_unicode_string( destination_string: *mut UNICODE_STRING, source_string: &mut [u16], ) -> NTSTATUS { if source_string.len() > UNICODE_STRING_MAX_CHARS as usize || !source_string.iter().rev().any(|i| *i == 0) { return STATUS_INVALID_PARAMETER; } RtlInitUnicodeString(destination_string, source_string.as_mut_ptr()); STATUS_SUCCESS } // SECURITY_CONTEXT_TRACKING_MODE is not available in windows_sys yet, but it's a pretty simple definition // so in order to maintain API compatibility we'll replicate it here. See https://github.com/microsoft/win32metadata/issues/1464 #[allow(non_camel_case_types)] pub type SECURITY_CONTEXT_TRACKING_MODE = u8; // Usually this is defined in a C header. There is no Rust equivalent of this in windows_sys yet, so we redefine it here. // See https://github.com/microsoft/win32metadata/issues/1462 pub const SYMLINK_FLAG_RELATIVE: u32 = 1; /// Strictly speaking this should be provided by something like windows_sys, however the definition isn't there, /// so we'll replicate it from the headers. These structures impact safety sensitive code and should only be changed /// in order to more accurately reflect the definition in Ntifs.h /// See https://github.com/microsoft/win32metadata/issues/1463 #[allow(non_snake_case)] pub(crate) mod reparse_definitions { #[repr(C)] #[derive(Clone, Copy)] pub struct REPARSE_DATA_BUFFER { pub ReparseTag: u32, pub ReparseDataLength: u16, pub Reserved: u16, pub u: REPARSE_DATA_BUFFER_u, } #[repr(C)] #[derive(Clone, Copy)] pub union REPARSE_DATA_BUFFER_u { pub SymbolicLinkReparseBuffer: REPARSE_DATA_BUFFER_u_SymbolicLinkReparseBuffer, pub MountPointReparseBuffer: REPARSE_DATA_BUFFER_u_MountPointReparseBuffer, pub GenericReparseBuffer: REPARSE_DATA_BUFFER_u_GenericReparseBuffer, } #[repr(C)] #[derive(Clone, Copy)] pub struct REPARSE_DATA_BUFFER_u_SymbolicLinkReparseBuffer { pub SubstituteNameOffset: u16, pub SubstituteNameLength: u16, pub PrintNameOffset: u16, pub PrintNameLength: u16, pub Flags: u32, pub PathBuffer: [u16; 1], } #[repr(C)] #[derive(Clone, Copy)] pub struct REPARSE_DATA_BUFFER_u_MountPointReparseBuffer { pub SubstituteNameOffset: u16, pub SubstituteNameLength: u16, pub PrintNameOffset: u16, pub PrintNameLength: u16, pub PathBuffer: [u16; 1], } #[repr(C)] #[derive(Clone, Copy)] pub struct REPARSE_DATA_BUFFER_u_GenericReparseBuffer { pub DataBuffer: [u16; 1], } } } #[derive(Clone, Default)] pub(crate) struct OpenOptionsImpl { create: bool, create_new: bool, truncate: bool, read: bool, write: OpenOptionsWriteMode, follow: Option, // LARGE_INTEGER defined as signed 64-bit // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-large_integer-r1 allocation_size: i64, desired_access: Option, create_options: Option, //https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#dwFlagsAndAttributes file_attributes: u32, object_attributes: u32, security_descriptor: Option, security_qos: Option, } impl fmt::Debug for OpenOptionsImpl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let qos_value = self.security_qos.and(Some("SET")).unwrap_or("NOTSET"); let descriptor_value = self .security_descriptor .and(Some("SET")) .unwrap_or("NOTSET"); f.debug_struct("OpenOptionsImpl") .field("object_attributes", &self.object_attributes) .field("security_qos", &qos_value) .field("security_descriptor", &descriptor_value) .finish() } } #[derive(Clone, Default)] struct DesiredAccess(u32); struct FileOpenDisposition(u32); #[derive(Clone, Default)] struct CreateOptions(u32); #[derive(PartialEq, Eq, PartialOrd, Ord)] enum OpenSymLink where MLE: Fn() -> io::Error, { OpenLinkFile, RaiseError(MLE), } fn open_link_file() -> OpenSymLink io::Error> { // maybe a trait would be easier. #[allow(unused_assignments)] let mut right_type_wrong_value = OpenSymLink::RaiseError(make_already_exists_error); right_type_wrong_value = OpenSymLink::OpenLinkFile; right_type_wrong_value } fn make_already_exists_error() -> io::Error { io::Error::from(ErrorKind::AlreadyExists) } pub(crate) fn make_loop_error() -> io::Error { io::Error::from_raw_os_error(ERROR_CANT_RESOLVE_FILENAME as i32) } // Cache the workaround for https://twitter.com/rbtcollins/status/1617211985384407044 #[cfg(feature = "workaround-procmon")] mod procmon { use std::{ io::Error, sync::atomic::{AtomicBool, Ordering}, }; use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED; static WORKAROUND_CHECKED: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| false.into()); static WORKAROUND_VALUE: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| false.into()); const ENV_VAR_NAME: &str = "FS_AT_WORKAROUND_PROCMON"; pub(crate) fn workaround(e: Error, ok: T) -> Result { if e.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) { // https://twitter.com/rbtcollins/status/1617211985384407044 let workaround = if !AtomicBool::load(&WORKAROUND_CHECKED, Ordering::Relaxed) { use std::env::var; let workaround = var(ENV_VAR_NAME).is_ok(); AtomicBool::store(&WORKAROUND_VALUE, workaround, Ordering::Relaxed); AtomicBool::store(&WORKAROUND_CHECKED, true, Ordering::Relaxed); workaround } else { AtomicBool::load(&WORKAROUND_VALUE, Ordering::Relaxed) }; if workaround { // run under procmon this library receives ACCESS_DENIED on some // DeviceIOControl calls :- but they seem to still take effect // for write calls, and succeed for reading links when an actual // link is present. e.g. ERROR_NOT_A_REPARSE_POINT. this // could mask other errors too but this code path is opt-in. return Ok(ok); } } Err(e) } } impl OpenOptionsImpl { pub fn read(&mut self, read: bool) { self.read = read; } pub fn write(&mut self, write: OpenOptionsWriteMode) { self.write = write; } pub fn truncate(&mut self, truncate: bool) { self.truncate = truncate; } pub fn create(&mut self, create: bool) { self.create = create; } pub fn create_new(&mut self, create_new: bool) { self.create_new = create_new; } pub fn follow(&mut self, follow: bool) { self.follow = Some(follow); } // NtCreateFile has too many arguments, and making a builder in our build // interface itself seems overkill. #[allow(clippy::too_many_arguments)] fn do_create_file( &self, f: &File, path: &Path, desired_access: DesiredAccess, create_disposition: FileOpenDisposition, create_options: CreateOptions, open_symlink: OpenSymLink, ) -> Result where MLE: Fn() -> io::Error, { let mut handle: MaybeUninit = MaybeUninit::uninit(); let mut object_attributes: OBJECT_ATTRIBUTES = unsafe { zeroed() }; object_attributes.Length = size_of::() as u32; object_attributes.RootDirectory = f.as_raw_handle() as isize; let u16_path = path.as_os_str().encode_wide().collect::>(); let mut rtl_string = OSUnicodeString::try_from(u16_path)?; object_attributes.ObjectName = &mut rtl_string.inner; // Only OBJ_CASE_INSENSITIVE is defined currently. What of // https://docs.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes // should be permitted through? Everything? // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile // only specifies OBJ_CASE_INSENSITIVE object_attributes.Attributes = self.object_attributes & OBJ_CASE_INSENSITIVE as u32; // Should allow setting this; NULL is sane but not fully flexible. // https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptors let mut security_descriptor = self.security_descriptor; object_attributes.SecurityDescriptor = match security_descriptor { Some(ref mut val) => val as *mut SECURITY_DESCRIPTOR as *mut c_void, None => ptr::null_mut(), }; // https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_quality_of_service let mut security_qos = self.security_qos; object_attributes.SecurityQualityOfService = match security_qos { Some(ref mut val) => val as *mut SECURITY_QUALITY_OF_SERVICE as *mut c_void, None => ptr::null_mut(), }; let mut status_block = MaybeUninit::uninit(); // Perhaps not worth exposing? let mut allocation_size = self.allocation_size; let allocation_size_ptr = if allocation_size > 0 { &mut allocation_size as *mut i64 } else { ptr::null_mut::() }; let file_attributes = if self.file_attributes == 0 { FILE_ATTRIBUTE_NORMAL } else { self.file_attributes }; let create_disposition = create_disposition.0; let create_options = create_options.0; let desired_access = desired_access.0; // TODO: support EA attributes if someone asks for it. let ea_buffer = null_mut(); let ea_length = 0; // This should be exposed (e.g. to permit secure temp dirs, secure untarring etc). let share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; unsafe { let ntstatus = NtCreateFile( handle.as_mut_ptr(), desired_access, &mut object_attributes, status_block.as_mut_ptr(), allocation_size_ptr, file_attributes, share_access, create_disposition, create_options, ea_buffer, ea_length, ); NTStatusError::from(ntstatus) }?; let status_block = unsafe { status_block.assume_init() }; // can be // FILE_CREATED // FILE_OPENED // FILE_OVERWRITTEN // FILE_SUPERSEDED // FILE_EXISTS // FILE_DOES_NOT_EXIST let information = u32::try_from(status_block.Information) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; match information { FILE_CREATED | FILE_OPENED | FILE_OVERWRITTEN => { // could be success : we have an FD // Check if we're opening let handle: HANDLE = unsafe { handle.assume_init() }; if matches!(open_symlink, OpenSymLink::RaiseError(_)) && Self::is_symlink(handle)? { // we found a symlink (due to setting // FILE_OPEN_REPARSE_POINT), but we're not permitted to open // the link itself (e.g. follow(false) was set). Synthesis an OS error. match open_symlink { OpenSymLink::RaiseError(make_link_error) => Err(make_link_error()), _ => unreachable!(), } } else { Ok(unsafe { File::from_raw_handle(handle as *mut c_void) }) } } FILE_SUPERSEDED | FILE_EXISTS | FILE_DOES_NOT_EXIST => { // Not covered by test coverage yet. unimplemented!("expected FILE_CREATED|FILE_OPENED|FILE_OVERWRITTEN|FILE_SUPERSEDED|FILE_EXISTS|FILE_DOES_NOT_EXIST, got {}", status_block.Information); } _ => { // Not covered by test coverage yet. unimplemented!("expected FILE_CREATED|FILE_OPENED|FILE_OVERWRITTEN|FILE_SUPERSEDED|FILE_EXISTS|FILE_DOES_NOT_EXIST, got {}", status_block.Information); } } } pub fn mkdir_at(&self, f: &File, path: &Path) -> Result { // get_access_mode must not be used for opening a directory // ... see docs or we must have file use the Ext trait always. let desired_access = DesiredAccess(DELETE | FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_WRITE_ATTRIBUTES); let mut create_disposition = self.get_file_disposition(true)?; if create_disposition.0 & (FILE_CREATE | FILE_OPEN_IF) == 0 { // per docs: create/open/openif required to open a dir, and this // function - mkdir - only creates. Permit users to opt into // create-or-open by calling .create(true) themselves. create_disposition.0 |= FILE_CREATE; } // we must open a directory. // For consistency with unix.rs, never follow a symlink at the location of // the mkdir target. let create_options = CreateOptions(FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT); let open_symlink = OpenSymLink::RaiseError(make_already_exists_error); self.do_create_file( f, path, desired_access, create_disposition, create_options, open_symlink, ) .map_err(|e| { if e.raw_os_error() == Some(ERROR_DIRECTORY as i32) { // NotADirectory happens when opening with FILE_OPEN_IF a Symlink // with link-type File and FILE_OPEN_REPARSE_POINT - nofollow. But // AlreadyExists is a better error consistent with Unix : the // NotADirectory error is leakage from the implementation of // symlinks on Windows. io::Error::new(ErrorKind::AlreadyExists, e) } else { e } }) } pub fn open_at(&self, f: &File, path: &Path) -> Result { let desired_access = self.get_open_at_access_mode()?; let create_disposition = self.get_file_disposition(false)?; // TODO: create options needs to be controlled through OpenOptions too. // FILE_SYNCHRONOUS_IO_NONALERT is set by CreateFile with the options // Rust itself uses - this lets the OS position tracker work. It also // requires SYNCHRONIZE on the access mode. We should permit users to // expect particular types, but until we make that explicit, we need to // open any kind of file when requested let create_options = CreateOptions( FILE_SYNCHRONOUS_IO_NONALERT | if let Some(CreateOptions(custom_options)) = self.create_options { custom_options } else if matches!(self.follow, Some(false)) || self.create_new { // Follow is disabled, or create_new, which for unix is == // O_EXCL | O_CREAT and defined as rejecting a symlink at // the target path. FILE_OPEN_REPARSE_POINT } else { 0 }, ); #[cfg(feature = "log")] log::trace!( "open_at: {}, access: {:#0x?} create_options: {:#0x?}", path.display(), desired_access.0, create_options.0 ); let open_symlink = if self.follow.unwrap_or(true) { OpenSymLink::OpenLinkFile } else { // Be compatible with Unix code - O_NOFOLLOW without O_PATH generates ELOOP. OpenSymLink::RaiseError(make_loop_error) }; self.do_create_file( f, path, desired_access, create_disposition, create_options, open_symlink, ) .map_err(|e| { if e.raw_os_error() == Some(ERROR_DIRECTORY as i32) { // NotADirectory happens when opening with FILE_OVERWRITE_IF // (e.g. truncate) a Symlink with link-type Dir and follow enabled. But // AlreadyExists is a better error consistent with Unix : the // NotADirectory error is leakage from the implementation of // symlinks on Windows. Here we need to retry io::Error::new(ErrorKind::AlreadyExists, e) } else { e } }) } pub fn open_dir_at(&self, f: &File, path: &Path) -> Result { let desired_access = self.get_open_dir_at_access_mode()?; let create_disposition = self.get_file_disposition(false)?; // TODO: create options needs to be controlled through OpenOptions too. // FILE_SYNCHRONOUS_IO_NONALERT is set by CreateFile with the options // Rust itself uses - this lets the OS position tracker work. It also // requires SYNCHRONIZE on the access mode. We should permit users to // expect particular types, but until we make that explicit, we need to // open any kind of file when requested # FILE_NON_DIRECTORY_FILE | let create_options = CreateOptions( FILE_SYNCHRONOUS_IO_NONALERT | if let Some(CreateOptions(custom_options)) = self.create_options { custom_options } else if matches!(self.follow, Some(true)) { 0 } else { FILE_OPEN_REPARSE_POINT }, ); #[cfg(feature = "log")] log::trace!( "open_dir_at: {}, access: {:#0x?} create_options: {:#0x?}", path.display(), desired_access.0, create_options.0 ); let open_symlink = { // Be compatible with Unix code - O_NOFOLLOW without O_PATH generates ELOOP. OpenSymLink::RaiseError(make_loop_error) }; self.do_create_file( f, path, desired_access, create_disposition, create_options, open_symlink, ) } pub fn open_path_at(&self, f: &File, path: &Path) -> Result { let desired_access = DesiredAccess( SYNCHRONIZE | if let Some(DesiredAccess(custom_access)) = self.desired_access { custom_access } else { 0 }, ); let create_disposition = self.get_file_disposition(false)?; // TODO: create options needs to be controlled through OpenOptions too. // FILE_SYNCHRONOUS_IO_NONALERT is set by CreateFile with the options // Rust itself uses - this lets the OS position tracker work. It also // requires SYNCHRONIZE on the access mode. let create_options = CreateOptions( FILE_SYNCHRONOUS_IO_NONALERT | if let Some(CreateOptions(custom_options)) = self.create_options { custom_options } else { FILE_OPEN_REPARSE_POINT }, ); #[cfg(feature = "log")] log::trace!( "open_path_at: {}, access: {:#0x?} create_options: {:#0x?}", path.display(), desired_access.0, create_options.0 ); self.do_create_file( f, path, desired_access, create_disposition, create_options, open_link_file(), ) } pub fn symlink_at( &self, d: &File, linkname: &Path, link_entry_type: LinkEntryType, target: &Path, ) -> Result<()> { // 1 - create a plain old file/dir atomically. let link_file = match link_entry_type { LinkEntryType::Dir => OpenOptions::default() .create_new(true) .write(OpenOptionsWriteMode::Write) .mkdir_at(d, linkname), LinkEntryType::File => OpenOptions::default() .create_new(true) .write(OpenOptionsWriteMode::Write) .open_at(d, linkname), LinkEntryType::Other => unimplemented!("can't create reparse points [yet["), }?; // 2 - convert it to a symlink // Symlinks can be absolute or relative. The discriminator rules are not // clear for this, but it seems like we want the following for a // absolute path // - no dependence on implicit state like 'current working directory on // drive X'. //is_absolute is perhaps good enough. Ultimately callers of this need to // provide reasonable working data. let os_target = target.as_os_str().to_owned(); let mut final_target = OsString::new(); let absolute = target.is_absolute(); // target might not start with \??\. if absolute && os_target.encode_wide().take(4).collect::>() != [0x005C, 0x003F, 0x003F, 0x005C] { // prefix target with \??\ final_target.push(r"\??\"); } final_target.push(&os_target); // Symlink needs two strings: print (e.g. d:\foo) and substitute (e.g. // \??\d:\foo) for print string we take the supplied path. For // substitute, final_target. // TODO: make this more like zero-copy. let print_path = os_target.encode_wide().collect::>(); let subst_path = final_target.encode_wide().collect::>(); let path_length = print_path.len() + subst_path.len(); // Size of the union, -1 for the 1 byte in-struct array, + path lengths. let reparse_data_length = mem::size_of::() - 1 + path_length * 2; // u32 + USHORT*2 let reparse_length = reparse_data_length + 8; let mut reparse_data_vec: Vec = vec![0; reparse_length]; // todo alignment safety: calculate the size in multiples of // REPARSE_DATA_BUFFER.len and then cast down. let (head, aligned, _tail) = unsafe { reparse_data_vec.align_to_mut::() }; if !head.is_empty() { // TODO: use body instead later on? return Err(io::Error::new( io::ErrorKind::Other, "non-aligned struct allocation", )); } let reparse_data = &mut aligned[0]; reparse_data.ReparseTag = IO_REPARSE_TAG_SYMLINK; let to_u16 = |l| { TryFrom::try_from(l) .map_err(|_e| io::Error::new(io::ErrorKind::Other, "path length too long")) }; reparse_data.ReparseDataLength = to_u16(reparse_data_length)?; if !absolute { reparse_data.u.SymbolicLinkReparseBuffer.Flags = SYMLINK_FLAG_RELATIVE; } reparse_data .u .SymbolicLinkReparseBuffer .SubstituteNameLength = to_u16(subst_path.len() * 2)?; reparse_data .u .SymbolicLinkReparseBuffer .SubstituteNameOffset = 0; reparse_data.u.SymbolicLinkReparseBuffer.PrintNameLength = to_u16(print_path.len() * 2)?; reparse_data.u.SymbolicLinkReparseBuffer.PrintNameOffset = to_u16(subst_path.len() * 2)?; let path_addr = unsafe { reparse_data.u.SymbolicLinkReparseBuffer.PathBuffer.as_ptr() as *const u8 }; let path_offset = unsafe { path_addr.offset_from(aligned.as_ptr() as *const u8) } as usize; // copy the strings in: let print_path_u8 = unsafe { std::slice::from_raw_parts(print_path.as_ptr().cast::(), print_path.len() * 2) }; let subst_path_u8 = unsafe { std::slice::from_raw_parts(subst_path.as_ptr().cast::(), subst_path.len() * 2) }; reparse_data_vec[path_offset..path_offset + subst_path.len() * 2] .copy_from_slice(subst_path_u8); reparse_data_vec[path_offset + subst_path.len() * 2 ..path_offset + subst_path.len() * 2 + print_path.len() * 2] .copy_from_slice(print_path_u8); let bool_result = unsafe { DeviceIoControl( link_file.as_raw_handle() as HANDLE, FSCTL_SET_REPARSE_POINT, reparse_data_vec.as_ptr() as *const c_void, reparse_data_vec.len() as u32, ptr::null_mut(), 0, ptr::null_mut(), ptr::null_mut(), ) }; let r = cvt::cvt(bool_result).map(|_v| ()); #[cfg(feature = "workaround-procmon")] return r.or_else(|e| procmon::workaround(e, ())); #[cfg(not(feature = "workaround-procmon"))] return r; } pub fn rmdir_at(&self, f: &File, p: &Path) -> Result<()> { // we must open a directory self.mark_for_deletion(f, p, CreateOptions(FILE_DIRECTORY_FILE)) } pub fn unlink_at(&self, f: &File, p: &Path) -> Result<()> { self.mark_for_deletion(f, p, CreateOptions(0)) } fn mark_for_deletion(&self, f: &File, p: &Path, create_options: CreateOptions) -> Result<()> { let desired_access = DesiredAccess(DELETE); let create_disposition = FileOpenDisposition(FILE_OPEN); // Only delete what was named :- do not do link processing let create_options = CreateOptions(create_options.0 | FILE_OPEN_REPARSE_POINT); let open_symlink = open_link_file(); let to_remove = self.do_create_file( f, p, desired_access, create_disposition, create_options, open_symlink, )?; to_remove.delete_by_handle().map_err(|(_, e)| e) } fn is_symlink(handle: HANDLE) -> Result { let mut reparse_buffer: Aligned< A8, [MaybeUninit; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize], > = Aligned([MaybeUninit::::uninit(); MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize]); let mut out_size = 0; let bool_result = unsafe { DeviceIoControl( handle, FSCTL_GET_REPARSE_POINT, ptr::null(), 0, // output buffer reparse_buffer.as_mut_ptr().cast(), // size of output buffer MAXIMUM_REPARSE_DATA_BUFFER_SIZE, // number of bytes returned &mut out_size, // OVERLAPPED structure ptr::null_mut(), ) }; let result = cvt::cvt(bool_result); if let Err(e) = result { if e.raw_os_error() != Some(ERROR_NOT_A_REPARSE_POINT as i32) { // This is ugly. But procmon seems to interfere. #[cfg(feature = "workaround-procmon")] return procmon::workaround(e, false); #[cfg(not(feature = "workaround-procmon"))] return Err(e); } return Ok(false); }; if out_size < size_of::() as u32 { // Success but not enough data to read the tag return Err(io::Error::new( io::ErrorKind::Other, "Insufficient data from DeviceIOControl", )); } let reparse_buffer = reparse_buffer.as_ptr().cast::(); Ok(unsafe { matches!( (*reparse_buffer).ReparseTag, IO_REPARSE_TAG_SYMLINK | IO_REPARSE_TAG_MOUNT_POINT ) }) } fn get_file_disposition(&self, call_defaults_create: bool) -> Result { if self.create_new { Ok(FileOpenDisposition(FILE_CREATE)) } else if self.truncate { Ok(FileOpenDisposition(FILE_OVERWRITE_IF)) } else if self.create { Ok(FileOpenDisposition(FILE_OPEN_IF)) } else if call_defaults_create { // mkdir should still work without create / truncate called - // its poor ergonomics otherwise. Ok(FileOpenDisposition(FILE_CREATE)) } else { // just open the existing file. Ok(FileOpenDisposition(FILE_OPEN)) } } fn get_open_at_access_mode(&self) -> Result { // FILE_SYNCHRONOUS_IO_NONALERT is set by CreateFile with the options // Rust itself uses - this lets the OS position tracker work. It also // requires SYNCHRONIZE on the access mode. let mut desired_access = SYNCHRONIZE; if let Some(DesiredAccess(custom_access)) = self.desired_access { return Ok(DesiredAccess(custom_access | desired_access)); } if self.read { desired_access |= FILE_GENERIC_READ; } // rust has match (self.read, self.write, self.append, self.access_mode) { desired_access |= match (self.write, None) { (.., Some(mode)) => mode, (OpenOptionsWriteMode::Write, None) => FILE_GENERIC_WRITE, (OpenOptionsWriteMode::Append, None) => FILE_GENERIC_WRITE & !FILE_WRITE_DATA, _ => 0, }; if desired_access == SYNCHRONIZE { // neither read nor write modes selected return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)); } Ok(DesiredAccess(desired_access)) } fn get_open_dir_at_access_mode(&self) -> Result { // FILE_SYNCHRONOUS_IO_NONALERT is set by CreateFile with the options // Rust itself uses - this lets the OS position tracker work. It also // requires SYNCHRONIZE on the access mode. let mut desired_access = SYNCHRONIZE; if let Some(DesiredAccess(custom_access)) = self.desired_access { return Ok(DesiredAccess(custom_access | desired_access)); } if self.read { desired_access |= FILE_READ_ATTRIBUTES | FILE_LIST_DIRECTORY | FILE_TRAVERSE; } // rust has match (self.read, self.write, self.append, self.access_mode) { desired_access |= match (self.write, None) { (.., Some(mode)) => mode, (OpenOptionsWriteMode::Write, None) => FILE_WRITE_ATTRIBUTES | DELETE, (OpenOptionsWriteMode::Append, None) => FILE_WRITE_ATTRIBUTES | DELETE, _ => 0, }; if desired_access == SYNCHRONIZE { // neither read nor write modes selected return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)); } Ok(DesiredAccess(desired_access)) } fn with_security_qos(&mut self, mutator: F) where F: FnOnce(&mut SECURITY_QUALITY_OF_SERVICE), { if self.security_qos.is_none() { self.security_qos = Some(unsafe { zeroed::() }).map(|mut qos| { qos.Length = size_of::() as u32; qos }); } self.security_qos = self.security_qos.map(|mut qos| { mutator(&mut qos); qos }); } } /// Extends OpenOptions with Windows specific parameters. /// /// Note that `open_at` uses `NTCreateFile`, not `CreateFile` and as such the /// flags and attributes values differ. pub trait OpenOptionsExt { /** Set the AllocationSize parameter to NTCreateFile. Only takes effect when creating a file. ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.allocation_size(12345); options.read(true); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn allocation_size(&mut self, val: i64) -> &mut Self; /** Set the DesiredAccess parameter to NTCreateFile for `open_at()`. Mostly overrides the parameter, giving caller control. In order to work with the IO model provided today, SYNCHRONIZE is always included. This is an implementation detail and could change in future. ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS,FILE_READ_ATTRIBUTES}; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.desired_access(FILE_READ_ATTRIBUTES); let child_file = options.open_at(&parent_dir, "child").unwrap(); child_file.metadata(); ``` */ fn desired_access(&mut self, desired_access: u32) -> &mut Self; /** Set the CreateOptions parameter to NTCreateFile for `open_at()`. Mostly overrides the parameter, giving callers detailed control. This causes methods such as `follow` to have no effect. In order to work with the IO model provided today, FILE_SYNCHRONOUS_IO_NONALERT is always included. This is an implementation detail and could change in future. ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::{FILE_FLAG_BACKUP_SEMANTICS,FILE_READ_ATTRIBUTES}; use windows_sys::Win32::System::WindowsProgramming::FILE_NO_EA_KNOWLEDGE; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.create_options(FILE_NO_EA_KNOWLEDGE); let child_file = options.open_at(&parent_dir, "child"); ``` */ fn create_options(&mut self, create_options: u32) -> &mut Self; /** Set the FileAttributes field used with NTCreateFile. When this is not called, fs_at uses FILE_ATTRIBUTE_NORMAL [Microsoft API documentation](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#dwFlagsAndAttributes) - see the dwFlagsAndAttributes values. ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::{FILE_ATTRIBUTE_TEMPORARY, FILE_FLAG_BACKUP_SEMANTICS}; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.read(true); options.file_attributes(FILE_ATTRIBUTE_TEMPORARY); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn file_attributes(&mut self, val: u32) -> &mut Self; /** Set the Attributes field of the ObjectAttributes parameter to NTCreateFile. ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; use windows_sys::Win32::System::Kernel::OBJ_CASE_INSENSITIVE; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.read(true); options.object_attributes(OBJ_CASE_INSENSITIVE as u32); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` The default behaviour requests case sensitive behaviour from Windows, but due to Windows kernel behaviour unless explicit configuration outside of the scope of this crate has been done, case preserving case insensitive semantics will always apply. */ fn object_attributes(&mut self, val: u32) -> &mut Self; /** Set the SecurityDescriptor field of the ObjectsAttributes parameter to NTCreateFile. This is optional, but allows for fine grained control if needed. [Microsoft API documentation](https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptors) */ fn security_descriptor(&mut self, descriptor: SECURITY_DESCRIPTOR) -> &mut Self; /** Set the SecurityQualityOfService ImpersonationLevel field of the ObjectsAttributes parameter to NTCreateFile. This **should** be set if working with named pipes - or if users can control the paths that your process will open. [Microsoft API documentation](https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_quality_of_service) ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::{Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS, Security::SecurityIdentification}; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS as u32); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.read(true); options.security_qos_impersonation(SecurityIdentification as u32); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn security_qos_impersonation(&mut self, level: u32) -> &mut Self; /** Set the SecurityQualityOfService ContextTrackingMode field of the ObjectsAttributes parameter to NTCreateFile. [Microsoft API documentation](https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_quality_of_service) ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::{Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS, Security::SECURITY_DYNAMIC_TRACKING}; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.read(true); options.security_qos_context_tracking(SECURITY_DYNAMIC_TRACKING); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn security_qos_context_tracking(&mut self, mode: SECURITY_CONTEXT_TRACKING_MODE) -> &mut Self; /** Set the SecurityQualityOfService EffectiveOnly field of the ObjectsAttributes parameter to NTCreateFile. [Microsoft API documentation](https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_quality_of_service) ```no_run use std::fs; use std::os::windows::fs::OpenOptionsExt as StdOpenOptionsExt; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; use fs_at::OpenOptions; use fs_at::os::windows::OpenOptionsExt; let mut options = fs::OpenOptions::new(); options.read(true); options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); let mut parent_dir = options.open(".").unwrap(); let mut options = OpenOptions::default(); options.read(true); options.security_qos_effective_only(true); let dir_file = options.mkdir_at(&mut parent_dir, "foo"); ``` */ fn security_qos_effective_only(&mut self, effective_only: bool) -> &mut Self; } impl OpenOptionsExt for OpenOptions { fn allocation_size(&mut self, val: i64) -> &mut Self { self._impl.allocation_size = val; self } fn create_options(&mut self, create_options: u32) -> &mut Self { self._impl.create_options = Some(CreateOptions(create_options)); self } fn desired_access(&mut self, desired_access: u32) -> &mut Self { self._impl.desired_access = Some(DesiredAccess(desired_access)); self } fn file_attributes(&mut self, val: u32) -> &mut Self { self._impl.file_attributes = val; self } fn object_attributes(&mut self, val: u32) -> &mut Self { self._impl.object_attributes = val; self } fn security_descriptor(&mut self, descriptor: SECURITY_DESCRIPTOR) -> &mut Self { self._impl.security_descriptor = Some(descriptor); self } fn security_qos_impersonation(&mut self, level: u32) -> &mut Self { self._impl .with_security_qos(|qos| qos.ImpersonationLevel = level as i32); self } fn security_qos_context_tracking(&mut self, mode: SECURITY_CONTEXT_TRACKING_MODE) -> &mut Self { self._impl .with_security_qos(|qos| qos.ContextTrackingMode = mode); self } fn security_qos_effective_only(&mut self, effective_only: bool) -> &mut Self { let native_value = u8::from(effective_only); self._impl .with_security_qos(|qos| qos.EffectiveOnly = native_value); self } } /// Extends `File` with Windows capabilities. pub trait FileExt { // Deletes the file by the open handle. This will attempt to use posix // semantics, if that fails with an appropriate error code, it will then // attempt win7 deletion semantics, and if that fails with access denied, it // will attempt to remove the readonly attribute, mark the file for // deletion, and finally restore the attribute (for correctness with // hardlinked files). // // On unhandled errors, the file is returned along with the error, to permit // alternative code paths by the caller. fn delete_by_handle(self) -> std::result::Result<(), (File, io::Error)>; } fn delete_with_posix(f: File) -> std::result::Result { // Try for modern delete semantics: POSIX_SEMANTICS and bypass the // readonly flag. let mut delete_disposition = FILE_DISPOSITION_INFO_EX { Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS | FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, }; match cvt::cvt(unsafe { SetFileInformationByHandle( f.as_raw_handle() as HANDLE, FileDispositionInfoEx, &mut delete_disposition as *mut FILE_DISPOSITION_INFO_EX as *const c_void, mem::size_of::() as u32, ) }) { Ok(_) => Ok(f), Err(e) => Err((f, e)), } } fn delete_with_win7(f: File) -> std::result::Result { let mut delete_disposition = FILE_DISPOSITION_INFO { DeleteFile: TRUE as u8, }; match cvt::cvt(unsafe { SetFileInformationByHandle( f.as_raw_handle() as HANDLE, FileDispositionInfo, &mut delete_disposition as *mut FILE_DISPOSITION_INFO as *const c_void, mem::size_of::() as u32, ) }) { Ok(_) => Ok(f), Err(e) => Err((f, e)), } } fn delete_with_win7_readonly( f: File, e: io::Error, ) -> std::result::Result { // 1) reset readonly attribute let m = match f.metadata() { Ok(m) => m, Err(e) => return Err((f, e)), }; if !m.permissions().readonly() { return Err((f, e)); } let mut info = FILE_BASIC_INFO { FileAttributes: m.file_attributes() & !FILE_ATTRIBUTE_READONLY, CreationTime: m.creation_time() as _, LastAccessTime: m.last_access_time() as _, LastWriteTime: m.last_write_time() as _, ChangeTime: 0, }; match cvt::cvt(unsafe { SetFileInformationByHandle( f.as_raw_handle() as HANDLE, FileBasicInfo, &mut info as *mut FILE_BASIC_INFO as *mut _, size_of::() as u32, ) }) { Ok(_) => (), Err(e) => return Err((f, e)), }; // 2) mark for deletion let f = delete_with_win7(f)?; // 3) reapply readonly attribute info.FileAttributes |= FILE_ATTRIBUTE_READONLY; match cvt::cvt(unsafe { SetFileInformationByHandle( f.as_raw_handle() as HANDLE, FileBasicInfo, &mut info as *mut FILE_BASIC_INFO as *mut _, size_of::() as u32, ) }) { Ok(_) => Ok(f), Err(e) => Err((f, e)), } } impl FileExt for File { fn delete_by_handle(self) -> std::result::Result<(), (File, io::Error)> { match delete_with_posix(self) .or_else(|(f, e)| { match e.raw_os_error().map(|i| i as u32) { Some(ERROR_NOT_SUPPORTED) | Some(ERROR_INVALID_PARAMETER) | Some(ERROR_INVALID_FUNCTION) => { // failed and looks like a compatibility issue, try deleting with windows 7 compatible logic delete_with_win7(f) } _ => Err((f, e)), } }) .or_else(|(f, e)| match e.kind() { // ACCESSDENIED may mean 'file was readonly'. ErrorKind::PermissionDenied => delete_with_win7_readonly(f, e), _ => Err((f, e)), }) { Ok(f) => { // Make it explicit that we're dropping the handle, as that can // cause IO and it makes profiling easier to have a single // callsite to instrument etc. mem::drop(f); Ok(()) } // return the file handle back so the user can take alternative // action if desired. Err((f, e)) => Err((f, e)), } } } #[derive(Debug)] pub(crate) struct ReadDirImpl<'a> { /// FILE_ID_BOTH_DIR_INFO is a variable-length struct, otherwise this would /// be a vec of that. None indicates end of iterator from the OS. buffer: Option>, d: &'a mut File, // byte offset in buffer to next entry to yield offset: usize, } impl<'a> ReadDirImpl<'a> { pub fn new(d: &mut File) -> Result { let mut result = ReadDirImpl { // Start with a page, can always grow it statically or dynamically if // needed. buffer: Some(vec![0_u8; 4096]), d, offset: 0, }; // TODO: can this ever fail as FindFirstFile does? result.fill_buffer(FileIdBothDirectoryRestartInfo)?; Ok(result) } fn fill_buffer(&mut self, class: FILE_INFO_BY_HANDLE_CLASS) -> Result { let buffer = self.buffer.as_mut().ok_or_else(|| { io::Error::new( io::ErrorKind::Other, "Attempt to fill buffer after end of dir", ) })?; // Implement // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilea // without ever doing path resolution... the docs for // GetFileInformationByHandleEx do not mention how to detect end of dir, // but FindNextFile does: // // ``` //If the function fails because no more matching files can be found, //the GetLastError function returns ERROR_NO_MORE_FILES. // ``` let result = cvt::cvt(unsafe { GetFileInformationByHandleEx( self.d.as_raw_handle() as HANDLE, class, buffer.as_mut_ptr() as *mut c_void, buffer.len() as u32, ) }); match result { Ok(_) => Ok(false), Err(e) if e.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) => Ok(true), Err(e) => Err(e), } } } impl Iterator for ReadDirImpl<'_> { type Item = Result; fn next(&mut self) -> Option { // if the buffer is empty, fill it; if the buffer is None, exit early. if self.offset >= self.buffer.as_ref()?.len() { match self.fill_buffer(FileIdBothDirectoryInfo) { Ok(false) => { self.offset = 0; } Ok(true) => { self.buffer = None; return None; } Err(e) => return Some(Err(e)), } } // offset is now valid. Dereference into a struct. let struct_mem = &self.buffer.as_ref()?[self.offset..]; let info = unsafe { &*struct_mem.as_ptr().cast::() }; self.offset = if info.NextEntryOffset == 0 { self.buffer.as_ref()?.len() } else { info.NextEntryOffset as usize + self.offset }; let name = OsString::from_wide(unsafe { slice::from_raw_parts( info.FileName.as_ptr(), info.FileNameLength as usize / size_of::(), ) }); Some(Ok(DirEntryImpl { name })) // // // Read Attributes, Delete, Synchronize // Disposition: Open // Options: Synchronous IO Non-Alert, Open Reparse Point // } } #[derive(Debug)] pub(crate) struct DirEntryImpl { name: OsString, } impl DirEntryImpl { pub fn name(&self) -> &OsStr { &self.name } } #[cfg(test)] mod tests { use std::{fs::rename, io::Result}; use tempfile::TempDir; use test_log::test; use windows_sys::Win32::System::Kernel::OBJ_CASE_INSENSITIVE; use crate::{os::windows::OpenOptionsExt, testsupport::open_dir, OpenOptions}; #[test] // #[should_panic(expected = "Cannot create a file when that file already exists.")] fn mkdir_at_case_insensitive() -> Result<()> { // This tests that when case insensitivity is enabled, making a // colliding dir fails - but we have no way to easily/reliably turn case // insensitivity off for now. So its a bit unnecessary. let tmp = TempDir::new()?; let parent = tmp.path().join("parent"); let renamed_parent = tmp.path().join("renamed-parent"); std::fs::create_dir(&parent)?; let parent_file = open_dir(&parent)?; rename(parent, renamed_parent)?; let mut create_opt = OpenOptions::default(); create_opt.create(true); create_opt.mkdir_at(&parent_file, "child")?; create_opt.object_attributes(OBJ_CASE_INSENSITIVE as u32); // Incorrectly passes because we're just using .create() now create_opt.mkdir_at(&parent_file, "Child")?; Ok(()) } }