├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── relative-path-utils ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── glob │ │ ├── mod.rs │ │ └── tests.rs │ ├── lib.rs │ └── root │ │ ├── mod.rs │ │ ├── unix.rs │ │ └── windows.rs └── tests │ └── root.rs └── relative-path ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── lib.rs ├── path_ext.rs ├── relative_path_buf.rs └── tests.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '48 13 * * 1' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | msrv: 17 | runs-on: ${{matrix.os}} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, windows-latest, macos-latest] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: dtolnay/rust-toolchain@1.66 25 | - run: cargo build --workspace --all-features 26 | 27 | test: 28 | runs-on: ${{matrix.os}} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [ubuntu-latest, windows-latest, macos-latest] 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | - uses: Swatinem/rust-cache@v2 37 | - run: cargo test --workspace --all-targets --all-features 38 | - run: cargo test --workspace --doc --all-features 39 | 40 | build_feature: 41 | runs-on: ubuntu-latest 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | feature: ["", serde, alloc, "serde,alloc", std, "alloc,std"] 46 | env: 47 | RUSTFLAGS: -D warnings 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@1.66 51 | with: 52 | components: clippy 53 | - uses: Swatinem/rust-cache@v2 54 | - run: cargo check -p relative-path --no-default-features --features "${{matrix.feature}}" 55 | - run: cargo clippy -p relative-path --no-default-features --features "${{matrix.feature}}" 56 | 57 | build_utils_feature: 58 | runs-on: ubuntu-latest 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | feature: ["", root, alloc, "root,alloc", std, "alloc,std"] 63 | env: 64 | RUSTFLAGS: -D warnings 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: dtolnay/rust-toolchain@1.66 68 | with: 69 | components: clippy 70 | - uses: Swatinem/rust-cache@v2 71 | - run: cargo check -p relative-path-utils --no-default-features --features "${{matrix.feature}}" 72 | - run: cargo clippy -p relative-path-utils --no-default-features --features "${{matrix.feature}}" 73 | 74 | clippy: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: dtolnay/rust-toolchain@stable 79 | with: 80 | components: clippy 81 | - uses: Swatinem/rust-cache@v2 82 | - run: cargo clippy --workspace --all-features --all-targets -- -D warnings 83 | 84 | docs: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v4 88 | - uses: dtolnay/rust-toolchain@nightly 89 | - uses: Swatinem/rust-cache@v2 90 | - run: cargo doc -p relative-path --all-features 91 | env: 92 | RUSTFLAGS: --cfg relative_path_docsrs 93 | RUSTDOCFLAGS: --cfg relative_path_docsrs 94 | 95 | rustfmt: 96 | runs-on: ubuntu-latest 97 | steps: 98 | - uses: actions/checkout@v4 99 | - uses: dtolnay/rust-toolchain@stable 100 | with: 101 | components: rustfmt 102 | - run: cargo fmt --check --all 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "relative-path", 5 | "relative-path-utils", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # relative-path 2 | 3 | [github](https://github.com/udoprog/relative-path) 4 | [crates.io](https://crates.io/crates/relative-path) 5 | [docs.rs](https://docs.rs/relative-path) 6 | [build status](https://github.com/udoprog/relative-path/actions?query=branch%3Amain) 7 | 8 | Portable relative UTF-8 paths for Rust. 9 | 10 | This crate provides a module analogous to [`std::path`], with the following 11 | characteristics: 12 | 13 | * The path separator is set to a fixed character (`/`), regardless of 14 | platform. 15 | * Relative paths cannot represent a path in the filesystem without first 16 | specifying *what they are relative to* using functions such as [`to_path`] 17 | and [`to_logical_path`]. 18 | * Relative paths are always guaranteed to be valid UTF-8 strings. 19 | 20 | On top of this we support many operations that guarantee the same behavior 21 | across platforms. 22 | 23 | For more utilities to manipulate relative paths, see the 24 | [`relative-path-utils` crate]. 25 | 26 |
27 | 28 | ## Usage 29 | 30 | Add `relative-path` to your `Cargo.toml`: 31 | 32 | ```toml 33 | relative-path = "2.0.1" 34 | ``` 35 | 36 | Start using relative paths: 37 | 38 | ```rust 39 | use serde::{Serialize, Deserialize}; 40 | use relative_path::RelativePath; 41 | 42 | #[derive(Serialize, Deserialize)] 43 | struct Manifest<'a> { 44 | #[serde(borrow)] 45 | source: &'a RelativePath, 46 | } 47 | 48 | ``` 49 | 50 |
51 | 52 | ## Serde Support 53 | 54 | This library includes serde support that can be enabled with the `serde` 55 | feature. 56 | 57 |
58 | 59 | ## Why is `std::path` a portability hazard? 60 | 61 | Path representations differ across platforms. 62 | 63 | * Windows permits using drive volumes (multiple roots) as a prefix (e.g. 64 | `"c:\"`) and backslash (`\`) as a separator. 65 | * Unix references absolute paths from a single root and uses forward slash 66 | (`/`) as a separator. 67 | 68 | If we use `PathBuf`, Storing paths in a manifest would allow our application 69 | to build and run on one platform but potentially not others. 70 | 71 | Consider the following data model and corresponding toml for a manifest: 72 | 73 | ```rust 74 | use std::path::PathBuf; 75 | 76 | use serde::{Serialize, Deserialize}; 77 | 78 | #[derive(Serialize, Deserialize)] 79 | struct Manifest { 80 | source: PathBuf, 81 | } 82 | ``` 83 | 84 | ```toml 85 | source = "C:\\Users\\udoprog\\repo\\data\\source" 86 | ``` 87 | 88 | This will run for you (assuming `source` exists). So you go ahead and check 89 | the manifest into git. The next day your Linux colleague calls you and 90 | wonders what they have ever done to wrong you? 91 | 92 | So what went wrong? Well two things. You forgot to make the `source` 93 | relative, so anyone at the company which has a different username than you 94 | won't be able to use it. So you go ahead and fix that: 95 | 96 | ```toml 97 | source = "data\\source" 98 | ``` 99 | 100 | But there is still one problem! A backslash (`\`) is only a legal path 101 | separator on Windows. Luckily you learn that forward slashes are supported 102 | both on Windows *and* Linux. So you opt for: 103 | 104 | ```toml 105 | source = "data/source" 106 | ``` 107 | 108 | Things are working now. So all is well... Right? Sure, but we can do better. 109 | 110 | This crate provides types that work with *portable relative paths* (hence 111 | the name). So by using [`RelativePath`] we can systematically help avoid 112 | portability issues like the one above. Avoiding issues at the source is 113 | preferably over spending 5 minutes of onboarding time on a theoretical 114 | problem, hoping that your new hires will remember what to do if they ever 115 | encounter it. 116 | 117 | Using [`RelativePathBuf`] we can fix our data model like this: 118 | 119 | ```rust 120 | use relative_path::RelativePathBuf; 121 | use serde::{Serialize, Deserialize}; 122 | 123 | #[derive(Serialize, Deserialize)] 124 | pub struct Manifest { 125 | source: RelativePathBuf, 126 | } 127 | ``` 128 | 129 | And where it's used: 130 | 131 | ```rust 132 | use std::fs; 133 | use std::env::current_dir; 134 | 135 | let manifest: Manifest = todo!(); 136 | 137 | let root = current_dir()?; 138 | let source = manifest.source.to_path(&root); 139 | let content = fs::read(&source)?; 140 | ``` 141 | 142 |
143 | 144 | ## Overview 145 | 146 | Conversion to a platform-specific [`Path`] happens through the [`to_path`] 147 | and [`to_logical_path`] functions. Where you are required to specify the 148 | path that prefixes the relative path. This can come from a function such as 149 | [`std::env::current_dir`]. 150 | 151 | ```rust 152 | use std::env::current_dir; 153 | use std::path::Path; 154 | 155 | use relative_path::RelativePath; 156 | 157 | let root = current_dir()?; 158 | 159 | // to_path unconditionally concatenates a relative path with its base: 160 | let relative_path = RelativePath::new("../foo/./bar"); 161 | let full_path = relative_path.to_path(&root); 162 | assert_eq!(full_path, root.join("..\\foo\\.\\bar")); 163 | 164 | // to_logical_path tries to apply the logical operations that the relative 165 | // path corresponds to: 166 | let relative_path = RelativePath::new("../foo/./bar"); 167 | let full_path = relative_path.to_logical_path(&root); 168 | 169 | // Replicate the operation performed by `to_logical_path`. 170 | let mut parent = root.clone(); 171 | parent.pop(); 172 | assert_eq!(full_path, parent.join("foo\\bar")); 173 | ``` 174 | 175 | When two relative paths are compared to each other, their exact component 176 | makeup determines equality. 177 | 178 | ```rust 179 | use relative_path::RelativePath; 180 | 181 | assert_ne!( 182 | RelativePath::new("foo/bar/../baz"), 183 | RelativePath::new("foo/baz") 184 | ); 185 | ``` 186 | 187 | Using platform-specific path separators to construct relative paths is not 188 | supported. 189 | 190 | Path separators from other platforms are simply treated as part of a 191 | component: 192 | 193 | ```rust 194 | use relative_path::RelativePath; 195 | 196 | assert_ne!( 197 | RelativePath::new("foo/bar"), 198 | RelativePath::new("foo\\bar") 199 | ); 200 | 201 | assert_eq!(1, RelativePath::new("foo\\bar").components().count()); 202 | assert_eq!(2, RelativePath::new("foo/bar").components().count()); 203 | ``` 204 | 205 | To see if two relative paths are equivalent you can use [`normalize`]: 206 | 207 | ```rust 208 | use relative_path::RelativePath; 209 | 210 | assert_eq!( 211 | RelativePath::new("foo/bar/../baz").normalize(), 212 | RelativePath::new("foo/baz").normalize(), 213 | ); 214 | ``` 215 | 216 |
217 | 218 | ## Additional portability notes 219 | 220 | While relative paths avoid the most egregious portability issue, that 221 | absolute paths will work equally unwell on all platforms. We cannot avoid 222 | all. This section tries to document additional portability hazards that we 223 | are aware of. 224 | 225 | [`RelativePath`], similarly to [`Path`], makes no guarantees that its 226 | constituent components make up legal file names. While components are 227 | strictly separated by slashes, we can still store things in them which may 228 | not be used as legal paths on all platforms. 229 | 230 | * A `NUL` character is not permitted on unix platforms - this is a 231 | terminator in C-based filesystem APIs. Slash (`/`) is also used as a path 232 | separator. 233 | * Windows has a number of [reserved characters and names][windows-reserved] 234 | (like `CON`, `PRN`, and `AUX`) which cannot legally be part of a 235 | filesystem component. 236 | * Windows paths are [case-insensitive by default][windows-case]. So, 237 | `Foo.txt` and `foo.txt` are the same files on windows. But they are 238 | considered different paths on most unix systems. 239 | 240 | A relative path that *accidentally* contains a platform-specific components 241 | will largely result in a nonsensical paths being generated in the hope that 242 | they will fail fast during development and testing. 243 | 244 | ```rust 245 | use relative_path::{RelativePath, PathExt}; 246 | use std::path::Path; 247 | 248 | if cfg!(windows) { 249 | assert_eq!( 250 | Path::new("foo\\c:\\bar\\baz"), 251 | RelativePath::new("c:\\bar\\baz").to_path("foo") 252 | ); 253 | } 254 | 255 | if cfg!(unix) { 256 | assert_eq!( 257 | Path::new("foo/bar/baz"), 258 | RelativePath::new("/bar/baz").to_path("foo") 259 | ); 260 | } 261 | 262 | assert_eq!( 263 | Path::new("foo").relative_to("bar")?, 264 | RelativePath::new("../foo"), 265 | ); 266 | ``` 267 | 268 | [`None`]: https://doc.rust-lang.org/std/option/enum.Option.html 269 | [`normalize`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.normalize 270 | [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html 271 | [`RelativePath`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html 272 | [`RelativePathBuf`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePathBuf.html 273 | [`std::env::current_dir`]: https://doc.rust-lang.org/std/env/fn.current_dir.html 274 | [`std::path`]: https://doc.rust-lang.org/std/path/index.html 275 | [`to_logical_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_logical_path 276 | [`to_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_path 277 | [windows-reserved]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 278 | [windows-case]: https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity 279 | [`relative-path-utils` crate]: https://docs.rs/relative-path-utils 280 | -------------------------------------------------------------------------------- /relative-path-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "relative-path-utils" 3 | version = "0.3.1" 4 | authors = ["John-John Tedro "] 5 | edition = "2021" 6 | rust-version = "1.66" 7 | description = "Portable, relative paths for Rust." 8 | documentation = "https://docs.rs/relative-path" 9 | readme = "README.md" 10 | homepage = "https://github.com/udoprog/relative-path" 11 | repository = "https://github.com/udoprog/relative-path" 12 | license = "MIT OR Apache-2.0" 13 | keywords = ["path"] 14 | categories = ["filesystem"] 15 | 16 | [lints.rust] 17 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(relative_path_docsrs)'] } 18 | 19 | [features] 20 | default = ["root"] 21 | alloc = [] 22 | std = [] 23 | root = ["alloc", "std", "dep:windows-sys", "dep:libc", "dep:errno"] 24 | 25 | [dependencies] 26 | relative-path = { path = "../relative-path", version = "2.0.1", default-features = false, features = ["alloc"] } 27 | 28 | [target.'cfg(windows)'.dependencies.windows-sys] 29 | version = "0.52.0" 30 | optional = true 31 | features = [ 32 | "Wdk_Foundation", 33 | "Wdk_Storage_FileSystem", 34 | "Wdk_System_SystemServices", 35 | "Win32_Foundation", 36 | "Win32_Storage_FileSystem", 37 | "Win32_System_IO", 38 | "Win32_System_WindowsProgramming", 39 | "Win32_Security", 40 | "Win32_System_Kernel", 41 | ] 42 | 43 | [target.'cfg(unix)'.dependencies] 44 | libc = { version = "0.2.151", optional = true } 45 | errno = { version = "0.3.8", optional = true } 46 | 47 | [dev-dependencies] 48 | anyhow = "1.0.76" 49 | serde = { version = "1.0.160", features = ["derive"] } 50 | 51 | [package.metadata.docs.rs] 52 | all-features = true 53 | rustdoc-args = ["--cfg", "relative_path_docsrs"] 54 | -------------------------------------------------------------------------------- /relative-path-utils/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /relative-path-utils/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /relative-path-utils/README.md: -------------------------------------------------------------------------------- 1 | # relative-path-utils 2 | 3 | [github](https://github.com/udoprog/relative-path) 4 | [crates.io](https://crates.io/crates/relative-path-utils) 5 | [docs.rs](https://docs.rs/relative-path-utils) 6 | [build status](https://github.com/udoprog/relative-path/actions?query=branch%3Amain) 7 | 8 | Utilities for working with relative paths. 9 | 10 | This crate contains: 11 | * [`Root`] the `root` feature - A root directory that can be used to open 12 | files relative to it. 13 | * [`Glob`] the `root` feature - A glob pattern that can be used to match 14 | files relative to a [`Root`]. 15 | 16 | [`Root`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Root.html 17 | [`Glob`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Glob.html 18 | -------------------------------------------------------------------------------- /relative-path-utils/src/glob/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | use core::fmt; 5 | use core::mem; 6 | 7 | use alloc::borrow::ToOwned; 8 | use alloc::boxed::Box; 9 | use alloc::collections::VecDeque; 10 | use alloc::vec::Vec; 11 | 12 | use std::io; 13 | 14 | use relative_path::{RelativePath, RelativePathBuf}; 15 | 16 | use crate::Root; 17 | 18 | type Result = std::result::Result; 19 | 20 | #[derive(Debug)] 21 | pub struct Error { 22 | kind: ErrorKind, 23 | } 24 | 25 | impl Error { 26 | fn new(kind: ErrorKind) -> Self { 27 | Self { kind } 28 | } 29 | } 30 | 31 | impl fmt::Display for Error { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | match &self.kind { 34 | ErrorKind::ReadDir(..) => write!(f, "Error reading directory"), 35 | ErrorKind::DirEntry(..) => write!(f, "Error getting directory entry"), 36 | } 37 | } 38 | } 39 | 40 | impl From for Error { 41 | #[inline] 42 | fn from(kind: ErrorKind) -> Self { 43 | Self::new(kind) 44 | } 45 | } 46 | 47 | #[derive(Debug)] 48 | enum ErrorKind { 49 | ReadDir(io::Error), 50 | DirEntry(io::Error), 51 | } 52 | 53 | impl std::error::Error for Error { 54 | #[allow(clippy::match_same_arms)] 55 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 56 | match &self.kind { 57 | ErrorKind::ReadDir(error) => Some(error), 58 | ErrorKind::DirEntry(error) => Some(error), 59 | } 60 | } 61 | } 62 | 63 | /// A compiled glob expression. 64 | /// 65 | /// This is returned by [`Root::glob`]. 66 | pub struct Glob<'a> { 67 | root: &'a Root, 68 | components: Vec>, 69 | } 70 | 71 | impl<'a> Glob<'a> { 72 | /// Construct a new glob pattern. 73 | pub(super) fn new(root: &'a Root, pattern: &'a RelativePath) -> Self { 74 | let components = compile_pattern(pattern); 75 | Self { root, components } 76 | } 77 | 78 | /// Construct a new matcher over the compiled glob pattern. 79 | /// 80 | /// # Examples 81 | /// 82 | /// ```no_run 83 | /// use relative_path_utils::Root; 84 | /// 85 | /// let root = Root::new("src")?; 86 | /// 87 | /// let glob = root.glob("**/*.rs"); 88 | /// 89 | /// let mut results = Vec::new(); 90 | /// 91 | /// for e in glob.matcher() { 92 | /// results.push(e?); 93 | /// } 94 | /// 95 | /// results.sort(); 96 | /// assert_eq!(results, vec!["lib.rs", "main.rs"]); 97 | /// # Ok::<_, Box>(()) 98 | /// ``` 99 | #[must_use] 100 | pub fn matcher(&self) -> Matcher<'_> { 101 | Matcher { 102 | root: self.root, 103 | queue: [(RelativePathBuf::new(), self.components.as_ref())] 104 | .into_iter() 105 | .collect(), 106 | } 107 | } 108 | } 109 | 110 | impl<'a> Matcher<'a> { 111 | /// Perform an expansion in the filesystem. 112 | fn expand_filesystem( 113 | &mut self, 114 | current: &RelativePath, 115 | rest: &'a [Component<'a>], 116 | mut m: M, 117 | ) -> Result<()> 118 | where 119 | M: FnMut(&str) -> bool, 120 | { 121 | if let Ok(m) = self.root.metadata(current) { 122 | if !m.is_dir() { 123 | return Ok(()); 124 | } 125 | } else { 126 | return Ok(()); 127 | } 128 | 129 | for e in self.root.read_dir(current).map_err(ErrorKind::ReadDir)? { 130 | let e = e.map_err(ErrorKind::DirEntry)?; 131 | let c = e.file_name(); 132 | let c = c.to_string_lossy(); 133 | 134 | if !m(c.as_ref()) { 135 | continue; 136 | } 137 | 138 | let mut new = current.to_owned(); 139 | new.push(c.as_ref()); 140 | self.queue.push_back((new, rest)); 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | /// Perform star star expansion. 147 | fn walk(&mut self, current: &RelativePathBuf, rest: &'a [Component<'a>]) -> Result<()> { 148 | self.queue.push_back((current.clone(), rest)); 149 | 150 | let mut queue = VecDeque::new(); 151 | queue.push_back(current.clone()); 152 | 153 | while let Some(current) = queue.pop_front() { 154 | let Ok(m) = self.root.metadata(¤t) else { 155 | continue; 156 | }; 157 | 158 | if !m.is_dir() { 159 | continue; 160 | } 161 | 162 | for e in self.root.read_dir(¤t).map_err(ErrorKind::ReadDir)? { 163 | let e = e.map_err(ErrorKind::DirEntry)?; 164 | let c = e.file_name(); 165 | let c = c.to_string_lossy(); 166 | let next = current.join(c.as_ref()); 167 | self.queue.push_back((next.clone(), rest)); 168 | queue.push_back(next); 169 | } 170 | } 171 | 172 | Ok(()) 173 | } 174 | } 175 | 176 | /// A matcher that can be iterated over for matched relative path buffers. 177 | pub struct Matcher<'a> { 178 | root: &'a Root, 179 | queue: VecDeque<(RelativePathBuf, &'a [Component<'a>])>, 180 | } 181 | 182 | impl Iterator for Matcher<'_> { 183 | type Item = Result; 184 | 185 | fn next(&mut self) -> Option { 186 | 'outer: loop { 187 | let (mut path, mut components) = self.queue.pop_front()?; 188 | 189 | while let [first, rest @ ..] = components { 190 | match first { 191 | Component::ParentDir => { 192 | path = path.join(relative_path::Component::ParentDir); 193 | } 194 | Component::Normal(normal) => { 195 | path = path.join(normal); 196 | } 197 | Component::Fragment(fragment) => { 198 | if let Err(e) = 199 | self.expand_filesystem(&path, rest, |name| fragment.is_match(name)) 200 | { 201 | return Some(Err(e)); 202 | } 203 | 204 | continue 'outer; 205 | } 206 | Component::StarStar => { 207 | if let Err(e) = self.walk(&path, rest) { 208 | return Some(Err(e)); 209 | } 210 | 211 | continue 'outer; 212 | } 213 | } 214 | 215 | components = rest; 216 | } 217 | 218 | return Some(Ok(path)); 219 | } 220 | } 221 | } 222 | 223 | #[derive(Clone)] 224 | enum Component<'a> { 225 | /// Parent directory. 226 | ParentDir, 227 | /// A normal component. 228 | Normal(&'a str), 229 | /// Normal component, compiled into a fragment. 230 | Fragment(Fragment<'a>), 231 | /// `**` component, which keeps expanding. 232 | StarStar, 233 | } 234 | 235 | fn compile_pattern(pattern: &RelativePath) -> Vec> { 236 | let mut output = Vec::new(); 237 | 238 | for c in pattern.components() { 239 | output.push(match c { 240 | relative_path::Component::CurDir => continue, 241 | relative_path::Component::ParentDir => Component::ParentDir, 242 | relative_path::Component::Normal("**") => Component::StarStar, 243 | relative_path::Component::Normal(normal) => { 244 | let fragment = Fragment::parse(normal); 245 | 246 | if let Some(normal) = fragment.as_literal() { 247 | Component::Normal(normal) 248 | } else { 249 | Component::Fragment(fragment) 250 | } 251 | } 252 | }); 253 | } 254 | 255 | output 256 | } 257 | 258 | #[derive(Debug, Clone, Copy)] 259 | enum Part<'a> { 260 | Star, 261 | Literal(&'a str), 262 | } 263 | 264 | /// A match fragment. 265 | #[derive(Debug, Clone)] 266 | pub(crate) struct Fragment<'a> { 267 | parts: Box<[Part<'a>]>, 268 | } 269 | 270 | impl<'a> Fragment<'a> { 271 | pub(crate) fn parse(string: &'a str) -> Fragment<'a> { 272 | let mut literal = true; 273 | let mut parts = Vec::new(); 274 | let mut start = None; 275 | 276 | for (n, c) in string.char_indices() { 277 | if c == '*' { 278 | if let Some(s) = start.take() { 279 | parts.push(Part::Literal(&string[s..n])); 280 | } 281 | 282 | if mem::take(&mut literal) { 283 | parts.push(Part::Star); 284 | } 285 | } else { 286 | if start.is_none() { 287 | start = Some(n); 288 | } 289 | 290 | literal = true; 291 | } 292 | } 293 | 294 | if let Some(s) = start { 295 | parts.push(Part::Literal(&string[s..])); 296 | } 297 | 298 | Fragment { 299 | parts: parts.into(), 300 | } 301 | } 302 | 303 | /// Test if the given string matches the current fragment. 304 | pub(crate) fn is_match(&self, string: &str) -> bool { 305 | let mut backtrack = VecDeque::new(); 306 | backtrack.push_back((self.parts.as_ref(), string)); 307 | 308 | while let Some((mut parts, mut string)) = backtrack.pop_front() { 309 | while let Some(part) = parts.first() { 310 | match part { 311 | Part::Star => { 312 | // Peek the next literal component. If we have a 313 | // trailing wildcard (which this constitutes) then it 314 | // is by definition a match. 315 | let Some(Part::Literal(peek)) = parts.get(1) else { 316 | return true; 317 | }; 318 | 319 | let Some(peek) = peek.chars().next() else { 320 | return true; 321 | }; 322 | 323 | while let Some(c) = string.chars().next() { 324 | if c == peek { 325 | backtrack.push_front(( 326 | parts, 327 | string.get(c.len_utf8()..).unwrap_or_default(), 328 | )); 329 | break; 330 | } 331 | 332 | string = string.get(c.len_utf8()..).unwrap_or_default(); 333 | } 334 | } 335 | Part::Literal(literal) => { 336 | // The literal component must be an exact prefix of the 337 | // current string. 338 | let Some(remainder) = string.strip_prefix(literal) else { 339 | return false; 340 | }; 341 | 342 | string = remainder; 343 | } 344 | } 345 | 346 | parts = parts.get(1..).unwrap_or_default(); 347 | } 348 | 349 | if string.is_empty() { 350 | return true; 351 | } 352 | } 353 | 354 | false 355 | } 356 | 357 | /// Treat the fragment as a single normal component. 358 | fn as_literal(&self) -> Option<&'a str> { 359 | if let [Part::Literal(one)] = self.parts.as_ref() { 360 | Some(one) 361 | } else { 362 | None 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /relative-path-utils/src/glob/tests.rs: -------------------------------------------------------------------------------- 1 | use super::Fragment; 2 | 3 | #[test] 4 | fn test_fragment() { 5 | let fragment = Fragment::parse("abc*def.*"); 6 | assert!(fragment.is_match("abc_xyz_def.exe")); 7 | assert!(fragment.is_match("abc_xyz_def.")); 8 | assert!(!fragment.is_match("ab_xyz_def.exe")); 9 | assert!(!fragment.is_match("abcdef")); 10 | assert!(!fragment.is_match("abc_xyz_def")); 11 | 12 | let fragment = Fragment::parse("*def"); 13 | assert!(fragment.is_match("abcdef")); 14 | assert!(!fragment.is_match("abcdeftrailing")); 15 | 16 | let fragment = Fragment::parse("abc*"); 17 | assert!(fragment.is_match("abcdef")); 18 | assert!(!fragment.is_match("leadingabc")); 19 | } 20 | -------------------------------------------------------------------------------- /relative-path-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [github](https://github.com/udoprog/relative-path) 2 | //! [crates.io](https://crates.io/crates/relative-path-utils) 3 | //! [docs.rs](https://docs.rs/relative-path-utils) 4 | //! 5 | //! Utilities for working with relative paths. 6 | //! 7 | //! This crate contains: 8 | //! * [`Root`] the `root` feature - A root directory that can be used to open 9 | //! files relative to it. 10 | //! * [`Glob`] the `root` feature - A glob pattern that can be used to match 11 | //! files relative to a [`Root`]. 12 | //! 13 | //! [`Root`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Root.html 14 | //! [`Glob`]: https://docs.rs/relative-path-utils/latest/relative_path_utils/struct.Glob.html 15 | 16 | #![deny(missing_docs)] 17 | #![no_std] 18 | #![cfg_attr(relative_path_docsrs, feature(doc_cfg))] 19 | 20 | #[cfg(feature = "alloc")] 21 | extern crate alloc; 22 | 23 | #[cfg(feature = "std")] 24 | extern crate std; 25 | 26 | #[cfg(feature = "root")] 27 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "root")))] 28 | #[doc(inline)] 29 | pub use self::root::{DirEntry, Metadata, OpenOptions, ReadDir, Root}; 30 | #[cfg(feature = "root")] 31 | mod root; 32 | 33 | #[cfg(feature = "root")] 34 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "root")))] 35 | #[doc(inline)] 36 | pub use self::glob::Glob; 37 | #[cfg(feature = "root")] 38 | mod glob; 39 | -------------------------------------------------------------------------------- /relative-path-utils/src/root/mod.rs: -------------------------------------------------------------------------------- 1 | // Some documentation copied from the Rust project under the MIT license. 2 | // 3 | // See https://github.com/rust-lang/rust 4 | 5 | use alloc::string::String; 6 | use alloc::vec::Vec; 7 | 8 | use std::ffi::OsString; 9 | use std::fs::File; 10 | use std::io::{self, Read, Write}; 11 | use std::path::Path; 12 | 13 | use crate::Glob; 14 | use relative_path::RelativePath; 15 | 16 | #[cfg_attr(windows, path = "windows.rs")] 17 | #[cfg_attr(unix, path = "unix.rs")] 18 | mod imp; 19 | 20 | #[cfg(not(any(windows, unix)))] 21 | compile_error!("root is only supported on cfg(windows) and cfg(unix)"); 22 | 23 | /// An open root directory from which relative paths can be opened. 24 | /// 25 | /// In contrast to using APIs such as [`RelativePath::to_path`], this does not 26 | /// require allocations to open a path. 27 | /// 28 | /// This is achieved by keeping an open handle to the directory and using 29 | /// platform-specific APIs to open a relative path, such as [`openat`] on `unix`. 30 | /// 31 | /// [`openat`]: https://linux.die.net/man/2/openat 32 | pub struct Root { 33 | inner: imp::Root, 34 | } 35 | 36 | impl Root { 37 | /// Open the given directory that can be used as a root for opening and 38 | /// manipulating relative paths. 39 | /// 40 | /// # Errors 41 | /// 42 | /// Errors if the underlying I/O operation fails. 43 | /// 44 | /// # Examples 45 | /// 46 | /// ```no_run 47 | /// use relative_path_utils::Root; 48 | /// 49 | /// let root = Root::new(".")?; 50 | /// # Ok::<_, std::io::Error>(()) 51 | /// ``` 52 | pub fn new

(path: P) -> io::Result 53 | where 54 | P: AsRef, 55 | { 56 | Ok(Self { 57 | inner: imp::Root::new(path.as_ref())?, 58 | }) 59 | } 60 | 61 | /// Construct an open options associated with this root. 62 | /// 63 | /// # Examples 64 | /// 65 | /// ```no_run 66 | /// use relative_path_utils::Root; 67 | /// 68 | /// let root = Root::new(".")?; 69 | /// 70 | /// let file = root.open_options().read(true).open("foo.txt"); 71 | /// # Ok::<_, std::io::Error>(()) 72 | /// ``` 73 | pub fn open_options(&self) -> OpenOptions { 74 | OpenOptions { 75 | root: &self.inner, 76 | options: imp::OpenOptions::new(), 77 | } 78 | } 79 | 80 | /// Opens a file in write-only mode. 81 | /// 82 | /// This function will create a file if it does not exist, and will truncate 83 | /// it if it does. 84 | /// 85 | /// Depending on the platform, this function may fail if the full directory 86 | /// path does not exist. See the [`OpenOptions::open`] function for more 87 | /// details. 88 | /// 89 | /// See also [`Root::write()`] for a simple function to create a file with a 90 | /// given data. 91 | /// 92 | /// # Errors 93 | /// 94 | /// Errors if the underlying I/O operation fails. 95 | /// 96 | /// # Examples 97 | /// 98 | /// ```no_run 99 | /// use std::io::Write; 100 | /// 101 | /// use relative_path_utils::Root; 102 | /// 103 | /// let root = Root::new(".")?; 104 | /// 105 | /// let mut f = root.create("foo.txt")?; 106 | /// f.write_all(&1234_u32.to_be_bytes())?; 107 | /// # Ok::<_, std::io::Error>(()) 108 | /// ``` 109 | pub fn create

(&self, path: P) -> io::Result 110 | where 111 | P: AsRef, 112 | { 113 | self.open_options().write(true).create(true).open(path) 114 | } 115 | 116 | /// Attempts to open a file in read-only mode. 117 | /// 118 | /// See the [`OpenOptions::open`] method for more details. 119 | /// 120 | /// If you only need to read the entire file contents, consider 121 | /// [`std::fs::read()`][Root::read] or 122 | /// [`std::fs::read_to_string()`][Root::read_to_string] instead. 123 | /// 124 | /// # Errors 125 | /// 126 | /// This function will return an error if `path` does not already exist. 127 | /// Other errors may also be returned according to [`OpenOptions::open`]. 128 | /// 129 | /// # Examples 130 | /// 131 | /// ```no_run 132 | /// use std::io::Read; 133 | /// 134 | /// use relative_path_utils::Root; 135 | /// 136 | /// let root = Root::new(".")?; 137 | /// 138 | /// let mut f = root.open("foo.txt")?; 139 | /// let mut data = vec![]; 140 | /// f.read_to_end(&mut data)?; 141 | /// # Ok::<_, std::io::Error>(()) 142 | /// ``` 143 | pub fn open

(&self, path: P) -> io::Result 144 | where 145 | P: AsRef, 146 | { 147 | self.open_options().read(true).open(path) 148 | } 149 | 150 | /// Read the entire contents of a file into a bytes vector. 151 | /// 152 | /// This is a convenience function for using [`File::open`] and 153 | /// [`read_to_end`] with fewer imports and without an intermediate variable. 154 | /// 155 | /// [`read_to_end`]: Read::read_to_end 156 | /// 157 | /// # Errors 158 | /// 159 | /// This function will return an error if `path` does not already exist. 160 | /// Other errors may also be returned according to [`OpenOptions::open`]. 161 | /// 162 | /// While reading from the file, this function handles 163 | /// [`io::ErrorKind::Interrupted`] with automatic retries. See [`io::Read`] 164 | /// documentation for details. 165 | /// 166 | /// # Examples 167 | /// 168 | /// ```no_run 169 | /// use std::net::SocketAddr; 170 | /// 171 | /// use relative_path_utils::Root; 172 | /// 173 | /// let root = Root::new(".")?; 174 | /// let foo: SocketAddr = String::from_utf8_lossy(&root.read("address.txt")?).parse()?; 175 | /// # Ok::<_, Box>(()) 176 | /// ``` 177 | pub fn read

(&self, path: P) -> io::Result> 178 | where 179 | P: AsRef, 180 | { 181 | fn inner(this: &Root, path: &RelativePath) -> io::Result> { 182 | let mut file = this.open(path)?; 183 | let size = file 184 | .metadata() 185 | .map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)) 186 | .ok(); 187 | let mut bytes = Vec::with_capacity(size.unwrap_or(0)); 188 | file.read_to_end(&mut bytes)?; 189 | Ok(bytes) 190 | } 191 | 192 | inner(self, path.as_ref()) 193 | } 194 | 195 | /// Read the entire contents of a file into a string. 196 | /// 197 | /// This is a convenience function for using [`File::open`] and 198 | /// [`read_to_string`] with fewer imports and without an intermediate 199 | /// variable. 200 | /// 201 | /// [`read_to_string`]: Read::read_to_string 202 | /// 203 | /// # Errors 204 | /// 205 | /// This function will return an error if `path` does not already exist. 206 | /// Other errors may also be returned according to [`OpenOptions::open`]. 207 | /// 208 | /// If the contents of the file are not valid UTF-8, then an error will also 209 | /// be returned. 210 | /// 211 | /// # Examples 212 | /// 213 | /// ```no_run 214 | /// use std::net::SocketAddr; 215 | /// 216 | /// use relative_path_utils::Root; 217 | /// 218 | /// let root = Root::new(".")?; 219 | /// let foo: SocketAddr = root.read_to_string("address.txt")?.parse()?; 220 | /// # Ok::<_, Box>(()) 221 | /// ``` 222 | pub fn read_to_string

(&self, path: P) -> io::Result 223 | where 224 | P: AsRef, 225 | { 226 | fn inner(this: &Root, path: &RelativePath) -> io::Result { 227 | let mut file = this.open(path)?; 228 | let size = file 229 | .metadata() 230 | .map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)) 231 | .ok(); 232 | let mut string = String::with_capacity(size.unwrap_or(0)); 233 | file.read_to_string(&mut string)?; 234 | Ok(string) 235 | } 236 | 237 | inner(self, path.as_ref()) 238 | } 239 | 240 | /// Write a slice as the entire contents of a file. 241 | /// 242 | /// This function will create a file if it does not exist, 243 | /// and will entirely replace its contents if it does. 244 | /// 245 | /// Depending on the platform, this function may fail if the 246 | /// full directory path does not exist. 247 | /// 248 | /// This is a convenience function for using [`File::create`] and 249 | /// [`write_all`] with fewer imports. 250 | /// 251 | /// [`write_all`]: Write::write_all 252 | /// 253 | /// # Errors 254 | /// 255 | /// Fails if an underlying I/O operation fails. 256 | /// 257 | /// # Examples 258 | /// 259 | /// ```no_run 260 | /// use relative_path_utils::Root; 261 | /// 262 | /// let root = Root::new(".")?; 263 | /// 264 | /// root.write("foo.txt", b"Lorem ipsum")?; 265 | /// root.write("bar.txt", "dolor sit")?; 266 | /// # Ok::<_, std::io::Error>(()) 267 | /// ``` 268 | pub fn write(&self, path: P, contents: C) -> io::Result<()> 269 | where 270 | P: AsRef, 271 | C: AsRef<[u8]>, 272 | { 273 | self.create(path)?.write_all(contents.as_ref()) 274 | } 275 | 276 | /// Given a path, query the file system to get information about a file, 277 | /// directory, etc. 278 | /// 279 | /// This function will traverse symbolic links to query information about 280 | /// the destination file. 281 | /// 282 | /// # Platform-specific behavior 283 | /// 284 | /// This function currently corresponds to the `stat` function on Unix and 285 | /// the `GetFileInformationByHandle` function on Windows. Note that, this 286 | /// [may change in the future][changes]. 287 | /// 288 | /// [changes]: io#platform-specific-behavior 289 | /// 290 | /// # Errors 291 | /// 292 | /// This function will return an error in the following situations, but is 293 | /// not limited to just these cases: 294 | /// 295 | /// * The user lacks permissions to perform `metadata` call on `path`. 296 | /// * `path` does not exist. 297 | /// 298 | /// # Examples 299 | /// 300 | /// ```rust,no_run 301 | /// use relative_path_utils::Root; 302 | /// 303 | /// let root = Root::new(".")?; 304 | /// let attr = root.metadata("file/path.txt")?; 305 | /// # Ok::<_, std::io::Error>(()) 306 | /// ``` 307 | pub fn metadata

(&self, path: P) -> io::Result 308 | where 309 | P: AsRef, 310 | { 311 | Ok(Metadata { 312 | inner: self.inner.metadata(path.as_ref())?, 313 | }) 314 | } 315 | 316 | /// Returns `true` if the path exists on disk and is pointing at a 317 | /// directory. 318 | /// 319 | /// This function will traverse symbolic links to query information about 320 | /// the destination file. 321 | /// 322 | /// If you cannot access the metadata of the file, e.g. because of a 323 | /// permission error or broken symbolic links, this will return `false`. 324 | /// 325 | /// # Examples 326 | /// 327 | /// ```no_run 328 | /// use relative_path_utils::Root; 329 | /// 330 | /// let root = Root::new(".")?; 331 | /// 332 | /// assert_eq!(root.is_dir("./is_a_directory/"), true); 333 | /// assert_eq!(root.is_dir("a_file.txt"), false); 334 | /// # Ok::<_, std::io::Error>(()) 335 | /// ``` 336 | /// 337 | /// # See Also 338 | /// 339 | /// This is a convenience function that coerces errors to false. If you want 340 | /// to check errors, call [`Root::metadata`] and handle its [`Result`]. Then 341 | /// call [`Metadata::is_dir`] if it was [`Ok`]. 342 | pub fn is_dir

(&self, path: P) -> bool 343 | where 344 | P: AsRef, 345 | { 346 | self.metadata(path).map(|m| m.is_dir()).unwrap_or(false) 347 | } 348 | 349 | /// Returns an iterator over the entries within a directory. 350 | /// 351 | /// The iterator will yield instances of 352 | /// [`io::Result`]<[`DirEntry`]>. New errors may be encountered 353 | /// after an iterator is initially constructed. Entries for the current and 354 | /// parent directories (typically `.` and `..`) are skipped. 355 | /// 356 | /// # Platform-specific behavior 357 | /// 358 | /// This function currently corresponds to the `opendir` function on Unix 359 | /// and the `FindFirstFile` function on Windows. Advancing the iterator 360 | /// currently corresponds to `readdir` on Unix and `FindNextFile` on Windows. 361 | /// Note that, this [may change in the future][changes]. 362 | /// 363 | /// [changes]: io#platform-specific-behavior 364 | /// 365 | /// The order in which this iterator returns entries is platform and filesystem 366 | /// dependent. 367 | /// 368 | /// # Errors 369 | /// 370 | /// This function will return an error in the following situations, but is not 371 | /// limited to just these cases: 372 | /// 373 | /// * The provided `path` doesn't exist. 374 | /// * The process lacks permissions to view the contents. 375 | /// * The `path` points at a non-directory file. 376 | /// 377 | /// # Examples 378 | /// 379 | /// ``` 380 | /// use std::io; 381 | /// 382 | /// use relative_path_utils::{Root, DirEntry}; 383 | /// use relative_path::RelativePath; 384 | /// 385 | /// // one possible implementation of walking a directory only visiting files 386 | /// fn visit_dirs(root: &Root, dir: &RelativePath, cb: &dyn Fn(&DirEntry)) -> io::Result<()> { 387 | /// if root.is_dir(dir) { 388 | /// for entry in root.read_dir(dir)? { 389 | /// let entry = entry?; 390 | /// let file_name = entry.file_name(); 391 | /// let path = dir.join(file_name.to_string_lossy().as_ref()); 392 | /// 393 | /// if root.is_dir(&path) { 394 | /// visit_dirs(root, &path, cb)?; 395 | /// } else { 396 | /// cb(&entry); 397 | /// } 398 | /// } 399 | /// } 400 | /// 401 | /// Ok(()) 402 | /// } 403 | /// ``` 404 | pub fn read_dir

(&self, path: P) -> io::Result 405 | where 406 | P: AsRef, 407 | { 408 | self.inner 409 | .read_dir(path.as_ref()) 410 | .map(|inner| ReadDir { inner }) 411 | } 412 | 413 | /// Parse a glob over the specified path. 414 | /// 415 | /// To perform the globbing, use [`Glob::matcher`]. 416 | /// 417 | /// # Examples 418 | /// 419 | /// ```no_run 420 | /// use relative_path_utils::Root; 421 | /// 422 | /// let root = Root::new("src")?; 423 | /// 424 | /// let glob = root.glob("**/*.rs"); 425 | /// 426 | /// let mut results = Vec::new(); 427 | /// 428 | /// for e in glob.matcher() { 429 | /// results.push(e?); 430 | /// } 431 | /// 432 | /// results.sort(); 433 | /// assert_eq!(results, vec!["lib.rs", "main.rs"]); 434 | /// # Ok::<_, Box>(()) 435 | /// ``` 436 | pub fn glob<'a, P>(&'a self, path: &'a P) -> Glob<'a> 437 | where 438 | P: ?Sized + AsRef, 439 | { 440 | Glob::new(self, path.as_ref()) 441 | } 442 | } 443 | 444 | /// Options and flags which can be used to configure how a file is opened. 445 | /// 446 | /// This builder exposes the ability to configure how a [`File`] is opened and 447 | /// what operations are permitted on the open file. The [`File::open`] and 448 | /// [`File::create`] methods are aliases for commonly used options using this 449 | /// builder. 450 | /// 451 | /// Generally speaking, when using `OpenOptions`, you'll first call 452 | /// [`Root::open_options`], then chain calls to methods to set each option, then 453 | /// call [`OpenOptions::open`], passing the path of the file you're trying to 454 | /// open. This will give you a [`io::Result`] with a [`File`] inside that you 455 | /// can further operate on. 456 | /// 457 | /// # Examples 458 | /// 459 | /// Opening a file to read: 460 | /// 461 | /// ```no_run 462 | /// use relative_path_utils::Root; 463 | /// 464 | /// let root = Root::new(".")?; 465 | /// 466 | /// let file = root.open_options().read(true).open("foo.txt"); 467 | /// # Ok::<_, std::io::Error>(()) 468 | /// ``` 469 | /// 470 | /// Opening a file for both reading and writing, as well as creating it if it 471 | /// doesn't exist: 472 | /// 473 | /// ```no_run 474 | /// use relative_path_utils::Root; 475 | /// 476 | /// let root = Root::new(".")?; 477 | /// 478 | /// let file = root 479 | /// .open_options() 480 | /// .read(true) 481 | /// .write(true) 482 | /// .create(true) 483 | /// .open("foo.txt")?; 484 | /// # Ok::<_, std::io::Error>(()) 485 | /// ``` 486 | #[derive(Clone, Debug)] 487 | #[must_use] 488 | pub struct OpenOptions<'a> { 489 | root: &'a imp::Root, 490 | options: imp::OpenOptions, 491 | } 492 | 493 | impl OpenOptions<'_> { 494 | /// Sets the option for read access. 495 | /// 496 | /// This option, when true, will indicate that the file should be 497 | /// `read`-able if opened. 498 | /// 499 | /// # Examples 500 | /// 501 | /// ```no_run 502 | /// use relative_path_utils::Root; 503 | /// 504 | /// let root = Root::new(".")?; 505 | /// 506 | /// let file = root.open_options().read(true).open("foo.txt"); 507 | /// # Ok::<_, std::io::Error>(()) 508 | /// ``` 509 | pub fn read(&mut self, read: bool) -> &mut Self { 510 | self.options.read(read); 511 | self 512 | } 513 | 514 | /// Sets the option for write access. 515 | /// 516 | /// This option, when true, will indicate that the file should be 517 | /// `write`-able if opened. 518 | /// 519 | /// If the file already exists, any write calls on it will overwrite its 520 | /// contents, without truncating it. 521 | /// 522 | /// # Examples 523 | /// 524 | /// ```no_run 525 | /// use relative_path_utils::Root; 526 | /// 527 | /// let root = Root::new(".")?; 528 | /// 529 | /// let file = root.open_options().write(true).open("foo.txt"); 530 | /// # Ok::<_, std::io::Error>(()) 531 | /// ``` 532 | pub fn write(&mut self, write: bool) -> &mut Self { 533 | self.options.write(write); 534 | self 535 | } 536 | 537 | /// Sets the option for the append mode. 538 | /// 539 | /// This option, when true, means that writes will append to a file instead 540 | /// of overwriting previous contents. Note that setting 541 | /// `.write(true).append(true)` has the same effect as setting only 542 | /// `.append(true)`. 543 | /// 544 | /// For most filesystems, the operating system guarantees that all writes 545 | /// are atomic: no writes get mangled because another process writes at the 546 | /// same time. 547 | /// 548 | /// One maybe obvious note when using append-mode: make sure that all data 549 | /// that belongs together is written to the file in one operation. This can 550 | /// be done by concatenating strings before passing them to [`write()`], or 551 | /// using a buffered writer (with a buffer of adequate size), and calling 552 | /// [`flush()`] when the message is complete. 553 | /// 554 | /// If a file is opened with both read and append access, beware that after 555 | /// opening, and after every write, the position for reading may be set at 556 | /// the end of the file. So, before writing, save the current position 557 | /// (using [`seek`]\([`SeekFrom`]::[`Current`]\(0))), and 558 | /// restore it before the next read. 559 | /// 560 | /// ## Note 561 | /// 562 | /// This function doesn't create the file if it doesn't exist. Use the 563 | /// [`OpenOptions::create`] method to do so. 564 | /// 565 | /// [`write()`]: Write::write "io::Write::write" 566 | /// [`flush()`]: Write::flush "io::Write::flush" 567 | /// [`seek`]: std::io::Seek::seek "io::Seek::seek" 568 | /// [`SeekFrom`]: std::io::SeekFrom 569 | /// [`Current`]: std::io::SeekFrom::Current 570 | /// 571 | /// # Examples 572 | /// 573 | /// ```no_run 574 | /// use relative_path_utils::Root; 575 | /// 576 | /// let root = Root::new(".")?; 577 | /// 578 | /// let file = root.open_options().append(true).open("foo.txt"); 579 | /// # Ok::<_, std::io::Error>(()) 580 | /// ``` 581 | pub fn append(&mut self, append: bool) -> &mut Self { 582 | self.options.append(append); 583 | self 584 | } 585 | 586 | /// Sets the option for truncating a previous file. 587 | /// 588 | /// If a file is successfully opened with this option set it will truncate 589 | /// the file to 0 length if it already exists. 590 | /// 591 | /// The file must be opened with write access for truncate to work. 592 | /// 593 | /// # Examples 594 | /// 595 | /// ```no_run 596 | /// use relative_path_utils::Root; 597 | /// 598 | /// let root = Root::new(".")?; 599 | /// 600 | /// let file = root.open_options().write(true).truncate(true).open("foo.txt"); 601 | /// # Ok::<_, std::io::Error>(()) 602 | /// ``` 603 | pub fn truncate(&mut self, truncate: bool) -> &mut Self { 604 | self.options.truncate(truncate); 605 | self 606 | } 607 | 608 | /// Sets the option to create a new file, or open it if it already exists. 609 | /// 610 | /// In order for the file to be created, [`OpenOptions::write`] or 611 | /// [`OpenOptions::append`] access must be used. 612 | /// 613 | /// See also [`Root::write()`] for a simple function to create a file with a 614 | /// given data. 615 | /// 616 | /// # Examples 617 | /// 618 | /// ```no_run 619 | /// use relative_path_utils::Root; 620 | /// 621 | /// let root = Root::new(".")?; 622 | /// 623 | /// let file = root.open_options().write(true).create(true).open("foo.txt"); 624 | /// # Ok::<_, std::io::Error>(()) 625 | /// ``` 626 | pub fn create(&mut self, create: bool) -> &mut Self { 627 | self.options.create(create); 628 | self 629 | } 630 | 631 | /// Sets the option to create a new file, failing if it already exists. 632 | /// 633 | /// No file is allowed to exist at the target location, also no (dangling) symlink. In this 634 | /// way, if the call succeeds, the file returned is guaranteed to be new. 635 | /// 636 | /// This option is useful because it is atomic. Otherwise between checking 637 | /// whether a file exists and creating a new one, the file may have been 638 | /// created by another process (a TOCTOU race condition / attack). 639 | /// 640 | /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are 641 | /// ignored. 642 | /// 643 | /// The file must be opened with write or append access in order to create 644 | /// a new file. 645 | /// 646 | /// [`.create()`]: OpenOptions::create 647 | /// [`.truncate()`]: OpenOptions::truncate 648 | /// 649 | /// # Examples 650 | /// 651 | /// ```no_run 652 | /// use relative_path_utils::Root; 653 | /// 654 | /// let root = Root::new(".")?; 655 | /// 656 | /// let file = root.open_options().write(true).create_new(true).open("foo.txt"); 657 | /// # Ok::<_, std::io::Error>(()) 658 | /// ``` 659 | pub fn create_new(&mut self, create_new: bool) -> &mut Self { 660 | self.options.create_new(create_new); 661 | self 662 | } 663 | 664 | /// Opens a file at `path` with the options specified by `self`. 665 | /// 666 | /// # Errors 667 | /// 668 | /// This function will return an error under a number of different 669 | /// circumstances. Some of these error conditions are listed here, together 670 | /// with their [`io::ErrorKind`]. The mapping to [`io::ErrorKind`]s is not 671 | /// part of the compatibility contract of the function. 672 | /// 673 | /// * [`NotFound`]: The specified file does not exist and neither `create` 674 | /// or `create_new` is set. 675 | /// * [`NotFound`]: One of the directory components of the file path does 676 | /// not exist. 677 | /// * [`PermissionDenied`]: The user lacks permission to get the specified 678 | /// access rights for the file. 679 | /// * [`PermissionDenied`]: The user lacks permission to open one of the 680 | /// directory components of the specified path. 681 | /// * [`AlreadyExists`]: `create_new` was specified and the file already 682 | /// exists. 683 | /// * [`InvalidInput`]: Invalid combinations of open options (truncate 684 | /// without write access, no access mode set, etc.). 685 | /// 686 | /// The following errors don't match any existing [`io::ErrorKind`] at the moment: 687 | /// * One of the directory components of the specified file path 688 | /// was not, in fact, a directory. 689 | /// * Filesystem-level errors: full disk, write permission 690 | /// requested on a read-only file system, exceeded disk quota, too many 691 | /// open files, too long filename, too many symbolic links in the 692 | /// specified path (Unix-like systems only), etc. 693 | /// 694 | /// # Examples 695 | /// 696 | /// ```no_run 697 | /// use relative_path_utils::Root; 698 | /// 699 | /// let root = Root::new(".")?; 700 | /// 701 | /// let file = root.open_options().read(true).open("foo.txt"); 702 | /// # Ok::<_, std::io::Error>(()) 703 | /// ``` 704 | /// 705 | /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists 706 | /// [`InvalidInput`]: io::ErrorKind::InvalidInput 707 | /// [`NotFound`]: io::ErrorKind::NotFound 708 | /// [`PermissionDenied`]: io::ErrorKind::PermissionDenied 709 | pub fn open

(&self, path: P) -> io::Result 710 | where 711 | P: AsRef, 712 | { 713 | self.root.open_at(path.as_ref(), &self.options) 714 | } 715 | } 716 | 717 | /// Iterator over the entries in a directory. 718 | /// 719 | /// This iterator is returned from the [`Root::read_dir`] function and will 720 | /// yield instances of [`io::Result`]<[`DirEntry`]>. Through a 721 | /// [`DirEntry`] information like the entry's path and possibly other metadata 722 | /// can be learned. 723 | /// 724 | /// The order in which this iterator returns entries is platform and filesystem 725 | /// dependent. 726 | /// 727 | /// # Errors 728 | /// 729 | /// This [`io::Result`] will be an [`Err`] if there's some sort of intermittent 730 | /// IO error during iteration. 731 | pub struct ReadDir { 732 | inner: imp::ReadDir, 733 | } 734 | 735 | impl Iterator for ReadDir { 736 | type Item = io::Result; 737 | 738 | #[inline] 739 | fn next(&mut self) -> Option { 740 | let inner = self.inner.next()?; 741 | Some(inner.map(|inner| DirEntry { inner })) 742 | } 743 | } 744 | 745 | /// Entries returned by the [`ReadDir`] iterator. 746 | /// 747 | /// An instance of `DirEntry` represents an entry inside of a directory on the 748 | /// filesystem. Each entry can be inspected via methods to learn about the full 749 | /// path or possibly other metadata through per-platform extension traits. 750 | /// 751 | /// # Platform-specific behavior 752 | /// 753 | /// On Unix, the `DirEntry` struct contains an internal reference to the open 754 | /// directory. Holding `DirEntry` objects will consume a file handle even after 755 | /// the `ReadDir` iterator is dropped. 756 | pub struct DirEntry { 757 | inner: imp::DirEntry, 758 | } 759 | 760 | impl DirEntry { 761 | /// Returns the file name of this directory entry without any 762 | /// leading path component(s). 763 | /// 764 | /// As an example, 765 | /// the output of the function will result in "foo" for all the following paths: 766 | /// - "./foo" 767 | /// - "/the/foo" 768 | /// - "../../foo" 769 | /// 770 | /// # Examples 771 | /// 772 | /// ```no_run 773 | /// use relative_path_utils::Root; 774 | /// 775 | /// let mut root = Root::new(".")?; 776 | /// 777 | /// for entry in root.read_dir("src")? { 778 | /// let entry = entry?; 779 | /// println!("{:?}", entry.file_name()); 780 | /// } 781 | /// # Ok::<_, std::io::Error>(()) 782 | /// ``` 783 | #[must_use] 784 | pub fn file_name(&self) -> OsString { 785 | self.inner.file_name() 786 | } 787 | } 788 | 789 | /// Metadata information about a file. 790 | /// 791 | /// This structure is returned from the [`metadata`] function and represents 792 | /// known metadata about a file such as its permissions, size, modification 793 | /// times, etc. 794 | /// 795 | /// [`metadata`]: Root::metadata 796 | #[derive(Clone)] 797 | pub struct Metadata { 798 | inner: imp::Metadata, 799 | } 800 | 801 | impl Metadata { 802 | /// Returns `true` if this metadata is for a directory. The result is 803 | /// mutually exclusive to the result of [`Metadata::is_file`]. 804 | /// 805 | /// # Examples 806 | /// 807 | /// ```no_run 808 | /// use relative_path_utils::Root; 809 | /// 810 | /// let root = Root::new(".")?; 811 | /// 812 | /// let metadata = root.metadata("foo.txt")?; 813 | /// assert!(!metadata.is_dir()); 814 | /// # Ok::<_, std::io::Error>(()) 815 | /// ``` 816 | #[must_use] 817 | #[inline] 818 | pub fn is_dir(&self) -> bool { 819 | self.inner.is_dir() 820 | } 821 | 822 | /// Returns `true` if this metadata is for a regular file. The result is 823 | /// mutually exclusive to the result of [`Metadata::is_dir`]. 824 | /// 825 | /// When the goal is simply to read from (or write to) the source, the most 826 | /// reliable way to test the source can be read (or written to) is to open 827 | /// it. Only using `is_file` can break workflows like `diff <( prog_a )` on 828 | /// a Unix-like system for example. See [`Root::open`] or 829 | /// [`OpenOptions::open`] for more information. 830 | /// 831 | /// # Examples 832 | /// 833 | /// ```no_run 834 | /// use relative_path_utils::Root; 835 | /// 836 | /// let root = Root::new(".")?; 837 | /// 838 | /// let metadata = root.metadata("foo.txt")?; 839 | /// assert!(metadata.is_file()); 840 | /// # Ok::<_, std::io::Error>(()) 841 | /// ``` 842 | #[must_use] 843 | #[inline] 844 | pub fn is_file(&self) -> bool { 845 | self.inner.is_file() 846 | } 847 | 848 | /// Returns `true` if this metadata is for a symbolic link. 849 | /// 850 | /// # Examples 851 | /// 852 | /// ```no_run 853 | /// use relative_path_utils::Root; 854 | /// 855 | /// let root = Root::new(".")?; 856 | /// 857 | /// let metadata = root.metadata("foo.txt")?; 858 | /// assert!(metadata.is_symlink()); 859 | /// # Ok::<_, std::io::Error>(()) 860 | /// ``` 861 | #[must_use] 862 | pub fn is_symlink(&self) -> bool { 863 | self.inner.is_symlink() 864 | } 865 | } 866 | -------------------------------------------------------------------------------- /relative-path-utils/src/root/unix.rs: -------------------------------------------------------------------------------- 1 | use core::mem::MaybeUninit; 2 | 3 | use alloc::vec::Vec; 4 | 5 | use std::ffi::{CString, OsString}; 6 | use std::fs::File; 7 | use std::io; 8 | use std::os::fd::OwnedFd; 9 | use std::os::fd::{AsFd, AsRawFd}; 10 | use std::os::fd::{FromRawFd, IntoRawFd}; 11 | use std::path::{Path, MAIN_SEPARATOR}; 12 | 13 | #[cfg(not(any( 14 | all(target_os = "linux", not(target_env = "musl")), 15 | target_os = "emscripten", 16 | target_os = "l4re", 17 | target_os = "android", 18 | target_os = "hurd", 19 | )))] 20 | use libc::{fstat as fstat64, stat as stat64}; 21 | #[cfg(any( 22 | all(target_os = "linux", not(target_env = "musl")), 23 | target_os = "emscripten", 24 | target_os = "l4re", 25 | target_os = "hurd" 26 | ))] 27 | use libc::{fstat64, stat64}; 28 | 29 | use relative_path::{Component, RelativePath}; 30 | 31 | #[derive(Debug)] 32 | pub(super) struct Root { 33 | handle: OwnedFd, 34 | } 35 | 36 | impl Root { 37 | pub(super) fn new(path: &Path) -> io::Result { 38 | let file = std::fs::OpenOptions::new().read(true).open(path)?; 39 | 40 | Ok(Root { 41 | handle: OwnedFd::from(file), 42 | }) 43 | } 44 | 45 | pub(super) fn open_at(&self, path: &RelativePath, options: &OpenOptions) -> io::Result { 46 | Ok(File::from(self.open_at_inner(path, options)?)) 47 | } 48 | 49 | fn open_at_inner(&self, path: &RelativePath, options: &OpenOptions) -> io::Result { 50 | let path = convert_to_c_string(path)?; 51 | 52 | let flags = libc::O_CLOEXEC | options.get_creation_mode()? | options.get_access_mode()?; 53 | 54 | unsafe { 55 | let fd = libc::openat(self.handle.as_raw_fd(), path.as_ptr(), flags); 56 | 57 | if fd == -1 { 58 | return Err(io::Error::last_os_error()); 59 | } 60 | 61 | Ok(OwnedFd::from_raw_fd(fd)) 62 | } 63 | } 64 | 65 | pub(super) fn metadata(&self, path: &RelativePath) -> io::Result { 66 | let owned; 67 | 68 | let fd = if is_current(path) { 69 | self.handle.as_fd() 70 | } else { 71 | let mut opts = OpenOptions::new(); 72 | opts.read(true); 73 | owned = self.open_at_inner(path, &opts)?; 74 | owned.as_fd() 75 | }; 76 | 77 | unsafe { 78 | let mut stat = MaybeUninit::zeroed(); 79 | 80 | if fstat64(fd.as_raw_fd(), stat.as_mut_ptr()) == -1 { 81 | return Err(io::Error::last_os_error()); 82 | } 83 | 84 | let stat = stat.assume_init(); 85 | 86 | Ok(Metadata { stat }) 87 | } 88 | } 89 | 90 | pub(super) fn read_dir(&self, path: &RelativePath) -> io::Result { 91 | let mut opts = OpenOptions::new(); 92 | opts.read(true); 93 | let fd = self.open_at_inner(path, &opts)?; 94 | 95 | let dir = unsafe { 96 | let handle = libc::fdopendir(fd.as_raw_fd()); 97 | 98 | if handle.is_null() { 99 | return Err(io::Error::last_os_error()); 100 | } 101 | 102 | // fdopendir() takes ownership of the file descriptor, and it will 103 | // be closed when the `Dir` handle is dropped. 104 | _ = fd.into_raw_fd(); 105 | Dir { handle } 106 | }; 107 | 108 | Ok(ReadDir { 109 | dir, 110 | end_of_stream: false, 111 | }) 112 | } 113 | } 114 | 115 | pub(super) struct ReadDir { 116 | dir: Dir, 117 | end_of_stream: bool, 118 | } 119 | 120 | impl Iterator for ReadDir { 121 | type Item = io::Result; 122 | 123 | #[inline] 124 | fn next(&mut self) -> Option { 125 | self.inner_next() 126 | } 127 | } 128 | 129 | #[cfg(any( 130 | target_os = "android", 131 | target_os = "linux", 132 | target_os = "solaris", 133 | target_os = "fuchsia", 134 | target_os = "redox", 135 | target_os = "illumos", 136 | target_os = "aix", 137 | target_os = "nto", 138 | target_os = "vita", 139 | target_os = "hurd", 140 | ))] 141 | mod read_dir { 142 | use std::ffi::c_char; 143 | use std::ffi::CStr; 144 | use std::ffi::OsString; 145 | use std::io; 146 | use std::mem::size_of; 147 | use std::os::unix::ffi::OsStringExt; 148 | 149 | use super::{DirEntry, ReadDir}; 150 | 151 | impl ReadDir { 152 | pub(super) fn inner_next(&mut self) -> Option> { 153 | const D_NAME_OFFSET: usize = size_of::() 154 | + size_of::() 155 | + size_of::() 156 | + size_of::(); 157 | 158 | if self.end_of_stream { 159 | return None; 160 | } 161 | 162 | unsafe { 163 | loop { 164 | // As of POSIX.1-2017, readdir() is not required to be thread safe; only 165 | // readdir_r() is. However, readdir_r() cannot correctly handle platforms 166 | // with unlimited or variable NAME_MAX. Many modern platforms guarantee 167 | // thread safety for readdir() as long an individual DIR* is not accessed 168 | // concurrently, which is sufficient for Rust. 169 | errno::set_errno(errno::Errno(0)); 170 | let entry_ptr = libc::readdir64(self.dir.handle); 171 | 172 | if entry_ptr.is_null() { 173 | // We either encountered an error, or reached the end. Either way, 174 | // the next call to next() should return None. 175 | self.end_of_stream = true; 176 | 177 | // To distinguish between errors and end-of-directory, we had to clear 178 | // errno beforehand to check for an error now. 179 | return match errno::errno().0 { 180 | 0 => None, 181 | e => Some(Err(io::Error::from_raw_os_error(e))), 182 | }; 183 | } 184 | 185 | // d_name is guaranteed to be null-terminated. 186 | let name = 187 | CStr::from_ptr(entry_ptr.cast::().add(D_NAME_OFFSET).cast::()); 188 | 189 | let name_bytes = name.to_bytes(); 190 | 191 | if name_bytes == b"." || name_bytes == b".." { 192 | continue; 193 | } 194 | 195 | return Some(Ok(DirEntry { 196 | file_name: OsString::from_vec(name_bytes.to_vec()), 197 | })); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | #[cfg(not(any( 205 | target_os = "android", 206 | target_os = "linux", 207 | target_os = "solaris", 208 | target_os = "fuchsia", 209 | target_os = "redox", 210 | target_os = "illumos", 211 | target_os = "aix", 212 | target_os = "nto", 213 | target_os = "vita", 214 | target_os = "hurd", 215 | )))] 216 | mod read_dir { 217 | use std::ffi::OsString; 218 | use std::io; 219 | use std::mem::MaybeUninit; 220 | use std::os::unix::ffi::OsStringExt; 221 | use std::ptr; 222 | use std::slice; 223 | 224 | use super::{DirEntry, ReadDir}; 225 | 226 | impl ReadDir { 227 | pub(super) fn inner_next(&mut self) -> Option> { 228 | if self.end_of_stream { 229 | return None; 230 | } 231 | 232 | unsafe { 233 | loop { 234 | let mut entry = MaybeUninit::zeroed(); 235 | let mut entry_ptr = ptr::null_mut(); 236 | 237 | let err = libc::readdir_r(self.dir.handle, entry.as_mut_ptr(), &mut entry_ptr); 238 | 239 | if err != 0 { 240 | if entry_ptr.is_null() { 241 | // We encountered an error (which will be returned in this iteration), but 242 | // we also reached the end of the directory stream. The `end_of_stream` 243 | // flag is enabled to make sure that we return `None` in the next iteration 244 | // (instead of looping forever) 245 | self.end_of_stream = true; 246 | } 247 | 248 | return Some(Err(io::Error::from_raw_os_error(err))); 249 | } 250 | 251 | if entry_ptr.is_null() { 252 | return None; 253 | } 254 | 255 | let entry = entry.assume_init(); 256 | 257 | let name_bytes = slice::from_raw_parts( 258 | entry.d_name.as_ptr() as *const u8, 259 | entry.d_namlen as usize, 260 | ); 261 | 262 | if name_bytes == b"." || name_bytes == b".." { 263 | continue; 264 | } 265 | 266 | return Some(Ok(DirEntry { 267 | file_name: OsString::from_vec(name_bytes.to_vec()), 268 | })); 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | pub(crate) struct DirEntry { 276 | file_name: OsString, 277 | } 278 | 279 | impl DirEntry { 280 | pub(crate) fn file_name(&self) -> OsString { 281 | self.file_name.clone() 282 | } 283 | } 284 | 285 | struct Dir { 286 | handle: *mut libc::DIR, 287 | } 288 | 289 | unsafe impl Send for Dir {} 290 | unsafe impl Sync for Dir {} 291 | 292 | impl Drop for Dir { 293 | #[inline] 294 | fn drop(&mut self) { 295 | _ = unsafe { libc::closedir(self.handle) }; 296 | } 297 | } 298 | 299 | unsafe impl Send for OpenOptions {} 300 | unsafe impl Sync for OpenOptions {} 301 | 302 | #[derive(Clone, Debug)] 303 | #[allow(clippy::struct_excessive_bools)] 304 | pub(super) struct OpenOptions { 305 | // generic 306 | read: bool, 307 | write: bool, 308 | append: bool, 309 | truncate: bool, 310 | create: bool, 311 | create_new: bool, 312 | } 313 | 314 | impl OpenOptions { 315 | pub(super) fn new() -> OpenOptions { 316 | OpenOptions { 317 | // generic 318 | read: false, 319 | write: false, 320 | append: false, 321 | truncate: false, 322 | create: false, 323 | create_new: false, 324 | } 325 | } 326 | 327 | pub(super) fn read(&mut self, read: bool) { 328 | self.read = read; 329 | } 330 | 331 | pub(super) fn write(&mut self, write: bool) { 332 | self.write = write; 333 | } 334 | 335 | pub(super) fn append(&mut self, append: bool) { 336 | self.append = append; 337 | } 338 | 339 | pub(super) fn truncate(&mut self, truncate: bool) { 340 | self.truncate = truncate; 341 | } 342 | 343 | pub(super) fn create(&mut self, create: bool) { 344 | self.create = create; 345 | } 346 | 347 | pub(super) fn create_new(&mut self, create_new: bool) { 348 | self.create_new = create_new; 349 | } 350 | 351 | fn get_access_mode(&self) -> io::Result { 352 | match (self.read, self.write, self.append) { 353 | (true, false, false) => Ok(libc::O_RDONLY), 354 | (false, true, false) => Ok(libc::O_WRONLY), 355 | (true, true, false) => Ok(libc::O_RDWR), 356 | (false, _, true) => Ok(libc::O_WRONLY | libc::O_APPEND), 357 | (true, _, true) => Ok(libc::O_RDWR | libc::O_APPEND), 358 | (false, false, false) => Err(io::Error::from_raw_os_error(libc::EINVAL)), 359 | } 360 | } 361 | 362 | fn get_creation_mode(&self) -> io::Result { 363 | match (self.write, self.append) { 364 | (true, false) => {} 365 | (false, false) => { 366 | if self.truncate || self.create || self.create_new { 367 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 368 | } 369 | } 370 | (_, true) => { 371 | if self.truncate && !self.create_new { 372 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 373 | } 374 | } 375 | } 376 | 377 | Ok(match (self.create, self.truncate, self.create_new) { 378 | (false, false, false) => 0, 379 | (true, false, false) => libc::O_CREAT, 380 | (false, true, false) => libc::O_TRUNC, 381 | (true, true, false) => libc::O_CREAT | libc::O_TRUNC, 382 | (_, _, true) => libc::O_CREAT | libc::O_EXCL, 383 | }) 384 | } 385 | } 386 | 387 | #[derive(Clone)] 388 | pub(super) struct Metadata { 389 | stat: stat64, 390 | } 391 | 392 | impl Metadata { 393 | #[inline] 394 | pub(super) fn is_dir(&self) -> bool { 395 | self.is(libc::S_IFDIR) 396 | } 397 | 398 | #[inline] 399 | pub(super) fn is_file(&self) -> bool { 400 | self.is(libc::S_IFREG) 401 | } 402 | 403 | #[inline] 404 | pub(super) fn is_symlink(&self) -> bool { 405 | self.is(libc::S_IFLNK) 406 | } 407 | 408 | #[inline] 409 | pub(super) fn is(&self, mode: libc::mode_t) -> bool { 410 | self.masked() == mode 411 | } 412 | 413 | #[inline] 414 | fn masked(&self) -> libc::mode_t { 415 | self.stat.st_mode & libc::S_IFMT 416 | } 417 | } 418 | 419 | fn is_current(path: &RelativePath) -> bool { 420 | path.components().all(|c| c == Component::CurDir) 421 | } 422 | 423 | fn convert_to_c_string(input: &RelativePath) -> io::Result { 424 | let mut path = Vec::new(); 425 | 426 | for c in input.components() { 427 | match c { 428 | Component::CurDir => {} 429 | Component::ParentDir => { 430 | if path.is_empty() { 431 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 432 | } 433 | 434 | let index = path 435 | .iter() 436 | .rposition(|&b| b == MAIN_SEPARATOR as u8) 437 | .unwrap_or(0); 438 | path.truncate(index); 439 | } 440 | Component::Normal(normal) => { 441 | if normal.as_bytes().contains(&b'\0') { 442 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 443 | } 444 | 445 | if !path.is_empty() { 446 | path.push(b'/'); 447 | } 448 | 449 | path.extend_from_slice(normal.as_bytes()); 450 | } 451 | } 452 | } 453 | 454 | if path.is_empty() { 455 | path.push(b'.'); 456 | } 457 | 458 | path.push(0); 459 | 460 | // SAFETY: We've checked safety requirements of CString above. 461 | let owned = unsafe { CString::from_vec_with_nul_unchecked(path) }; 462 | Ok(owned) 463 | } 464 | -------------------------------------------------------------------------------- /relative-path-utils/src/root/windows.rs: -------------------------------------------------------------------------------- 1 | use core::mem::{self, align_of, size_of, MaybeUninit}; 2 | use core::ptr; 3 | use core::slice; 4 | 5 | use alloc::vec::Vec; 6 | 7 | use std::ffi::OsString; 8 | use std::fs::File; 9 | use std::io; 10 | use std::os::windows::ffi::OsStringExt; 11 | use std::os::windows::fs::OpenOptionsExt; 12 | use std::os::windows::io::{AsHandle, AsRawHandle, FromRawHandle, OwnedHandle, RawHandle}; 13 | use std::path::Path; 14 | use std::path::MAIN_SEPARATOR; 15 | 16 | use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; 17 | use windows_sys::Wdk::Storage::FileSystem as nt; 18 | use windows_sys::Wdk::System::SystemServices::SL_RESTART_SCAN; 19 | use windows_sys::Win32::Foundation::FALSE; 20 | use windows_sys::Win32::Foundation::{ 21 | RtlNtStatusToDosError, ERROR_INVALID_PARAMETER, HANDLE, STATUS_NO_MORE_FILES, STATUS_PENDING, 22 | STATUS_SUCCESS, UNICODE_STRING, 23 | }; 24 | use windows_sys::Win32::Storage::FileSystem as c; 25 | use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; 26 | 27 | use relative_path::{Component, RelativePath}; 28 | 29 | #[allow(clippy::cast_possible_wrap)] 30 | const EINVAL: i32 = ERROR_INVALID_PARAMETER as i32; 31 | const MAIN_SEPARATOR_U16: u16 = MAIN_SEPARATOR as u16; 32 | 33 | #[derive(Debug)] 34 | pub(super) struct Root { 35 | handle: OwnedHandle, 36 | } 37 | 38 | impl Root { 39 | pub(super) fn new(path: &Path) -> io::Result { 40 | let file = std::fs::OpenOptions::new() 41 | .read(true) 42 | .attributes(c::FILE_FLAG_BACKUP_SEMANTICS) 43 | .open(path)?; 44 | 45 | Ok(Root { 46 | handle: OwnedHandle::from(file), 47 | }) 48 | } 49 | 50 | pub(super) fn open_at(&self, path: &RelativePath, options: &OpenOptions) -> io::Result { 51 | let handle = self.open_at_inner(path, options)?; 52 | Ok(File::from(handle)) 53 | } 54 | 55 | pub(super) fn open_at_inner( 56 | &self, 57 | path: &RelativePath, 58 | options: &OpenOptions, 59 | ) -> io::Result { 60 | let path = encode_path_wide(path)?; 61 | 62 | // SAFETY: All the operations and parameters are correctly used. 63 | unsafe { 64 | let object_name = unicode_string_ref(&path)?; 65 | 66 | let attributes = OBJECT_ATTRIBUTES { 67 | Length: len_of::(), 68 | RootDirectory: self.handle.as_raw_handle() as HANDLE, 69 | ObjectName: &object_name, 70 | Attributes: 0, 71 | SecurityDescriptor: ptr::null(), 72 | SecurityQualityOfService: ptr::null(), 73 | }; 74 | 75 | let mut status_block = IO_STATUS_BLOCK { 76 | Anonymous: windows_sys::Win32::System::IO::IO_STATUS_BLOCK_0 { 77 | Status: STATUS_PENDING, 78 | }, 79 | Information: 0, 80 | }; 81 | 82 | let mut handle = MaybeUninit::zeroed(); 83 | 84 | let status = nt::NtCreateFile( 85 | handle.as_mut_ptr(), 86 | c::SYNCHRONIZE | options.get_access_mode()?, 87 | &attributes, 88 | &mut status_block, 89 | ptr::null_mut(), 90 | 0, 91 | options.share_mode, 92 | options.get_creation_mode()?, 93 | nt::FILE_SYNCHRONOUS_IO_ALERT | options.custom_create_options, 94 | ptr::null(), 95 | 0, 96 | ); 97 | 98 | if status != STATUS_SUCCESS { 99 | return Err(nt_error(status)); 100 | } 101 | 102 | Ok(OwnedHandle::from_raw_handle( 103 | handle.assume_init() as RawHandle 104 | )) 105 | } 106 | } 107 | 108 | pub(super) fn metadata(&self, path: &RelativePath) -> io::Result { 109 | let owned; 110 | 111 | let handle = if is_current(path) { 112 | self.handle.as_handle() 113 | } else { 114 | let mut opts = OpenOptions::new(); 115 | opts.read(true); 116 | opts.access_mode = Some(c::FILE_READ_ATTRIBUTES); 117 | // No read or write permissions are necessary 118 | // opts.access_mode = Some(0); 119 | // opts.custom_create_options = nt::FILE_OPEN_FOR_BACKUP_INTENT; 120 | owned = self.open_at_inner(path, &opts)?; 121 | owned.as_handle() 122 | }; 123 | 124 | unsafe { 125 | let mut info = MaybeUninit::zeroed(); 126 | 127 | if c::GetFileInformationByHandle(handle.as_raw_handle() as isize, info.as_mut_ptr()) 128 | == FALSE 129 | { 130 | return Err(io::Error::last_os_error()); 131 | } 132 | 133 | let info = info.assume_init(); 134 | 135 | let mut reparse_tag = 0; 136 | 137 | if info.dwFileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 { 138 | let mut attr_tag = MaybeUninit::::zeroed(); 139 | 140 | let result = c::GetFileInformationByHandleEx( 141 | self.handle.as_raw_handle() as isize, 142 | c::FileAttributeTagInfo, 143 | attr_tag.as_mut_ptr().cast(), 144 | mem::size_of::() 145 | .try_into() 146 | .unwrap(), 147 | ); 148 | 149 | if result == FALSE { 150 | return Err(io::Error::last_os_error()); 151 | } 152 | 153 | let attr_tag = attr_tag.assume_init(); 154 | 155 | if attr_tag.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 { 156 | reparse_tag = attr_tag.ReparseTag; 157 | } 158 | } 159 | 160 | Ok(Metadata { 161 | attributes: info.dwFileAttributes, 162 | reparse_tag, 163 | }) 164 | } 165 | } 166 | 167 | pub(super) fn read_dir(&self, path: &RelativePath) -> io::Result { 168 | let handle = if is_current(path) { 169 | self.handle.try_clone()? 170 | } else { 171 | let mut opts = OpenOptions::new(); 172 | opts.access_mode = Some(c::FILE_LIST_DIRECTORY); 173 | self.open_at_inner(path, &opts)? 174 | }; 175 | 176 | Ok(ReadDir { 177 | handle, 178 | buffer: AlignedBuf::new(), 179 | at: None, 180 | first: true, 181 | }) 182 | } 183 | } 184 | 185 | #[repr(C)] 186 | struct AlignedBuf { 187 | _align: [T; 0], 188 | buf: [u8; N], 189 | } 190 | 191 | impl AlignedBuf { 192 | fn new() -> Self { 193 | Self { 194 | _align: [], 195 | buf: [0; N], 196 | } 197 | } 198 | 199 | #[allow(clippy::cast_possible_truncation, clippy::unused_self)] 200 | fn len(&self) -> u32 { 201 | N as u32 202 | } 203 | 204 | unsafe fn as_ptr_at(&self, at: usize) -> *const T { 205 | assert!(at % align_of::() == 0); 206 | self.buf.as_ptr().add(at).cast() 207 | } 208 | 209 | fn as_mut_ptr(&mut self) -> *mut u8 { 210 | self.buf.as_mut_ptr() 211 | } 212 | } 213 | 214 | pub(super) struct ReadDir { 215 | handle: OwnedHandle, 216 | buffer: AlignedBuf, 217 | first: bool, 218 | at: Option, 219 | } 220 | 221 | impl Iterator for ReadDir { 222 | type Item = io::Result; 223 | 224 | #[inline] 225 | fn next(&mut self) -> Option { 226 | loop { 227 | while let Some(at) = self.at.take() { 228 | unsafe { 229 | let file_names = &*self 230 | .buffer 231 | .as_ptr_at(at) 232 | .cast::(); 233 | 234 | let len = file_names.FileNameLength; 235 | let ptr = file_names.FileName.as_ptr(); 236 | let name = slice::from_raw_parts(ptr, len as usize / 2); 237 | 238 | if file_names.NextEntryOffset != 0 { 239 | self.at = Some(at + file_names.NextEntryOffset as usize); 240 | } 241 | 242 | if let Some(entry) = DirEntry::new(name) { 243 | return Some(Ok(entry)); 244 | } 245 | } 246 | } 247 | 248 | unsafe { 249 | let mut status_block = IO_STATUS_BLOCK { 250 | Anonymous: windows_sys::Win32::System::IO::IO_STATUS_BLOCK_0 { 251 | Status: STATUS_PENDING, 252 | }, 253 | Information: 0, 254 | }; 255 | 256 | let status = nt::NtQueryDirectoryFileEx( 257 | self.handle.as_raw_handle() as HANDLE, 258 | 0, 259 | None, 260 | ptr::null(), 261 | &mut status_block, 262 | self.buffer.as_mut_ptr().cast(), 263 | self.buffer.len(), 264 | 12, // FileNamesInformation 265 | if mem::take(&mut self.first) { 266 | SL_RESTART_SCAN 267 | } else { 268 | 0 269 | }, 270 | ptr::null(), 271 | ); 272 | 273 | if status == STATUS_NO_MORE_FILES { 274 | return None; 275 | } 276 | 277 | if status != STATUS_SUCCESS { 278 | return Some(Err(nt_error(status))); 279 | } 280 | 281 | self.at = Some(0); 282 | } 283 | } 284 | } 285 | } 286 | 287 | pub(super) struct DirEntry { 288 | file_name: OsString, 289 | } 290 | 291 | impl DirEntry { 292 | fn new(data: &[u16]) -> Option { 293 | match data { 294 | // check for '.' and '..' 295 | [46] | [46, 46] => return None, 296 | _ => {} 297 | } 298 | 299 | Some(DirEntry { 300 | file_name: OsString::from_wide(data), 301 | }) 302 | } 303 | 304 | pub(super) fn file_name(&self) -> OsString { 305 | self.file_name.clone() 306 | } 307 | } 308 | 309 | fn is_current(path: &RelativePath) -> bool { 310 | path.components().all(|c| c == Component::CurDir) 311 | } 312 | 313 | fn encode_path_wide(input: &RelativePath) -> io::Result> { 314 | let mut path = Vec::with_capacity(input.as_str().len() * 2); 315 | 316 | for c in input.components() { 317 | match c { 318 | Component::CurDir => {} 319 | Component::ParentDir => { 320 | if path.is_empty() { 321 | return Err(io::Error::from_raw_os_error(EINVAL)); 322 | } 323 | 324 | let index = path 325 | .iter() 326 | .rposition(|window| *window == MAIN_SEPARATOR_U16) 327 | .unwrap_or(0); 328 | 329 | path.truncate(index); 330 | } 331 | Component::Normal(normal) => { 332 | if !path.is_empty() { 333 | path.extend_from_slice(MAIN_SEPARATOR.encode_utf16(&mut [0; 2])); 334 | } 335 | 336 | path.extend(normal.encode_utf16()); 337 | } 338 | } 339 | } 340 | 341 | if path.is_empty() { 342 | path.extend_from_slice('.'.encode_utf16(&mut [0; 2])); 343 | } 344 | 345 | Ok(path) 346 | } 347 | 348 | unsafe impl Send for OpenOptions {} 349 | unsafe impl Sync for OpenOptions {} 350 | 351 | #[derive(Clone, Debug)] 352 | #[allow(clippy::struct_excessive_bools)] 353 | pub(super) struct OpenOptions { 354 | // generic 355 | read: bool, 356 | write: bool, 357 | append: bool, 358 | truncate: bool, 359 | create: bool, 360 | create_new: bool, 361 | // system-specific 362 | custom_create_options: u32, 363 | share_mode: u32, 364 | access_mode: Option, 365 | } 366 | 367 | impl OpenOptions { 368 | pub(super) fn new() -> OpenOptions { 369 | OpenOptions { 370 | // generic 371 | read: false, 372 | write: false, 373 | append: false, 374 | truncate: false, 375 | create: false, 376 | create_new: false, 377 | // system-specific 378 | custom_create_options: 0, 379 | share_mode: c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE, 380 | access_mode: None, 381 | } 382 | } 383 | 384 | pub(super) fn read(&mut self, read: bool) { 385 | self.read = read; 386 | } 387 | 388 | pub(super) fn write(&mut self, write: bool) { 389 | self.write = write; 390 | } 391 | 392 | pub(super) fn append(&mut self, append: bool) { 393 | self.append = append; 394 | } 395 | 396 | pub(super) fn truncate(&mut self, truncate: bool) { 397 | self.truncate = truncate; 398 | } 399 | 400 | pub(super) fn create(&mut self, create: bool) { 401 | self.create = create; 402 | } 403 | 404 | pub(super) fn create_new(&mut self, create_new: bool) { 405 | self.create_new = create_new; 406 | } 407 | 408 | fn get_access_mode(&self) -> io::Result { 409 | // NtCreateFile does not support `GENERIC_READ`. 410 | const DEFAULT_READ: u32 = 411 | c::STANDARD_RIGHTS_READ | c::FILE_READ_DATA | c::FILE_READ_EA | c::FILE_READ_ATTRIBUTES; 412 | 413 | const DEFAULT_WRITE: u32 = c::STANDARD_RIGHTS_WRITE 414 | | c::FILE_WRITE_DATA 415 | | c::FILE_WRITE_EA 416 | | c::FILE_WRITE_ATTRIBUTES; 417 | 418 | let access_mode = match (self.read, self.write, self.append, self.access_mode) { 419 | (_, _, _, Some(access_mode)) => access_mode, 420 | (true, false, false, _) => DEFAULT_READ, 421 | (false, true, false, _) => DEFAULT_WRITE, 422 | (true, true, false, _) => DEFAULT_READ | DEFAULT_WRITE, 423 | (false, _, true, _) => c::FILE_GENERIC_WRITE & !c::FILE_WRITE_DATA, 424 | (true, _, true, _) => DEFAULT_READ | (c::FILE_GENERIC_WRITE & !c::FILE_WRITE_DATA), 425 | (false, false, false, _) => { 426 | return Err(io::Error::from_raw_os_error(EINVAL)); 427 | } 428 | }; 429 | 430 | Ok(access_mode) 431 | } 432 | 433 | fn get_creation_mode(&self) -> io::Result { 434 | match (self.write, self.append) { 435 | (true, false) => {} 436 | (false, false) => { 437 | if self.truncate || self.create || self.create_new { 438 | return Err(io::Error::from_raw_os_error(EINVAL)); 439 | } 440 | } 441 | (_, true) => { 442 | if self.truncate && !self.create_new { 443 | return Err(io::Error::from_raw_os_error(EINVAL)); 444 | } 445 | } 446 | } 447 | 448 | Ok(match (self.create, self.truncate, self.create_new) { 449 | (false, false, false) => nt::FILE_OPEN, 450 | (true, false, false) => nt::FILE_OPEN_IF, 451 | (false, true, false) => nt::FILE_OVERWRITE, 452 | (true, true, false) => nt::FILE_OVERWRITE_IF, 453 | (_, _, true) => nt::FILE_CREATE, 454 | }) 455 | } 456 | } 457 | 458 | #[derive(Clone)] 459 | pub(super) struct Metadata { 460 | attributes: u32, 461 | reparse_tag: u32, 462 | } 463 | 464 | impl Metadata { 465 | #[inline] 466 | pub(super) fn is_dir(&self) -> bool { 467 | !self.is_symlink() && self.is_directory() 468 | } 469 | 470 | #[inline] 471 | pub(super) fn is_file(&self) -> bool { 472 | !self.is_symlink() && !self.is_directory() 473 | } 474 | 475 | #[inline] 476 | pub(super) fn is_symlink(&self) -> bool { 477 | self.is_reparse_point() && self.is_reparse_tag_name_surrogate() 478 | } 479 | 480 | #[inline] 481 | #[allow(unused)] 482 | pub(super) fn is_symlink_dir(&self) -> bool { 483 | self.is_symlink() && self.is_directory() 484 | } 485 | 486 | #[inline] 487 | #[allow(unused)] 488 | pub(super) fn is_symlink_file(&self) -> bool { 489 | self.is_symlink() && !self.is_directory() 490 | } 491 | 492 | #[inline] 493 | fn is_directory(&self) -> bool { 494 | self.attributes & c::FILE_ATTRIBUTE_DIRECTORY != 0 495 | } 496 | 497 | #[inline] 498 | fn is_reparse_point(&self) -> bool { 499 | self.attributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0 500 | } 501 | 502 | #[inline] 503 | fn is_reparse_tag_name_surrogate(&self) -> bool { 504 | self.reparse_tag & 0x2000_0000 != 0 505 | } 506 | } 507 | 508 | #[allow(clippy::cast_possible_truncation)] 509 | unsafe fn len_of() -> u32 { 510 | size_of::() as u32 511 | } 512 | 513 | #[allow(clippy::cast_possible_wrap)] 514 | fn nt_error(status: i32) -> io::Error { 515 | io::Error::from_raw_os_error(unsafe { RtlNtStatusToDosError(status) } as i32) 516 | } 517 | 518 | fn unicode_string_ref(path: &[u16]) -> io::Result { 519 | let Ok(len) = u16::try_from(mem::size_of_val(path)) else { 520 | return Err(io::Error::from_raw_os_error(EINVAL)); 521 | }; 522 | 523 | Ok(UNICODE_STRING { 524 | Length: len, 525 | MaximumLength: len, 526 | Buffer: path.as_ptr().cast_mut(), 527 | }) 528 | } 529 | -------------------------------------------------------------------------------- /relative-path-utils/tests/root.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use anyhow::Result; 5 | use relative_path_utils::Root; 6 | 7 | const PATH: &str = env!("CARGO_TARGET_TMPDIR"); 8 | 9 | fn make_path(path: &'static str) -> PathBuf { 10 | Path::new(PATH).join(path) 11 | } 12 | 13 | fn root(path: &'static str) -> Root { 14 | match Root::new(make_path(path)) { 15 | Ok(root) => root, 16 | Err(error) => panic!("Failed to open root: {path}: {error}"), 17 | } 18 | } 19 | 20 | fn files(list: &[(&'static str, Option<&'static str>)]) { 21 | for (item, content) in list { 22 | let path = make_path(item); 23 | 24 | if let Some(content) = content { 25 | if let Some(parent) = path.parent() { 26 | if let Err(error) = fs::create_dir_all(parent) { 27 | panic!("Failed to create directory {}: {}", parent.display(), error); 28 | } 29 | } 30 | 31 | if let Err(error) = fs::write(&path, content) { 32 | panic!("Failed to create file {}: {}", path.display(), error); 33 | } 34 | } else if let Err(error) = fs::create_dir_all(&path) { 35 | panic!("Failed to create directory {}: {}", path.display(), error); 36 | } 37 | } 38 | } 39 | 40 | #[test] 41 | fn relative_open() -> Result<()> { 42 | files(&[("relative_open/src/root/first", Some("first content"))]); 43 | 44 | let r1 = root("relative_open/src/root"); 45 | assert_eq!(r1.read_to_string("first")?, "first content"); 46 | 47 | let r2 = root("relative_open/src"); 48 | assert_eq!(r2.read_to_string("root/first")?, "first content"); 49 | Ok(()) 50 | } 51 | 52 | #[test] 53 | fn read_dir() -> Result<()> { 54 | files(&[("read_dir/src/root/first", Some("first content"))]); 55 | files(&[("read_dir/src/root/second", Some("second content"))]); 56 | 57 | let r1 = root("."); 58 | let d = r1.read_dir("read_dir/src/root")?; 59 | 60 | let mut values = Vec::new(); 61 | 62 | for e in d { 63 | let e = e?; 64 | values.push(e.file_name().to_string_lossy().into_owned()); 65 | } 66 | 67 | values.sort(); 68 | 69 | assert_eq!(values, vec!["first", "second"]); 70 | Ok(()) 71 | } 72 | 73 | #[test] 74 | fn glob() -> Result<()> { 75 | files(&[("glob/src/root/first", Some("first content"))]); 76 | files(&[("glob/src/root/second", Some("second content"))]); 77 | 78 | let r1 = root("glob"); 79 | let glob = r1.glob("**/root/*"); 80 | 81 | let mut results = Vec::new(); 82 | 83 | for e in glob.matcher() { 84 | results.push(e?); 85 | } 86 | 87 | results.sort(); 88 | assert_eq!(results, vec!["src/root/first", "src/root/second"]); 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn read_root_dir() -> Result<()> { 94 | files(&[("read_root_dir/first", Some("first content"))]); 95 | 96 | let r1 = root("read_root_dir"); 97 | 98 | let mut values = Vec::new(); 99 | 100 | for e in r1.read_dir("")? { 101 | let e = e?; 102 | values.push(e.file_name().to_string_lossy().into_owned()); 103 | } 104 | 105 | for e in r1.read_dir("")? { 106 | let e = e?; 107 | values.push(e.file_name().to_string_lossy().into_owned()); 108 | } 109 | 110 | assert_eq!(values, vec!["first", "first"]); 111 | Ok(()) 112 | } 113 | 114 | #[test] 115 | fn test_parent_dir() -> Result<()> { 116 | files(&[("test_parent_dir/foo/bar/first", Some("first content"))]); 117 | files(&[("test_parent_dir/foo/second", Some("second content"))]); 118 | 119 | let r1 = root("test_parent_dir"); 120 | assert_eq!(r1.read_to_string("foo/bar/../second")?, "second content"); 121 | assert!(r1.read_to_string("../second").is_err()); 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /relative-path/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "relative-path" 3 | version = "2.0.1" 4 | authors = ["John-John Tedro "] 5 | edition = "2021" 6 | rust-version = "1.66" 7 | description = "Portable, relative paths for Rust." 8 | documentation = "https://docs.rs/relative-path" 9 | readme = "README.md" 10 | homepage = "https://github.com/udoprog/relative-path" 11 | repository = "https://github.com/udoprog/relative-path" 12 | license = "MIT OR Apache-2.0" 13 | keywords = ["path"] 14 | categories = ["filesystem"] 15 | 16 | [lints.rust] 17 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(relative_path_docsrs)'] } 18 | 19 | [features] 20 | default = ["std", "alloc"] 21 | alloc = ["serde?/alloc"] 22 | std = [] 23 | serde = ["dep:serde" ] 24 | 25 | [dependencies] 26 | serde = { version = "1.0.160", optional = true, default-features = false } 27 | 28 | [dev-dependencies] 29 | anyhow = "1.0.76" 30 | foldhash = "0.1.5" 31 | serde = { version = "1.0.160", features = ["derive"] } 32 | 33 | [package.metadata.docs.rs] 34 | all-features = true 35 | rustdoc-args = ["--cfg", "relative_path_docsrs"] 36 | -------------------------------------------------------------------------------- /relative-path/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /relative-path/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /relative-path/README.md: -------------------------------------------------------------------------------- 1 | # relative-path 2 | 3 | [github](https://github.com/udoprog/relative-path) 4 | [crates.io](https://crates.io/crates/relative-path) 5 | [docs.rs](https://docs.rs/relative-path) 6 | [build status](https://github.com/udoprog/relative-path/actions?query=branch%3Amain) 7 | 8 | Portable relative UTF-8 paths for Rust. 9 | 10 | This crate provides a module analogous to [`std::path`], with the following 11 | characteristics: 12 | 13 | * The path separator is set to a fixed character (`/`), regardless of 14 | platform. 15 | * Relative paths cannot represent a path in the filesystem without first 16 | specifying *what they are relative to* using functions such as [`to_path`] 17 | and [`to_logical_path`]. 18 | * Relative paths are always guaranteed to be valid UTF-8 strings. 19 | 20 | On top of this we support many operations that guarantee the same behavior 21 | across platforms. 22 | 23 | For more utilities to manipulate relative paths, see the 24 | [`relative-path-utils` crate]. 25 | 26 |
27 | 28 | ## Usage 29 | 30 | Add `relative-path` to your `Cargo.toml`: 31 | 32 | ```toml 33 | relative-path = "2.0.1" 34 | ``` 35 | 36 | Start using relative paths: 37 | 38 | ```rust 39 | use serde::{Serialize, Deserialize}; 40 | use relative_path::RelativePath; 41 | 42 | #[derive(Serialize, Deserialize)] 43 | struct Manifest<'a> { 44 | #[serde(borrow)] 45 | source: &'a RelativePath, 46 | } 47 | 48 | ``` 49 | 50 |
51 | 52 | ## Serde Support 53 | 54 | This library includes serde support that can be enabled with the `serde` 55 | feature. 56 | 57 |
58 | 59 | ## Why is `std::path` a portability hazard? 60 | 61 | Path representations differ across platforms. 62 | 63 | * Windows permits using drive volumes (multiple roots) as a prefix (e.g. 64 | `"c:\"`) and backslash (`\`) as a separator. 65 | * Unix references absolute paths from a single root and uses forward slash 66 | (`/`) as a separator. 67 | 68 | If we use `PathBuf`, Storing paths in a manifest would allow our application 69 | to build and run on one platform but potentially not others. 70 | 71 | Consider the following data model and corresponding toml for a manifest: 72 | 73 | ```rust 74 | use std::path::PathBuf; 75 | 76 | use serde::{Serialize, Deserialize}; 77 | 78 | #[derive(Serialize, Deserialize)] 79 | struct Manifest { 80 | source: PathBuf, 81 | } 82 | ``` 83 | 84 | ```toml 85 | source = "C:\\Users\\udoprog\\repo\\data\\source" 86 | ``` 87 | 88 | This will run for you (assuming `source` exists). So you go ahead and check 89 | the manifest into git. The next day your Linux colleague calls you and 90 | wonders what they have ever done to wrong you? 91 | 92 | So what went wrong? Well two things. You forgot to make the `source` 93 | relative, so anyone at the company which has a different username than you 94 | won't be able to use it. So you go ahead and fix that: 95 | 96 | ```toml 97 | source = "data\\source" 98 | ``` 99 | 100 | But there is still one problem! A backslash (`\`) is only a legal path 101 | separator on Windows. Luckily you learn that forward slashes are supported 102 | both on Windows *and* Linux. So you opt for: 103 | 104 | ```toml 105 | source = "data/source" 106 | ``` 107 | 108 | Things are working now. So all is well... Right? Sure, but we can do better. 109 | 110 | This crate provides types that work with *portable relative paths* (hence 111 | the name). So by using [`RelativePath`] we can systematically help avoid 112 | portability issues like the one above. Avoiding issues at the source is 113 | preferably over spending 5 minutes of onboarding time on a theoretical 114 | problem, hoping that your new hires will remember what to do if they ever 115 | encounter it. 116 | 117 | Using [`RelativePathBuf`] we can fix our data model like this: 118 | 119 | ```rust 120 | use relative_path::RelativePathBuf; 121 | use serde::{Serialize, Deserialize}; 122 | 123 | #[derive(Serialize, Deserialize)] 124 | pub struct Manifest { 125 | source: RelativePathBuf, 126 | } 127 | ``` 128 | 129 | And where it's used: 130 | 131 | ```rust 132 | use std::fs; 133 | use std::env::current_dir; 134 | 135 | let manifest: Manifest = todo!(); 136 | 137 | let root = current_dir()?; 138 | let source = manifest.source.to_path(&root); 139 | let content = fs::read(&source)?; 140 | ``` 141 | 142 |
143 | 144 | ## Overview 145 | 146 | Conversion to a platform-specific [`Path`] happens through the [`to_path`] 147 | and [`to_logical_path`] functions. Where you are required to specify the 148 | path that prefixes the relative path. This can come from a function such as 149 | [`std::env::current_dir`]. 150 | 151 | ```rust 152 | use std::env::current_dir; 153 | use std::path::Path; 154 | 155 | use relative_path::RelativePath; 156 | 157 | let root = current_dir()?; 158 | 159 | // to_path unconditionally concatenates a relative path with its base: 160 | let relative_path = RelativePath::new("../foo/./bar"); 161 | let full_path = relative_path.to_path(&root); 162 | assert_eq!(full_path, root.join("..\\foo\\.\\bar")); 163 | 164 | // to_logical_path tries to apply the logical operations that the relative 165 | // path corresponds to: 166 | let relative_path = RelativePath::new("../foo/./bar"); 167 | let full_path = relative_path.to_logical_path(&root); 168 | 169 | // Replicate the operation performed by `to_logical_path`. 170 | let mut parent = root.clone(); 171 | parent.pop(); 172 | assert_eq!(full_path, parent.join("foo\\bar")); 173 | ``` 174 | 175 | When two relative paths are compared to each other, their exact component 176 | makeup determines equality. 177 | 178 | ```rust 179 | use relative_path::RelativePath; 180 | 181 | assert_ne!( 182 | RelativePath::new("foo/bar/../baz"), 183 | RelativePath::new("foo/baz") 184 | ); 185 | ``` 186 | 187 | Using platform-specific path separators to construct relative paths is not 188 | supported. 189 | 190 | Path separators from other platforms are simply treated as part of a 191 | component: 192 | 193 | ```rust 194 | use relative_path::RelativePath; 195 | 196 | assert_ne!( 197 | RelativePath::new("foo/bar"), 198 | RelativePath::new("foo\\bar") 199 | ); 200 | 201 | assert_eq!(1, RelativePath::new("foo\\bar").components().count()); 202 | assert_eq!(2, RelativePath::new("foo/bar").components().count()); 203 | ``` 204 | 205 | To see if two relative paths are equivalent you can use [`normalize`]: 206 | 207 | ```rust 208 | use relative_path::RelativePath; 209 | 210 | assert_eq!( 211 | RelativePath::new("foo/bar/../baz").normalize(), 212 | RelativePath::new("foo/baz").normalize(), 213 | ); 214 | ``` 215 | 216 |
217 | 218 | ## Additional portability notes 219 | 220 | While relative paths avoid the most egregious portability issue, that 221 | absolute paths will work equally unwell on all platforms. We cannot avoid 222 | all. This section tries to document additional portability hazards that we 223 | are aware of. 224 | 225 | [`RelativePath`], similarly to [`Path`], makes no guarantees that its 226 | constituent components make up legal file names. While components are 227 | strictly separated by slashes, we can still store things in them which may 228 | not be used as legal paths on all platforms. 229 | 230 | * A `NUL` character is not permitted on unix platforms - this is a 231 | terminator in C-based filesystem APIs. Slash (`/`) is also used as a path 232 | separator. 233 | * Windows has a number of [reserved characters and names][windows-reserved] 234 | (like `CON`, `PRN`, and `AUX`) which cannot legally be part of a 235 | filesystem component. 236 | * Windows paths are [case-insensitive by default][windows-case]. So, 237 | `Foo.txt` and `foo.txt` are the same files on windows. But they are 238 | considered different paths on most unix systems. 239 | 240 | A relative path that *accidentally* contains a platform-specific components 241 | will largely result in a nonsensical paths being generated in the hope that 242 | they will fail fast during development and testing. 243 | 244 | ```rust 245 | use relative_path::{RelativePath, PathExt}; 246 | use std::path::Path; 247 | 248 | if cfg!(windows) { 249 | assert_eq!( 250 | Path::new("foo\\c:\\bar\\baz"), 251 | RelativePath::new("c:\\bar\\baz").to_path("foo") 252 | ); 253 | } 254 | 255 | if cfg!(unix) { 256 | assert_eq!( 257 | Path::new("foo/bar/baz"), 258 | RelativePath::new("/bar/baz").to_path("foo") 259 | ); 260 | } 261 | 262 | assert_eq!( 263 | Path::new("foo").relative_to("bar")?, 264 | RelativePath::new("../foo"), 265 | ); 266 | ``` 267 | 268 | [`None`]: https://doc.rust-lang.org/std/option/enum.Option.html 269 | [`normalize`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.normalize 270 | [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html 271 | [`RelativePath`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html 272 | [`RelativePathBuf`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePathBuf.html 273 | [`std::env::current_dir`]: https://doc.rust-lang.org/std/env/fn.current_dir.html 274 | [`std::path`]: https://doc.rust-lang.org/std/path/index.html 275 | [`to_logical_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_logical_path 276 | [`to_path`]: https://docs.rs/relative-path/1/relative_path/struct.RelativePath.html#method.to_path 277 | [windows-reserved]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 278 | [windows-case]: https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity 279 | [`relative-path-utils` crate]: https://docs.rs/relative-path-utils 280 | -------------------------------------------------------------------------------- /relative-path/src/path_ext.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | // Ported from the pathdiff crate, which adapted the original rustc's 12 | // path_relative_from 13 | // https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs 14 | // https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158 15 | 16 | use core::fmt; 17 | use std::error; 18 | use std::path::{Path, PathBuf}; 19 | use std::prelude::v1::*; 20 | 21 | use crate::{Component, RelativePathBuf}; 22 | 23 | // Prevent downstream implementations, so methods may be added without backwards 24 | // breaking changes. 25 | mod sealed { 26 | use std::path::{Path, PathBuf}; 27 | 28 | pub trait Sealed {} 29 | 30 | impl Sealed for Path {} 31 | impl Sealed for PathBuf {} 32 | } 33 | 34 | /// An error raised when attempting to convert a path using 35 | /// [`PathExt::relative_to`]. 36 | #[derive(Debug, Clone, PartialEq, Eq)] 37 | pub struct RelativeToError { 38 | kind: RelativeToErrorKind, 39 | } 40 | 41 | /// Error kind for [`RelativeToError`]. 42 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 43 | #[non_exhaustive] 44 | enum RelativeToErrorKind { 45 | /// Non-utf8 component in path. 46 | NonUtf8, 47 | /// Mismatching path prefixes. 48 | PrefixMismatch, 49 | /// A provided path is ambiguous, in that there is no way to determine which 50 | /// components should be added from one path to the other to traverse it. 51 | /// 52 | /// For example, `.` is ambiguous relative to `../..` because we don't know 53 | /// the names of the components being traversed. 54 | AmbiguousTraversal, 55 | /// This is a catch-all error since we don't control the `std::path` API a 56 | /// Components iterator might decide (intentionally or not) to produce 57 | /// components which violates its own contract. 58 | /// 59 | /// In particular we rely on only relative components being produced after 60 | /// the absolute prefix has been consumed. 61 | IllegalComponent, 62 | } 63 | 64 | impl fmt::Display for RelativeToError { 65 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 66 | match self.kind { 67 | RelativeToErrorKind::NonUtf8 => "path contains non-utf8 component".fmt(fmt), 68 | RelativeToErrorKind::PrefixMismatch => { 69 | "paths contain different absolute prefixes".fmt(fmt) 70 | } 71 | RelativeToErrorKind::AmbiguousTraversal => { 72 | "path traversal cannot be determined".fmt(fmt) 73 | } 74 | RelativeToErrorKind::IllegalComponent => "path contains illegal components".fmt(fmt), 75 | } 76 | } 77 | } 78 | 79 | impl error::Error for RelativeToError {} 80 | 81 | impl From for RelativeToError { 82 | #[inline] 83 | fn from(kind: RelativeToErrorKind) -> Self { 84 | Self { kind } 85 | } 86 | } 87 | 88 | /// Extension methods for [`Path`] and [`PathBuf`] to for building and 89 | /// interacting with [`RelativePath`]. 90 | /// 91 | /// [`RelativePath`]: crate::RelativePath 92 | pub trait PathExt: sealed::Sealed { 93 | /// Build a relative path from the provided directory to `self`. 94 | /// 95 | /// Producing a relative path like this is a logical operation and does not 96 | /// guarantee that the constructed path corresponds to what the filesystem 97 | /// would do. On Linux for example symbolic links could mean that the 98 | /// logical path doesn't correspond to the filesystem path. 99 | /// 100 | /// # Examples 101 | /// 102 | /// ``` 103 | /// use std::path::Path; 104 | /// use relative_path::{RelativePath, PathExt}; 105 | /// 106 | /// let baz = Path::new("/foo/bar/baz"); 107 | /// let bar = Path::new("/foo/bar"); 108 | /// let qux = Path::new("/foo/bar/qux"); 109 | /// 110 | /// assert_eq!(bar.relative_to(baz)?, RelativePath::new("../")); 111 | /// assert_eq!(baz.relative_to(bar)?, RelativePath::new("baz")); 112 | /// assert_eq!(qux.relative_to(baz)?, RelativePath::new("../qux")); 113 | /// assert_eq!(baz.relative_to(qux)?, RelativePath::new("../baz")); 114 | /// assert_eq!(bar.relative_to(qux)?, RelativePath::new("../")); 115 | /// # Ok::<_, relative_path::RelativeToError>(()) 116 | /// ``` 117 | /// 118 | /// # Errors 119 | /// 120 | /// Errors in case the provided path contains components which cannot be 121 | /// converted into a relative path as needed, such as non-utf8 data. 122 | fn relative_to

(&self, root: P) -> Result 123 | where 124 | P: AsRef; 125 | } 126 | 127 | impl PathExt for Path { 128 | fn relative_to

(&self, root: P) -> Result 129 | where 130 | P: AsRef, 131 | { 132 | use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir}; 133 | 134 | // Helper function to convert from a std::path::Component to a 135 | // relative_path::Component. 136 | fn std_to_c(c: std::path::Component<'_>) -> Result, RelativeToError> { 137 | Ok(match c { 138 | CurDir => Component::CurDir, 139 | ParentDir => Component::ParentDir, 140 | Normal(n) => Component::Normal(n.to_str().ok_or(RelativeToErrorKind::NonUtf8)?), 141 | _ => return Err(RelativeToErrorKind::IllegalComponent.into()), 142 | }) 143 | } 144 | 145 | let root = root.as_ref(); 146 | let mut a_it = self.components(); 147 | let mut b_it = root.components(); 148 | 149 | // Ensure that the two paths are both either relative, or have the same 150 | // prefix. Strips any common prefix the two paths do have. Prefixes are 151 | // platform dependent, but different prefixes would for example indicate 152 | // paths for different drives on Windows. 153 | let (a_head, b_head) = loop { 154 | match (a_it.next(), b_it.next()) { 155 | (Some(RootDir), Some(RootDir)) => (), 156 | (Some(Prefix(a)), Some(Prefix(b))) if a == b => (), 157 | (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => { 158 | return Err(RelativeToErrorKind::PrefixMismatch.into()); 159 | } 160 | (None, None) => break (None, None), 161 | (a, b) if a != b => break (a, b), 162 | _ => (), 163 | } 164 | }; 165 | 166 | let mut a_it = a_head.into_iter().chain(a_it); 167 | let mut b_it = b_head.into_iter().chain(b_it); 168 | let mut buf = RelativePathBuf::new(); 169 | 170 | loop { 171 | let a = if let Some(a) = a_it.next() { 172 | a 173 | } else { 174 | for _ in b_it { 175 | buf.push(Component::ParentDir); 176 | } 177 | 178 | break; 179 | }; 180 | 181 | match b_it.next() { 182 | Some(CurDir) => buf.push(std_to_c(a)?), 183 | Some(ParentDir) => { 184 | return Err(RelativeToErrorKind::AmbiguousTraversal.into()); 185 | } 186 | root => { 187 | if root.is_some() { 188 | buf.push(Component::ParentDir); 189 | } 190 | 191 | for comp in b_it { 192 | match comp { 193 | ParentDir => { 194 | if !buf.pop() { 195 | return Err(RelativeToErrorKind::AmbiguousTraversal.into()); 196 | } 197 | } 198 | CurDir => (), 199 | _ => buf.push(Component::ParentDir), 200 | } 201 | } 202 | 203 | buf.push(std_to_c(a)?); 204 | 205 | for c in a_it { 206 | buf.push(std_to_c(c)?); 207 | } 208 | 209 | break; 210 | } 211 | } 212 | } 213 | 214 | Ok(buf) 215 | } 216 | } 217 | 218 | impl PathExt for PathBuf { 219 | #[inline] 220 | fn relative_to

(&self, root: P) -> Result 221 | where 222 | P: AsRef, 223 | { 224 | self.as_path().relative_to(root) 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use std::path::Path; 231 | 232 | use super::{PathExt, RelativeToErrorKind}; 233 | use crate::{RelativePathBuf, RelativeToError}; 234 | 235 | macro_rules! assert_relative_to { 236 | ($path:expr, $base:expr, Ok($expected:expr) $(,)?) => { 237 | assert_eq!( 238 | Path::new($path).relative_to($base), 239 | Ok(RelativePathBuf::from($expected)) 240 | ); 241 | }; 242 | 243 | ($path:expr, $base:expr, Err($expected:ident) $(,)?) => { 244 | assert_eq!( 245 | Path::new($path).relative_to($base), 246 | Err(RelativeToError::from(RelativeToErrorKind::$expected)) 247 | ); 248 | }; 249 | } 250 | 251 | #[cfg(windows)] 252 | macro_rules! abs { 253 | ($path:expr) => { 254 | Path::new(concat!("C:\\", $path)) 255 | }; 256 | } 257 | 258 | #[cfg(not(windows))] 259 | macro_rules! abs { 260 | ($path:expr) => { 261 | Path::new(concat!("/", $path)) 262 | }; 263 | } 264 | 265 | #[test] 266 | #[cfg(windows)] 267 | fn test_different_prefixes() { 268 | assert_relative_to!("C:\\repo", "D:\\repo", Err(PrefixMismatch),); 269 | assert_relative_to!("C:\\repo", "C:\\repo", Ok("")); 270 | assert_relative_to!( 271 | "\\\\server\\share\\repo", 272 | "\\\\server2\\share\\repo", 273 | Err(PrefixMismatch), 274 | ); 275 | } 276 | 277 | #[test] 278 | fn test_absolute() { 279 | assert_relative_to!(abs!("foo"), abs!("bar"), Ok("../foo")); 280 | assert_relative_to!("foo", "bar", Ok("../foo")); 281 | assert_relative_to!(abs!("foo"), "bar", Err(PrefixMismatch)); 282 | assert_relative_to!("foo", abs!("bar"), Err(PrefixMismatch)); 283 | } 284 | 285 | #[test] 286 | fn test_identity() { 287 | assert_relative_to!(".", ".", Ok("")); 288 | assert_relative_to!("../foo", "../foo", Ok("")); 289 | assert_relative_to!("./foo", "./foo", Ok("")); 290 | assert_relative_to!("/foo", "/foo", Ok("")); 291 | assert_relative_to!("foo", "foo", Ok("")); 292 | 293 | assert_relative_to!("../foo/bar/baz", "../foo/bar/baz", Ok("")); 294 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok("")); 295 | } 296 | 297 | #[test] 298 | fn test_subset() { 299 | assert_relative_to!("foo", "fo", Ok("../foo")); 300 | assert_relative_to!("fo", "foo", Ok("../fo")); 301 | } 302 | 303 | #[test] 304 | fn test_empty() { 305 | assert_relative_to!("", "", Ok("")); 306 | assert_relative_to!("foo", "", Ok("foo")); 307 | assert_relative_to!("", "foo", Ok("..")); 308 | } 309 | 310 | #[test] 311 | fn test_relative() { 312 | assert_relative_to!("../foo", "../bar", Ok("../foo")); 313 | assert_relative_to!("../foo", "../foo/bar/baz", Ok("../..")); 314 | assert_relative_to!("../foo/bar/baz", "../foo", Ok("bar/baz")); 315 | 316 | assert_relative_to!("foo/bar/baz", "foo", Ok("bar/baz")); 317 | assert_relative_to!("foo/bar/baz", "foo/bar", Ok("baz")); 318 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok("")); 319 | assert_relative_to!("foo/bar/baz", "foo/bar/baz/", Ok("")); 320 | 321 | assert_relative_to!("foo/bar/baz/", "foo", Ok("bar/baz")); 322 | assert_relative_to!("foo/bar/baz/", "foo/bar", Ok("baz")); 323 | assert_relative_to!("foo/bar/baz/", "foo/bar/baz", Ok("")); 324 | assert_relative_to!("foo/bar/baz/", "foo/bar/baz/", Ok("")); 325 | 326 | assert_relative_to!("foo/bar/baz", "foo/", Ok("bar/baz")); 327 | assert_relative_to!("foo/bar/baz", "foo/bar/", Ok("baz")); 328 | assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok("")); 329 | } 330 | 331 | #[test] 332 | fn test_current_directory() { 333 | assert_relative_to!(".", "foo", Ok("../.")); 334 | assert_relative_to!("foo", ".", Ok("foo")); 335 | assert_relative_to!("/foo", "/.", Ok("foo")); 336 | } 337 | 338 | #[test] 339 | fn assert_does_not_skip_parents() { 340 | assert_relative_to!("some/path", "some/foo/baz/path", Ok("../../../path")); 341 | assert_relative_to!("some/path", "some/foo/bar/../baz/path", Ok("../../../path")); 342 | } 343 | 344 | #[test] 345 | fn test_ambiguous_paths() { 346 | // Parent directory name is unknown, so trying to make current directory 347 | // relative to it is impossible. 348 | assert_relative_to!(".", "../..", Err(AmbiguousTraversal)); 349 | assert_relative_to!(".", "a/../..", Err(AmbiguousTraversal)); 350 | // Common prefixes are ok. 351 | assert_relative_to!("../a/..", "../a/../b", Ok("..")); 352 | assert_relative_to!("../a/../b", "../a/..", Ok("b")); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /relative-path/src/relative_path_buf.rs: -------------------------------------------------------------------------------- 1 | use core::cmp; 2 | use core::fmt; 3 | use core::hash::{Hash, Hasher}; 4 | use core::iter::FromIterator; 5 | use core::ops; 6 | use core::str; 7 | 8 | use alloc::borrow::{Borrow, Cow, ToOwned}; 9 | use alloc::boxed::Box; 10 | use alloc::string::String; 11 | 12 | #[cfg(feature = "std")] 13 | use std::path; 14 | 15 | use super::{Component, RelativePath, PARENT_STR, SEP, STEM_SEP}; 16 | 17 | #[cfg(feature = "std")] 18 | use super::{FromPathError, FromPathErrorKind}; 19 | 20 | /// Traverse the given components and apply to the provided stack. 21 | /// 22 | /// This takes '.', and '..' into account. Where '.' doesn't change the stack, and '..' pops the 23 | /// last item or further adds parent components. 24 | #[inline(always)] 25 | pub(super) fn relative_traversal<'a, C>(buf: &mut RelativePathBuf, components: C) 26 | where 27 | C: IntoIterator>, 28 | { 29 | use self::Component::{CurDir, Normal, ParentDir}; 30 | 31 | for c in components { 32 | match c { 33 | CurDir => (), 34 | ParentDir => match buf.components().next_back() { 35 | Some(Component::ParentDir) | None => { 36 | buf.push(PARENT_STR); 37 | } 38 | _ => { 39 | buf.pop(); 40 | } 41 | }, 42 | Normal(name) => { 43 | buf.push(name); 44 | } 45 | } 46 | } 47 | } 48 | 49 | /// An owned, mutable relative path. 50 | /// 51 | /// This type provides methods to manipulate relative path objects. 52 | #[derive(Clone)] 53 | pub struct RelativePathBuf { 54 | inner: String, 55 | } 56 | 57 | impl RelativePathBuf { 58 | /// Create a new relative path buffer. 59 | #[must_use] 60 | #[inline] 61 | pub fn new() -> RelativePathBuf { 62 | RelativePathBuf { 63 | inner: String::new(), 64 | } 65 | } 66 | 67 | /// Internal constructor to allocate a relative path buf with the given capacity. 68 | #[inline] 69 | pub(super) fn with_capacity(cap: usize) -> RelativePathBuf { 70 | RelativePathBuf { 71 | inner: String::with_capacity(cap), 72 | } 73 | } 74 | 75 | /// Get the length of the underlying string in bytes. 76 | #[inline] 77 | pub(super) fn len(&self) -> usize { 78 | self.inner.len() 79 | } 80 | 81 | /// Try to convert a [`Path`] to a [`RelativePathBuf`]. 82 | /// 83 | /// [`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html 84 | /// 85 | /// # Examples 86 | /// 87 | /// ``` 88 | /// use relative_path::{RelativePath, RelativePathBuf, FromPathErrorKind}; 89 | /// use std::path::Path; 90 | /// 91 | /// assert_eq!( 92 | /// Ok(RelativePath::new("foo/bar").to_owned()), 93 | /// RelativePathBuf::from_path(Path::new("foo/bar")) 94 | /// ); 95 | /// ``` 96 | /// 97 | /// # Errors 98 | /// 99 | /// This will error in case the provided path is not a relative path, which 100 | /// is identifier by it having a [`Prefix`] or [`RootDir`] component. 101 | /// 102 | /// [`Prefix`]: std::path::Component::Prefix 103 | /// [`RootDir`]: std::path::Component::RootDir 104 | #[cfg(feature = "std")] 105 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "std")))] 106 | #[inline] 107 | pub fn from_path

(path: P) -> Result 108 | where 109 | P: AsRef, 110 | { 111 | use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir}; 112 | 113 | let mut buffer = RelativePathBuf::new(); 114 | 115 | for c in path.as_ref().components() { 116 | match c { 117 | Prefix(_) | RootDir => return Err(FromPathErrorKind::NonRelative.into()), 118 | CurDir => continue, 119 | ParentDir => buffer.push(PARENT_STR), 120 | Normal(s) => buffer.push(s.to_str().ok_or(FromPathErrorKind::NonUtf8)?), 121 | } 122 | } 123 | 124 | Ok(buffer) 125 | } 126 | 127 | /// Extends `self` with `path`. 128 | /// 129 | /// # Examples 130 | /// 131 | /// ``` 132 | /// use relative_path::RelativePathBuf; 133 | /// 134 | /// let mut path = RelativePathBuf::new(); 135 | /// path.push("foo"); 136 | /// path.push("bar"); 137 | /// 138 | /// assert_eq!("foo/bar", path); 139 | /// 140 | /// let mut path = RelativePathBuf::new(); 141 | /// path.push("foo"); 142 | /// path.push("/bar"); 143 | /// 144 | /// assert_eq!("foo/bar", path); 145 | /// ``` 146 | pub fn push

(&mut self, path: P) 147 | where 148 | P: AsRef, 149 | { 150 | let other = path.as_ref(); 151 | 152 | let other = if other.starts_with_sep() { 153 | &other.inner[1..] 154 | } else { 155 | &other.inner[..] 156 | }; 157 | 158 | if !self.inner.is_empty() && !self.ends_with_sep() { 159 | self.inner.push(SEP); 160 | } 161 | 162 | self.inner.push_str(other); 163 | } 164 | 165 | /// Updates [`file_name`] to `file_name`. 166 | /// 167 | /// If [`file_name`] was [`None`], this is equivalent to pushing 168 | /// `file_name`. 169 | /// 170 | /// Otherwise it is equivalent to calling [`pop`] and then pushing 171 | /// `file_name`. The new path will be a sibling of the original path. (That 172 | /// is, it will have the same parent.) 173 | /// 174 | /// [`file_name`]: RelativePath::file_name 175 | /// [`pop`]: RelativePathBuf::pop 176 | /// [`None`]: https://doc.rust-lang.org/std/option/enum.Option.html 177 | /// 178 | /// # Examples 179 | /// 180 | /// ``` 181 | /// use relative_path::RelativePathBuf; 182 | /// 183 | /// let mut buf = RelativePathBuf::from(""); 184 | /// assert!(buf.file_name() == None); 185 | /// buf.set_file_name("bar"); 186 | /// assert_eq!(RelativePathBuf::from("bar"), buf); 187 | /// 188 | /// assert!(buf.file_name().is_some()); 189 | /// buf.set_file_name("baz.txt"); 190 | /// assert_eq!(RelativePathBuf::from("baz.txt"), buf); 191 | /// 192 | /// buf.push("bar"); 193 | /// assert!(buf.file_name().is_some()); 194 | /// buf.set_file_name("bar.txt"); 195 | /// assert_eq!(RelativePathBuf::from("baz.txt/bar.txt"), buf); 196 | /// ``` 197 | pub fn set_file_name(&mut self, file_name: S) 198 | where 199 | S: AsRef, 200 | { 201 | if self.file_name().is_some() { 202 | let popped = self.pop(); 203 | debug_assert!(popped); 204 | } 205 | 206 | self.push(file_name.as_ref()); 207 | } 208 | 209 | /// Updates [`extension`] to `extension`. 210 | /// 211 | /// Returns `false` and does nothing if 212 | /// [`file_name`][RelativePath::file_name] is [`None`], returns `true` and 213 | /// updates the extension otherwise. 214 | /// 215 | /// If [`extension`] is [`None`], the extension is added; otherwise it is 216 | /// replaced. 217 | /// 218 | /// [`extension`]: RelativePath::extension 219 | /// 220 | /// # Examples 221 | /// 222 | /// ``` 223 | /// use relative_path::{RelativePath, RelativePathBuf}; 224 | /// 225 | /// let mut p = RelativePathBuf::from("feel/the"); 226 | /// 227 | /// p.set_extension("force"); 228 | /// assert_eq!(RelativePath::new("feel/the.force"), p); 229 | /// 230 | /// p.set_extension("dark_side"); 231 | /// assert_eq!(RelativePath::new("feel/the.dark_side"), p); 232 | /// 233 | /// assert!(p.pop()); 234 | /// p.set_extension("nothing"); 235 | /// assert_eq!(RelativePath::new("feel.nothing"), p); 236 | /// ``` 237 | pub fn set_extension(&mut self, extension: S) -> bool 238 | where 239 | S: AsRef, 240 | { 241 | let file_stem = match self.file_stem() { 242 | Some(stem) => stem, 243 | None => return false, 244 | }; 245 | 246 | let end_file_stem = file_stem[file_stem.len()..].as_ptr() as usize; 247 | let start = self.inner.as_ptr() as usize; 248 | self.inner.truncate(end_file_stem.wrapping_sub(start)); 249 | 250 | let extension = extension.as_ref(); 251 | 252 | if !extension.is_empty() { 253 | self.inner.push(STEM_SEP); 254 | self.inner.push_str(extension); 255 | } 256 | 257 | true 258 | } 259 | 260 | /// Truncates `self` to [`parent`][RelativePath::parent]. 261 | /// 262 | /// # Examples 263 | /// 264 | /// ``` 265 | /// use relative_path::{RelativePath, RelativePathBuf}; 266 | /// 267 | /// let mut p = RelativePathBuf::from("test/test.rs"); 268 | /// 269 | /// assert_eq!(true, p.pop()); 270 | /// assert_eq!(RelativePath::new("test"), p); 271 | /// assert_eq!(true, p.pop()); 272 | /// assert_eq!(RelativePath::new(""), p); 273 | /// assert_eq!(false, p.pop()); 274 | /// assert_eq!(RelativePath::new(""), p); 275 | /// ``` 276 | pub fn pop(&mut self) -> bool { 277 | match self.parent().map(|p| p.inner.len()) { 278 | Some(len) => { 279 | self.inner.truncate(len); 280 | true 281 | } 282 | None => false, 283 | } 284 | } 285 | 286 | /// Coerce to a [`RelativePath`] slice. 287 | #[must_use] 288 | #[inline] 289 | pub fn as_relative_path(&self) -> &RelativePath { 290 | self 291 | } 292 | 293 | /// Consumes the `RelativePathBuf`, yielding its internal [`String`] storage. 294 | /// 295 | /// # Examples 296 | /// 297 | /// ``` 298 | /// use relative_path::RelativePathBuf; 299 | /// 300 | /// let p = RelativePathBuf::from("/the/head"); 301 | /// let string = p.into_string(); 302 | /// assert_eq!(string, "/the/head".to_owned()); 303 | /// ``` 304 | #[must_use] 305 | #[inline] 306 | pub fn into_string(self) -> String { 307 | self.inner 308 | } 309 | 310 | /// Converts this `RelativePathBuf` into a [boxed][alloc::boxed::Box] 311 | /// [`RelativePath`]. 312 | #[must_use] 313 | #[inline] 314 | pub fn into_boxed_relative_path(self) -> Box { 315 | let rw = Box::into_raw(self.inner.into_boxed_str()) as *mut RelativePath; 316 | unsafe { Box::from_raw(rw) } 317 | } 318 | } 319 | 320 | impl Default for RelativePathBuf { 321 | #[inline] 322 | fn default() -> Self { 323 | RelativePathBuf::new() 324 | } 325 | } 326 | 327 | impl<'a> From<&'a RelativePath> for Cow<'a, RelativePath> { 328 | #[inline] 329 | fn from(s: &'a RelativePath) -> Cow<'a, RelativePath> { 330 | Cow::Borrowed(s) 331 | } 332 | } 333 | 334 | impl<'a> From for Cow<'a, RelativePath> { 335 | #[inline] 336 | fn from(s: RelativePathBuf) -> Cow<'a, RelativePath> { 337 | Cow::Owned(s) 338 | } 339 | } 340 | 341 | impl fmt::Debug for RelativePathBuf { 342 | #[inline] 343 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 344 | write!(fmt, "{:?}", &self.inner) 345 | } 346 | } 347 | 348 | impl AsRef for RelativePathBuf { 349 | #[inline] 350 | fn as_ref(&self) -> &RelativePath { 351 | RelativePath::new(&self.inner) 352 | } 353 | } 354 | 355 | impl AsRef for RelativePath { 356 | #[inline] 357 | fn as_ref(&self) -> &str { 358 | &self.inner 359 | } 360 | } 361 | 362 | impl Borrow for RelativePathBuf { 363 | #[inline] 364 | fn borrow(&self) -> &RelativePath { 365 | self 366 | } 367 | } 368 | 369 | impl<'a, T: ?Sized + AsRef> From<&'a T> for RelativePathBuf { 370 | #[inline] 371 | fn from(path: &'a T) -> RelativePathBuf { 372 | RelativePathBuf { 373 | inner: path.as_ref().to_owned(), 374 | } 375 | } 376 | } 377 | 378 | impl From for RelativePathBuf { 379 | #[inline] 380 | fn from(path: String) -> RelativePathBuf { 381 | RelativePathBuf { inner: path } 382 | } 383 | } 384 | 385 | impl From for String { 386 | #[inline] 387 | fn from(path: RelativePathBuf) -> String { 388 | path.into_string() 389 | } 390 | } 391 | 392 | impl ops::Deref for RelativePathBuf { 393 | type Target = RelativePath; 394 | 395 | #[inline] 396 | fn deref(&self) -> &RelativePath { 397 | RelativePath::new(&self.inner) 398 | } 399 | } 400 | 401 | impl cmp::PartialEq for RelativePathBuf { 402 | #[inline] 403 | fn eq(&self, other: &RelativePathBuf) -> bool { 404 | self.components() == other.components() 405 | } 406 | } 407 | 408 | impl cmp::Eq for RelativePathBuf {} 409 | 410 | impl cmp::PartialOrd for RelativePathBuf { 411 | #[inline] 412 | fn partial_cmp(&self, other: &RelativePathBuf) -> Option { 413 | Some(self.cmp(other)) 414 | } 415 | } 416 | 417 | impl cmp::Ord for RelativePathBuf { 418 | #[inline] 419 | fn cmp(&self, other: &RelativePathBuf) -> cmp::Ordering { 420 | self.components().cmp(other.components()) 421 | } 422 | } 423 | 424 | impl Hash for RelativePathBuf { 425 | #[inline] 426 | fn hash(&self, h: &mut H) 427 | where 428 | H: Hasher, 429 | { 430 | self.as_relative_path().hash(h); 431 | } 432 | } 433 | 434 | impl

Extend

for RelativePathBuf 435 | where 436 | P: AsRef, 437 | { 438 | #[inline] 439 | fn extend(&mut self, iter: I) 440 | where 441 | I: IntoIterator, 442 | { 443 | iter.into_iter().for_each(move |p| self.push(p.as_ref())); 444 | } 445 | } 446 | 447 | impl

FromIterator

for RelativePathBuf 448 | where 449 | P: AsRef, 450 | { 451 | #[inline] 452 | fn from_iter(iter: I) -> RelativePathBuf 453 | where 454 | I: IntoIterator, 455 | { 456 | let mut buf = RelativePathBuf::new(); 457 | buf.extend(iter); 458 | buf 459 | } 460 | } 461 | 462 | impl fmt::Display for RelativePathBuf { 463 | #[inline] 464 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 465 | fmt::Display::fmt(&self.inner, f) 466 | } 467 | } 468 | 469 | /// [`AsRef`] implementation for [`RelativePathBuf`]. 470 | /// 471 | /// # Examples 472 | /// 473 | /// ``` 474 | /// use relative_path::RelativePathBuf; 475 | /// 476 | /// let path = RelativePathBuf::from("foo/bar"); 477 | /// let string: &str = path.as_ref(); 478 | /// assert_eq!(string, "foo/bar"); 479 | /// ``` 480 | impl AsRef for RelativePathBuf { 481 | #[inline] 482 | fn as_ref(&self) -> &str { 483 | &self.inner 484 | } 485 | } 486 | 487 | /// [`serde::ser::Serialize`] implementation for [`RelativePathBuf`]. 488 | /// 489 | /// ``` 490 | /// use serde::Serialize; 491 | /// use relative_path::RelativePathBuf; 492 | /// 493 | /// #[derive(Serialize)] 494 | /// struct Document { 495 | /// path: RelativePathBuf, 496 | /// } 497 | /// ``` 498 | #[cfg(feature = "serde")] 499 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "serde")))] 500 | impl serde::ser::Serialize for RelativePathBuf { 501 | #[inline] 502 | fn serialize(&self, serializer: S) -> Result 503 | where 504 | S: serde::ser::Serializer, 505 | { 506 | serializer.serialize_str(&self.inner) 507 | } 508 | } 509 | 510 | /// [`serde::de::Deserialize`] implementation for [`RelativePathBuf`]. 511 | /// 512 | /// ``` 513 | /// use serde::Deserialize; 514 | /// use relative_path::RelativePathBuf; 515 | /// 516 | /// #[derive(Deserialize)] 517 | /// struct Document { 518 | /// path: RelativePathBuf, 519 | /// } 520 | /// ``` 521 | #[cfg(feature = "serde")] 522 | #[cfg_attr(relative_path_docsrs, doc(cfg(feature = "serde")))] 523 | impl<'de> serde::de::Deserialize<'de> for RelativePathBuf { 524 | fn deserialize(deserializer: D) -> Result 525 | where 526 | D: serde::de::Deserializer<'de>, 527 | { 528 | struct Visitor; 529 | 530 | impl serde::de::Visitor<'_> for Visitor { 531 | type Value = RelativePathBuf; 532 | 533 | #[inline] 534 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 535 | formatter.write_str("a relative path") 536 | } 537 | 538 | #[inline] 539 | fn visit_string(self, input: String) -> Result 540 | where 541 | E: serde::de::Error, 542 | { 543 | Ok(RelativePathBuf::from(input)) 544 | } 545 | 546 | #[inline] 547 | fn visit_str(self, input: &str) -> Result 548 | where 549 | E: serde::de::Error, 550 | { 551 | Ok(RelativePathBuf::from(input.to_owned())) 552 | } 553 | } 554 | 555 | deserializer.deserialize_str(Visitor) 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /relative-path/src/tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_lines)] 2 | 3 | use super::*; 4 | 5 | use std::format; 6 | use std::path::Path; 7 | use std::rc::Rc; 8 | use std::string::ToString; 9 | use std::sync::Arc; 10 | use std::vec; 11 | use std::vec::Vec; 12 | 13 | macro_rules! t( 14 | ($path:expr, iter: $iter:expr) => ( 15 | { 16 | let path = RelativePath::new($path); 17 | 18 | // Forward iteration 19 | let comps = path.iter().map(str::to_string).collect::>(); 20 | let exp: &[&str] = &$iter; 21 | let exps = exp.iter().map(|s| s.to_string()).collect::>(); 22 | assert!(comps == exps, "iter: Expected {:?}, found {:?}", 23 | exps, comps); 24 | 25 | // Reverse iteration 26 | let comps = RelativePath::new($path).iter().rev().map(str::to_string) 27 | .collect::>(); 28 | let exps = exps.into_iter().rev().collect::>(); 29 | assert!(comps == exps, "iter().rev(): Expected {:?}, found {:?}", 30 | exps, comps); 31 | } 32 | ); 33 | 34 | ($path:expr, parent: $parent:expr, file_name: $file:expr) => ( 35 | { 36 | let path = RelativePath::new($path); 37 | 38 | let parent = path.parent().map(|p| p.as_str()); 39 | let exp_parent: Option<&str> = $parent; 40 | assert!(parent == exp_parent, "parent: Expected {:?}, found {:?}", 41 | exp_parent, parent); 42 | 43 | let file = path.file_name(); 44 | let exp_file: Option<&str> = $file; 45 | assert!(file == exp_file, "file_name: Expected {:?}, found {:?}", 46 | exp_file, file); 47 | } 48 | ); 49 | 50 | ($path:expr, file_stem: $file_stem:expr, extension: $extension:expr) => ( 51 | { 52 | let path = RelativePath::new($path); 53 | 54 | let stem = path.file_stem(); 55 | let exp_stem: Option<&str> = $file_stem; 56 | assert!(stem == exp_stem, "file_stem: Expected {:?}, found {:?}", 57 | exp_stem, stem); 58 | 59 | let ext = path.extension(); 60 | let exp_ext: Option<&str> = $extension; 61 | assert!(ext == exp_ext, "extension: Expected {:?}, found {:?}", 62 | exp_ext, ext); 63 | } 64 | ); 65 | 66 | ($path:expr, iter: $iter:expr, 67 | parent: $parent:expr, file_name: $file:expr, 68 | file_stem: $file_stem:expr, extension: $extension:expr) => ( 69 | { 70 | t!($path, iter: $iter); 71 | t!($path, parent: $parent, file_name: $file); 72 | t!($path, file_stem: $file_stem, extension: $extension); 73 | } 74 | ); 75 | ); 76 | 77 | fn assert_components(components: &[&str], path: &RelativePath) { 78 | let components = components 79 | .iter() 80 | .copied() 81 | .map(Component::Normal) 82 | .collect::>(); 83 | let result: Vec<_> = path.components().collect(); 84 | assert_eq!(&components[..], &result[..]); 85 | } 86 | 87 | fn rp(input: &str) -> &RelativePath { 88 | RelativePath::new(input) 89 | } 90 | 91 | #[test] 92 | #[allow(clippy::cognitive_complexity)] 93 | pub fn test_decompositions() { 94 | t!("", 95 | iter: [], 96 | parent: None, 97 | file_name: None, 98 | file_stem: None, 99 | extension: None 100 | ); 101 | 102 | t!("foo", 103 | iter: ["foo"], 104 | parent: Some(""), 105 | file_name: Some("foo"), 106 | file_stem: Some("foo"), 107 | extension: None 108 | ); 109 | 110 | t!("/", 111 | iter: [], 112 | parent: Some(""), 113 | file_name: None, 114 | file_stem: None, 115 | extension: None 116 | ); 117 | 118 | t!("/foo", 119 | iter: ["foo"], 120 | parent: Some(""), 121 | file_name: Some("foo"), 122 | file_stem: Some("foo"), 123 | extension: None 124 | ); 125 | 126 | t!("foo/", 127 | iter: ["foo"], 128 | parent: Some(""), 129 | file_name: Some("foo"), 130 | file_stem: Some("foo"), 131 | extension: None 132 | ); 133 | 134 | t!("/foo/", 135 | iter: ["foo"], 136 | parent: Some(""), 137 | file_name: Some("foo"), 138 | file_stem: Some("foo"), 139 | extension: None 140 | ); 141 | 142 | t!("foo/bar", 143 | iter: ["foo", "bar"], 144 | parent: Some("foo"), 145 | file_name: Some("bar"), 146 | file_stem: Some("bar"), 147 | extension: None 148 | ); 149 | 150 | t!("/foo/bar", 151 | iter: ["foo", "bar"], 152 | parent: Some("/foo"), 153 | file_name: Some("bar"), 154 | file_stem: Some("bar"), 155 | extension: None 156 | ); 157 | 158 | t!("///foo///", 159 | iter: ["foo"], 160 | parent: Some(""), 161 | file_name: Some("foo"), 162 | file_stem: Some("foo"), 163 | extension: None 164 | ); 165 | 166 | t!("///foo///bar", 167 | iter: ["foo", "bar"], 168 | parent: Some("///foo"), 169 | file_name: Some("bar"), 170 | file_stem: Some("bar"), 171 | extension: None 172 | ); 173 | 174 | t!("./.", 175 | iter: [".", "."], 176 | parent: Some(""), 177 | file_name: None, 178 | file_stem: None, 179 | extension: None 180 | ); 181 | 182 | t!("/..", 183 | iter: [".."], 184 | parent: Some(""), 185 | file_name: None, 186 | file_stem: None, 187 | extension: None 188 | ); 189 | 190 | t!("../", 191 | iter: [".."], 192 | parent: Some(""), 193 | file_name: None, 194 | file_stem: None, 195 | extension: None 196 | ); 197 | 198 | t!("foo/.", 199 | iter: ["foo", "."], 200 | parent: Some(""), 201 | file_name: Some("foo"), 202 | file_stem: Some("foo"), 203 | extension: None 204 | ); 205 | 206 | t!("foo/..", 207 | iter: ["foo", ".."], 208 | parent: Some("foo"), 209 | file_name: None, 210 | file_stem: None, 211 | extension: None 212 | ); 213 | 214 | t!("foo/./", 215 | iter: ["foo", "."], 216 | parent: Some(""), 217 | file_name: Some("foo"), 218 | file_stem: Some("foo"), 219 | extension: None 220 | ); 221 | 222 | t!("foo/./bar", 223 | iter: ["foo", ".", "bar"], 224 | parent: Some("foo/."), 225 | file_name: Some("bar"), 226 | file_stem: Some("bar"), 227 | extension: None 228 | ); 229 | 230 | t!("foo/../", 231 | iter: ["foo", ".."], 232 | parent: Some("foo"), 233 | file_name: None, 234 | file_stem: None, 235 | extension: None 236 | ); 237 | 238 | t!("foo/../bar", 239 | iter: ["foo", "..", "bar"], 240 | parent: Some("foo/.."), 241 | file_name: Some("bar"), 242 | file_stem: Some("bar"), 243 | extension: None 244 | ); 245 | 246 | t!("./a", 247 | iter: [".", "a"], 248 | parent: Some("."), 249 | file_name: Some("a"), 250 | file_stem: Some("a"), 251 | extension: None 252 | ); 253 | 254 | t!(".", 255 | iter: ["."], 256 | parent: Some(""), 257 | file_name: None, 258 | file_stem: None, 259 | extension: None 260 | ); 261 | 262 | t!("./", 263 | iter: ["."], 264 | parent: Some(""), 265 | file_name: None, 266 | file_stem: None, 267 | extension: None 268 | ); 269 | 270 | t!("a/b", 271 | iter: ["a", "b"], 272 | parent: Some("a"), 273 | file_name: Some("b"), 274 | file_stem: Some("b"), 275 | extension: None 276 | ); 277 | 278 | t!("a//b", 279 | iter: ["a", "b"], 280 | parent: Some("a"), 281 | file_name: Some("b"), 282 | file_stem: Some("b"), 283 | extension: None 284 | ); 285 | 286 | t!("a/./b", 287 | iter: ["a", ".", "b"], 288 | parent: Some("a/."), 289 | file_name: Some("b"), 290 | file_stem: Some("b"), 291 | extension: None 292 | ); 293 | 294 | t!("a/b/c", 295 | iter: ["a", "b", "c"], 296 | parent: Some("a/b"), 297 | file_name: Some("c"), 298 | file_stem: Some("c"), 299 | extension: None 300 | ); 301 | 302 | t!(".foo", 303 | iter: [".foo"], 304 | parent: Some(""), 305 | file_name: Some(".foo"), 306 | file_stem: Some(".foo"), 307 | extension: None 308 | ); 309 | } 310 | 311 | #[test] 312 | pub fn test_stem_ext() { 313 | t!("foo", 314 | file_stem: Some("foo"), 315 | extension: None 316 | ); 317 | 318 | t!("foo.", 319 | file_stem: Some("foo"), 320 | extension: Some("") 321 | ); 322 | 323 | t!(".foo", 324 | file_stem: Some(".foo"), 325 | extension: None 326 | ); 327 | 328 | t!("foo.txt", 329 | file_stem: Some("foo"), 330 | extension: Some("txt") 331 | ); 332 | 333 | t!("foo.bar.txt", 334 | file_stem: Some("foo.bar"), 335 | extension: Some("txt") 336 | ); 337 | 338 | t!("foo.bar.", 339 | file_stem: Some("foo.bar"), 340 | extension: Some("") 341 | ); 342 | 343 | t!(".", file_stem: None, extension: None); 344 | 345 | t!("..", file_stem: None, extension: None); 346 | 347 | t!("", file_stem: None, extension: None); 348 | } 349 | 350 | #[test] 351 | pub fn test_set_file_name() { 352 | macro_rules! tfn( 353 | ($path:expr, $file:expr, $expected:expr) => ( { 354 | let mut p = RelativePathBuf::from($path); 355 | p.set_file_name($file); 356 | assert!(p.as_str() == $expected, 357 | "setting file name of {:?} to {:?}: Expected {:?}, got {:?}", 358 | $path, $file, $expected, 359 | p.as_str()); 360 | }); 361 | ); 362 | 363 | tfn!("foo", "foo", "foo"); 364 | tfn!("foo", "bar", "bar"); 365 | tfn!("foo", "", ""); 366 | tfn!("", "foo", "foo"); 367 | 368 | tfn!(".", "foo", "./foo"); 369 | tfn!("foo/", "bar", "bar"); 370 | tfn!("foo/.", "bar", "bar"); 371 | tfn!("..", "foo", "../foo"); 372 | tfn!("foo/..", "bar", "foo/../bar"); 373 | tfn!("/", "foo", "/foo"); 374 | } 375 | 376 | #[test] 377 | pub fn test_set_extension() { 378 | macro_rules! tse( 379 | ($path:expr, $ext:expr, $expected:expr, $output:expr) => ( { 380 | let mut p = RelativePathBuf::from($path); 381 | let output = p.set_extension($ext); 382 | assert!(p.as_str() == $expected && output == $output, 383 | "setting extension of {:?} to {:?}: Expected {:?}/{:?}, got {:?}/{:?}", 384 | $path, $ext, $expected, $output, 385 | p.as_str(), output); 386 | }); 387 | ); 388 | 389 | tse!("foo", "txt", "foo.txt", true); 390 | tse!("foo.bar", "txt", "foo.txt", true); 391 | tse!("foo.bar.baz", "txt", "foo.bar.txt", true); 392 | tse!(".test", "txt", ".test.txt", true); 393 | tse!("foo.txt", "", "foo", true); 394 | tse!("foo", "", "foo", true); 395 | tse!("", "foo", "", false); 396 | tse!(".", "foo", ".", false); 397 | tse!("foo/", "bar", "foo.bar", true); 398 | tse!("foo/.", "bar", "foo.bar", true); 399 | tse!("..", "foo", "..", false); 400 | tse!("foo/..", "bar", "foo/..", false); 401 | tse!("/", "foo", "/", false); 402 | } 403 | 404 | #[test] 405 | fn test_eq_recievers() { 406 | use std::borrow::Cow; 407 | 408 | let borrowed: &RelativePath = RelativePath::new("foo/bar"); 409 | let mut owned: RelativePathBuf = RelativePathBuf::new(); 410 | owned.push("foo"); 411 | owned.push("bar"); 412 | let borrowed_cow: Cow = borrowed.into(); 413 | let owned_cow: Cow = owned.clone().into(); 414 | 415 | macro_rules! t { 416 | ($($current:expr),+) => { 417 | $( 418 | assert_eq!($current, borrowed); 419 | assert_eq!($current, owned); 420 | assert_eq!($current, borrowed_cow); 421 | assert_eq!($current, owned_cow); 422 | )+ 423 | } 424 | } 425 | 426 | t!(borrowed, owned, borrowed_cow, owned_cow); 427 | } 428 | 429 | #[test] 430 | #[allow(clippy::cognitive_complexity)] 431 | pub fn test_compare() { 432 | use std::collections::hash_map::DefaultHasher; 433 | use std::hash::{Hash, Hasher}; 434 | 435 | fn hash(t: T) -> u64 { 436 | let mut s = DefaultHasher::new(); 437 | t.hash(&mut s); 438 | s.finish() 439 | } 440 | 441 | macro_rules! tc( 442 | ($path1:expr, $path2:expr, eq: $eq:expr, 443 | starts_with: $starts_with:expr, ends_with: $ends_with:expr, 444 | relative_from: $relative_from:expr) => ({ 445 | let path1 = RelativePath::new($path1); 446 | let path2 = RelativePath::new($path2); 447 | 448 | let eq = path1 == path2; 449 | assert!(eq == $eq, "{:?} == {:?}, expected {:?}, got {:?}", 450 | $path1, $path2, $eq, eq); 451 | assert!($eq == (hash(path1) == hash(path2)), 452 | "{:?} == {:?}, expected {:?}, got {} and {}", 453 | $path1, $path2, $eq, hash(path1), hash(path2)); 454 | 455 | let starts_with = path1.starts_with(path2); 456 | assert!(starts_with == $starts_with, 457 | "{:?}.starts_with({:?}), expected {:?}, got {:?}", $path1, $path2, 458 | $starts_with, starts_with); 459 | 460 | let ends_with = path1.ends_with(path2); 461 | assert!(ends_with == $ends_with, 462 | "{:?}.ends_with({:?}), expected {:?}, got {:?}", $path1, $path2, 463 | $ends_with, ends_with); 464 | 465 | let relative_from = path1.strip_prefix(path2) 466 | .map(|p| p.as_str()) 467 | .ok(); 468 | let exp: Option<&str> = $relative_from; 469 | assert!(relative_from == exp, 470 | "{:?}.strip_prefix({:?}), expected {:?}, got {:?}", 471 | $path1, $path2, exp, relative_from); 472 | }); 473 | ); 474 | 475 | tc!("", "", 476 | eq: true, 477 | starts_with: true, 478 | ends_with: true, 479 | relative_from: Some("") 480 | ); 481 | 482 | tc!("foo", "", 483 | eq: false, 484 | starts_with: true, 485 | ends_with: true, 486 | relative_from: Some("foo") 487 | ); 488 | 489 | tc!("", "foo", 490 | eq: false, 491 | starts_with: false, 492 | ends_with: false, 493 | relative_from: None 494 | ); 495 | 496 | tc!("foo", "foo", 497 | eq: true, 498 | starts_with: true, 499 | ends_with: true, 500 | relative_from: Some("") 501 | ); 502 | 503 | tc!("foo/", "foo", 504 | eq: true, 505 | starts_with: true, 506 | ends_with: true, 507 | relative_from: Some("") 508 | ); 509 | 510 | tc!("foo/bar", "foo", 511 | eq: false, 512 | starts_with: true, 513 | ends_with: false, 514 | relative_from: Some("bar") 515 | ); 516 | 517 | tc!("foo/bar/baz", "foo/bar", 518 | eq: false, 519 | starts_with: true, 520 | ends_with: false, 521 | relative_from: Some("baz") 522 | ); 523 | 524 | tc!("foo/bar", "foo/bar/baz", 525 | eq: false, 526 | starts_with: false, 527 | ends_with: false, 528 | relative_from: None 529 | ); 530 | } 531 | 532 | #[test] 533 | fn test_join() { 534 | assert_components(&["foo", "bar", "baz"], &rp("foo/bar").join("baz///")); 535 | assert_components( 536 | &["hello", "world", "foo", "bar", "baz"], 537 | &rp("hello/world").join("///foo/bar/baz"), 538 | ); 539 | assert_components(&["foo", "bar", "baz"], &rp("").join("foo/bar/baz")); 540 | } 541 | 542 | #[test] 543 | fn test_components_iterator() { 544 | use self::Component::*; 545 | 546 | assert_eq!( 547 | vec![Normal("hello"), Normal("world")], 548 | rp("/hello///world//").components().collect::>() 549 | ); 550 | } 551 | 552 | #[test] 553 | fn test_to_path_buf() { 554 | let path = rp("/hello///world//"); 555 | let path_buf = path.to_path("."); 556 | let expected = Path::new(".").join("hello").join("world"); 557 | assert_eq!(expected, path_buf); 558 | } 559 | 560 | #[test] 561 | fn test_eq() { 562 | assert_eq!(rp("//foo///bar"), rp("/foo/bar")); 563 | assert_eq!(rp("foo///bar"), rp("foo/bar")); 564 | assert_eq!(rp("foo"), rp("foo")); 565 | assert_eq!(rp("foo"), rp("foo").to_relative_path_buf()); 566 | } 567 | 568 | #[test] 569 | fn test_next_back() { 570 | use self::Component::*; 571 | 572 | let mut it = rp("baz/bar///foo").components(); 573 | assert_eq!(Some(Normal("foo")), it.next_back()); 574 | assert_eq!(Some(Normal("bar")), it.next_back()); 575 | assert_eq!(Some(Normal("baz")), it.next_back()); 576 | assert_eq!(None, it.next_back()); 577 | } 578 | 579 | #[test] 580 | fn test_parent() { 581 | let path = rp("baz/./bar/foo//./."); 582 | 583 | assert_eq!(Some(rp("baz/./bar")), path.parent()); 584 | assert_eq!( 585 | Some(rp("baz/.")), 586 | path.parent().and_then(RelativePath::parent) 587 | ); 588 | assert_eq!( 589 | Some(rp("")), 590 | path.parent() 591 | .and_then(RelativePath::parent) 592 | .and_then(RelativePath::parent) 593 | ); 594 | assert_eq!( 595 | None, 596 | path.parent() 597 | .and_then(RelativePath::parent) 598 | .and_then(RelativePath::parent) 599 | .and_then(RelativePath::parent) 600 | ); 601 | } 602 | 603 | #[test] 604 | fn test_relative_path_buf() { 605 | assert_eq!( 606 | rp("hello/world/."), 607 | rp("/hello///world//").to_owned().join(".") 608 | ); 609 | } 610 | 611 | #[test] 612 | fn test_normalize() { 613 | assert_eq!(rp("c/d"), rp("a/.././b/../c/d").normalize()); 614 | } 615 | 616 | #[test] 617 | fn test_relative_to() { 618 | assert_eq!( 619 | rp("foo/foo/bar"), 620 | rp("foo/bar").join_normalized("../foo/bar") 621 | ); 622 | 623 | assert_eq!( 624 | rp("../c/e"), 625 | rp("x/y").join_normalized("../../a/b/../../../c/d/../e") 626 | ); 627 | } 628 | 629 | #[test] 630 | fn test_from() { 631 | assert_eq!( 632 | rp("foo/bar").to_owned(), 633 | RelativePathBuf::from(String::from("foo/bar")), 634 | ); 635 | 636 | assert_eq!( 637 | RelativePathBuf::from(rp("foo/bar")), 638 | RelativePathBuf::from("foo/bar"), 639 | ); 640 | 641 | assert_eq!(rp("foo/bar").to_owned(), RelativePathBuf::from("foo/bar"),); 642 | 643 | assert_eq!(&*Box::::from(rp("foo/bar")), rp("foo/bar")); 644 | assert_eq!( 645 | &*Box::::from(RelativePathBuf::from("foo/bar")), 646 | rp("foo/bar") 647 | ); 648 | 649 | assert_eq!(&*Arc::::from(rp("foo/bar")), rp("foo/bar")); 650 | assert_eq!( 651 | &*Arc::::from(RelativePathBuf::from("foo/bar")), 652 | rp("foo/bar") 653 | ); 654 | 655 | assert_eq!(&*Rc::::from(rp("foo/bar")), rp("foo/bar")); 656 | assert_eq!( 657 | &*Rc::::from(RelativePathBuf::from("foo/bar")), 658 | rp("foo/bar") 659 | ); 660 | } 661 | 662 | #[test] 663 | fn test_relative_path_asref_str() { 664 | assert_eq!( 665 | >::as_ref(rp("foo/bar")), 666 | "foo/bar" 667 | ); 668 | } 669 | 670 | #[test] 671 | fn test_default() { 672 | assert_eq!(RelativePathBuf::new(), RelativePathBuf::default(),); 673 | } 674 | 675 | #[test] 676 | pub fn test_push() { 677 | macro_rules! tp( 678 | ($path:expr, $push:expr, $expected:expr) => ( { 679 | let mut actual = RelativePathBuf::from($path); 680 | actual.push($push); 681 | assert!(actual.as_str() == $expected, 682 | "pushing {:?} onto {:?}: Expected {:?}, got {:?}", 683 | $push, $path, $expected, actual.as_str()); 684 | }); 685 | ); 686 | 687 | tp!("", "foo", "foo"); 688 | tp!("foo", "bar", "foo/bar"); 689 | tp!("foo/", "bar", "foo/bar"); 690 | tp!("foo//", "bar", "foo//bar"); 691 | tp!("foo/.", "bar", "foo/./bar"); 692 | tp!("foo./.", "bar", "foo././bar"); 693 | tp!("foo", "", "foo/"); 694 | tp!("foo", ".", "foo/."); 695 | tp!("foo", "..", "foo/.."); 696 | } 697 | 698 | #[test] 699 | pub fn test_pop() { 700 | macro_rules! tp( 701 | ($path:expr, $expected:expr, $output:expr) => ( { 702 | let mut actual = RelativePathBuf::from($path); 703 | let output = actual.pop(); 704 | assert!(actual.as_str() == $expected && output == $output, 705 | "popping from {:?}: Expected {:?}/{:?}, got {:?}/{:?}", 706 | $path, $expected, $output, 707 | actual.as_str(), output); 708 | }); 709 | ); 710 | 711 | tp!("", "", false); 712 | tp!("/", "", true); 713 | tp!("foo", "", true); 714 | tp!(".", "", true); 715 | tp!("/foo", "", true); 716 | tp!("/foo/bar", "/foo", true); 717 | tp!("/foo/bar/.", "/foo", true); 718 | tp!("foo/bar", "foo", true); 719 | tp!("foo/.", "", true); 720 | tp!("foo//bar", "foo", true); 721 | } 722 | 723 | #[test] 724 | pub fn test_display() { 725 | // NB: display delegated to the underlying string. 726 | assert_eq!(RelativePathBuf::from("foo/bar").to_string(), "foo/bar"); 727 | assert_eq!(RelativePath::new("foo/bar").to_string(), "foo/bar"); 728 | 729 | assert_eq!(format!("{}", RelativePathBuf::from("foo/bar")), "foo/bar"); 730 | assert_eq!(format!("{}", RelativePath::new("foo/bar")), "foo/bar"); 731 | } 732 | 733 | #[cfg(unix)] 734 | #[test] 735 | pub fn test_unix_from_path() { 736 | use std::ffi::OsStr; 737 | use std::os::unix::ffi::OsStrExt; 738 | 739 | assert_eq!( 740 | Err(FromPathErrorKind::NonRelative.into()), 741 | RelativePath::from_path("/foo/bar") 742 | ); 743 | 744 | // Continuation byte without continuation. 745 | let non_utf8 = OsStr::from_bytes(&[0x80u8]); 746 | 747 | assert_eq!( 748 | Err(FromPathErrorKind::NonUtf8.into()), 749 | RelativePath::from_path(non_utf8) 750 | ); 751 | } 752 | 753 | #[cfg(windows)] 754 | #[test] 755 | pub fn test_windows_from_path() { 756 | assert_eq!( 757 | Err(FromPathErrorKind::NonRelative.into()), 758 | RelativePath::from_path("c:\\foo\\bar") 759 | ); 760 | 761 | assert_eq!( 762 | Err(FromPathErrorKind::BadSeparator.into()), 763 | RelativePath::from_path("foo\\bar") 764 | ); 765 | } 766 | 767 | #[cfg(unix)] 768 | #[test] 769 | pub fn test_unix_owned_from_path() { 770 | use std::ffi::OsStr; 771 | use std::os::unix::ffi::OsStrExt; 772 | 773 | assert_eq!( 774 | Err(FromPathErrorKind::NonRelative.into()), 775 | RelativePathBuf::from_path(Path::new("/foo/bar")) 776 | ); 777 | 778 | // Continuation byte without continuation. 779 | let non_utf8 = OsStr::from_bytes(&[0x80u8]); 780 | 781 | assert_eq!( 782 | Err(FromPathErrorKind::NonUtf8.into()), 783 | RelativePathBuf::from_path(Path::new(non_utf8)) 784 | ); 785 | } 786 | 787 | #[cfg(windows)] 788 | #[test] 789 | pub fn test_windows_owned_from_path() { 790 | assert_eq!( 791 | Err(FromPathErrorKind::NonRelative.into()), 792 | RelativePathBuf::from_path(Path::new("c:\\foo\\bar")) 793 | ); 794 | } 795 | --------------------------------------------------------------------------------