├── .gitignore ├── .rustfmt.toml ├── rust-toolchain.toml ├── README.md ├── benches └── bench.rs ├── Cargo.toml ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── LICENSE ├── src ├── fs.rs └── lib.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.90.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `deno_path_util` 2 | 3 | Common path utilities used across Deno's repos. 4 | 5 | ## Versioning Strategy 6 | 7 | This crate does not follow semver so if you're outside the Deno org make sure to pin it to a patch version. 8 | Instead a versioning strategy that optimizes for more efficient maintenance is 9 | used: 10 | 11 | - Do the dependencies of [Deno](https://github.com/denoland/deno) compile? 12 | - If yes, it's a patch release. 13 | - If no, it's a minor release. 14 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::path::PathBuf; 3 | 4 | use deno_path_util::normalize_path; 5 | 6 | fn main() { 7 | // Run registered benchmarks. 8 | divan::main(); 9 | } 10 | 11 | #[divan::bench(sample_size = 51200)] 12 | fn bench_normalize_path_changed(bencher: divan::Bencher) { 13 | let path = PathBuf::from("/testing/../this/./out/testing/../test"); 14 | bencher.bench(|| normalize_path(Cow::Borrowed(&path))) 15 | } 16 | 17 | #[divan::bench(sample_size = 51200)] 18 | fn bench_normalize_path_no_change(bencher: divan::Bencher) { 19 | let path = if cfg!(windows) { 20 | PathBuf::from("C:\\testing\\this\\out\\testing\\test") 21 | } else { 22 | PathBuf::from("/testing/this/out/testing/test") 23 | }; 24 | bencher.bench(|| normalize_path(Cow::Borrowed(&path))) 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_path_util" 3 | description = "Path utilities used in Deno" 4 | version = "0.6.4" 5 | edition = "2024" 6 | authors = ["the Deno authors"] 7 | license = "MIT" 8 | repository = "https://github.com/denoland/deno_path_util" 9 | 10 | [features] 11 | 12 | [dependencies] 13 | percent-encoding = "2.3.0" 14 | thiserror = "2" 15 | sys_traits.workspace = true 16 | deno_error = { version = "0.7.0", features = ["url"] } 17 | url = "2.5.1" 18 | 19 | [dev-dependencies] 20 | pretty_assertions = "1.4.0" 21 | sys_traits = { workspace = true, features = ["getrandom", "memory", "real"] } 22 | tempfile = "3.4.0" 23 | divan = "0.1.21" 24 | 25 | [workspace] 26 | members = ["."] 27 | 28 | [workspace.dependencies] 29 | sys_traits = "0.1.17" 30 | 31 | [[bench]] 32 | name = "bench" 33 | harness = false 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: "Kind of release" 8 | default: "minor" 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v2 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.20.0/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2025 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rust: 7 | name: deno_path_util-${{ matrix.os }} 8 | if: | 9 | (github.event_name == 'push' || !startsWith(github.event.pull_request.head.label, 'denoland:')) 10 | && github.ref_name != 'deno_path_util' 11 | && !startsWith(github.ref, 'refs/tags/deno/') 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 15 14 | strategy: 15 | matrix: 16 | os: [macOS-latest, ubuntu-latest, windows-latest] 17 | 18 | env: 19 | CARGO_INCREMENTAL: 0 20 | GH_ACTIONS: 1 21 | RUST_BACKTRACE: full 22 | RUSTFLAGS: -D warnings 23 | 24 | steps: 25 | - name: Clone repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Install Rust 29 | uses: dsherret/rust-toolchain-file@v1 30 | 31 | - uses: Swatinem/rust-cache@v2 32 | with: 33 | save-if: ${{ github.ref == 'refs/heads/main' }} 34 | 35 | - name: Format 36 | if: contains(matrix.os, 'ubuntu') 37 | run: | 38 | cargo fmt -- --check 39 | 40 | - name: Check builds Wasm 41 | if: contains(matrix.os, 'ubuntu') 42 | run: | 43 | rustup target add wasm32-unknown-unknown 44 | cargo check --all-features --target wasm32-unknown-unknown 45 | 46 | # Some tests mutate the current working directory while some others rely 47 | # on it; we run them sequentially to avoid flakiness. 48 | - name: Cargo test 49 | run: cargo test --locked --release --all-features --bins --tests --examples -- --test-threads=1 50 | 51 | - name: Lint 52 | if: contains(matrix.os, 'ubuntu') 53 | run: | 54 | cargo clippy --locked --all-features --all-targets -- -D clippy::all 55 | 56 | - name: Cargo publish 57 | if: | 58 | contains(matrix.os, 'ubuntu') && 59 | github.repository == 'denoland/deno_path_util' && 60 | startsWith(github.ref, 'refs/tags/') 61 | env: 62 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 63 | run: cargo publish 64 | 65 | - name: Get tag version 66 | if: contains(matrix.os, 'ubuntu') && startsWith(github.ref, 'refs/tags/') 67 | id: get_tag_version 68 | run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> "$GITHUB_OUTPUT" 69 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. MIT license. 2 | 3 | use std::io::Error; 4 | use std::io::ErrorKind; 5 | use std::io::Write; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | 9 | use sys_traits::FsCanonicalize; 10 | use sys_traits::FsCreateDirAll; 11 | use sys_traits::FsMetadata; 12 | use sys_traits::FsOpen; 13 | use sys_traits::FsRemoveFile; 14 | use sys_traits::FsRename; 15 | use sys_traits::OpenOptions; 16 | use sys_traits::SystemRandom; 17 | use sys_traits::ThreadSleep; 18 | 19 | use crate::get_atomic_path; 20 | 21 | /// Canonicalizes a path which might be non-existent by going up the 22 | /// ancestors until it finds a directory that exists, canonicalizes 23 | /// that path, then adds back the remaining path components. 24 | /// 25 | /// Note: When using this, you should be aware that a symlink may 26 | /// subsequently be created along this path by some other code. 27 | pub fn canonicalize_path_maybe_not_exists( 28 | sys: &impl FsCanonicalize, 29 | mut path: &Path, 30 | ) -> std::io::Result { 31 | let mut names_stack = Vec::new(); 32 | loop { 33 | match sys.fs_canonicalize(path) { 34 | Ok(mut canonicalized_path) => { 35 | for name in names_stack.into_iter().rev() { 36 | canonicalized_path = canonicalized_path.join(name); 37 | } 38 | return Ok(canonicalized_path); 39 | } 40 | Err(err) if err.kind() == ErrorKind::NotFound => { 41 | names_stack.push(match path.file_name() { 42 | Some(name) => name.to_owned(), 43 | None => return Err(err), 44 | }); 45 | path = match path.parent() { 46 | // When the provided path is a relative path (e.g. `foo/bar.txt`), 47 | // `path.parent()` ends up being the empty string as documented in 48 | // `std::path::Path::parent()` after going up the ancestor path. 49 | // In this case, what this function should return is a path joining 50 | // the current path with the provided path i.e. `{cwd}/foo/bar.txt`, 51 | // so we set `path` to `.` to indicate the current directory. 52 | Some(parent) if parent.as_os_str().is_empty() => Path::new("."), 53 | Some(parent) => parent, 54 | None => return Err(err), 55 | }; 56 | } 57 | Err(err) => return Err(err), 58 | } 59 | } 60 | } 61 | 62 | #[sys_traits::auto_impl] 63 | pub trait AtomicWriteFileWithRetriesSys: 64 | AtomicWriteFileSys + ThreadSleep 65 | { 66 | } 67 | 68 | pub fn atomic_write_file_with_retries( 69 | sys: &TSys, 70 | file_path: &Path, 71 | data: &[u8], 72 | mode: u32, 73 | ) -> std::io::Result<()> { 74 | let mut count = 0; 75 | loop { 76 | match atomic_write_file(sys, file_path, data, mode) { 77 | Ok(()) => return Ok(()), 78 | Err(err) => { 79 | if count >= 5 { 80 | // too many retries, return the error 81 | return Err(err); 82 | } 83 | count += 1; 84 | let sleep_ms = std::cmp::min(50, 10 * count); 85 | sys.thread_sleep(std::time::Duration::from_millis(sleep_ms)); 86 | } 87 | } 88 | } 89 | } 90 | 91 | #[sys_traits::auto_impl] 92 | pub trait AtomicWriteFileSys: 93 | FsCreateDirAll + FsMetadata + FsOpen + FsRemoveFile + FsRename + SystemRandom 94 | { 95 | } 96 | 97 | /// Writes the file to the file system at a temporary path, then 98 | /// renames it to the destination in a single sys call in order 99 | /// to never leave the file system in a corrupted state. 100 | /// 101 | /// This also handles creating the directory if a NotFound error 102 | /// occurs. 103 | pub fn atomic_write_file( 104 | sys: &TSys, 105 | file_path: &Path, 106 | data: &[u8], 107 | mode: u32, 108 | ) -> std::io::Result<()> { 109 | fn atomic_write_file_raw( 110 | sys: &TSys, 111 | temp_file_path: &Path, 112 | file_path: &Path, 113 | data: &[u8], 114 | mode: u32, 115 | ) -> std::io::Result<()> { 116 | let mut options = OpenOptions::new_write(); 117 | options.mode = Some(mode); 118 | let mut file = sys.fs_open(temp_file_path, &options)?; 119 | file.write_all(data)?; 120 | sys 121 | .fs_rename(temp_file_path, file_path) 122 | .inspect_err(|_err| { 123 | // clean up the created temp file on error 124 | let _ = sys.fs_remove_file(temp_file_path); 125 | }) 126 | } 127 | 128 | let temp_file_path = get_atomic_path(sys, file_path); 129 | 130 | if let Err(write_err) = 131 | atomic_write_file_raw(sys, &temp_file_path, file_path, data, mode) 132 | { 133 | if write_err.kind() == ErrorKind::NotFound { 134 | let parent_dir_path = file_path.parent().unwrap(); 135 | match sys.fs_create_dir_all(parent_dir_path) { 136 | Ok(()) => { 137 | return atomic_write_file_raw( 138 | sys, 139 | &temp_file_path, 140 | file_path, 141 | data, 142 | mode, 143 | ) 144 | .map_err(|err| add_file_context_to_err(file_path, err)); 145 | } 146 | Err(create_err) => { 147 | if !sys.fs_exists(parent_dir_path).unwrap_or(false) { 148 | return Err(Error::new( 149 | create_err.kind(), 150 | format!( 151 | "{:#} (for '{}')\nCheck the permission of the directory.", 152 | create_err, 153 | parent_dir_path.display() 154 | ), 155 | )); 156 | } 157 | } 158 | } 159 | } 160 | return Err(add_file_context_to_err(file_path, write_err)); 161 | } 162 | Ok(()) 163 | } 164 | 165 | fn add_file_context_to_err(file_path: &Path, err: Error) -> Error { 166 | Error::new( 167 | err.kind(), 168 | format!("{:#} (for '{}')", err, file_path.display()), 169 | ) 170 | } 171 | 172 | #[cfg(test)] 173 | mod test { 174 | use std::path::Path; 175 | use std::path::PathBuf; 176 | 177 | use sys_traits::EnvCurrentDir; 178 | use sys_traits::EnvSetCurrentDir; 179 | use sys_traits::FsCanonicalize; 180 | use sys_traits::FsCreateDirAll; 181 | use sys_traits::FsRead; 182 | use sys_traits::FsSymlinkDir; 183 | use sys_traits::impls::InMemorySys; 184 | use sys_traits::impls::RealSys; 185 | 186 | use super::atomic_write_file_with_retries; 187 | use super::canonicalize_path_maybe_not_exists; 188 | 189 | #[test] 190 | fn test_canonicalize_path_maybe_not_exists_in_memory() { 191 | let sys = InMemorySys::default(); 192 | 193 | // . 194 | // └── a 195 | // └── b (cwd) 196 | // └── c 197 | sys.fs_create_dir_all("/a/b/c").unwrap(); 198 | sys.env_set_current_dir("/a/b").unwrap(); 199 | 200 | let path = canonicalize_path_maybe_not_exists(&sys, Path::new("")).unwrap(); 201 | assert_eq!(path, PathBuf::from("/a/b")); 202 | let path = 203 | canonicalize_path_maybe_not_exists(&sys, Path::new(".")).unwrap(); 204 | assert_eq!(path, PathBuf::from("/a/b")); 205 | let path = 206 | canonicalize_path_maybe_not_exists(&sys, Path::new("d")).unwrap(); 207 | assert_eq!(path, PathBuf::from("/a/b/d")); 208 | let path = 209 | canonicalize_path_maybe_not_exists(&sys, Path::new("./d")).unwrap(); 210 | assert_eq!(path, PathBuf::from("/a/b/d")); 211 | let path = 212 | canonicalize_path_maybe_not_exists(&sys, Path::new("c")).unwrap(); 213 | assert_eq!(path, PathBuf::from("/a/b/c")); 214 | let path = 215 | canonicalize_path_maybe_not_exists(&sys, Path::new("./c")).unwrap(); 216 | assert_eq!(path, PathBuf::from("/a/b/c")); 217 | let path = 218 | canonicalize_path_maybe_not_exists(&sys, Path::new("c/d/e")).unwrap(); 219 | assert_eq!(path, PathBuf::from("/a/b/c/d/e")); 220 | let path = 221 | canonicalize_path_maybe_not_exists(&sys, Path::new("./c/d/e")).unwrap(); 222 | assert_eq!(path, PathBuf::from("/a/b/c/d/e")); 223 | } 224 | 225 | // Note: this test mutates the current working directory. Although it will be 226 | // restored at the end, if other tests relying on the cwd are run in parallel 227 | // to this test, they may fail. 228 | #[test] 229 | fn test_canonicalize_path_maybe_not_exists_real() { 230 | let sys = RealSys; 231 | let temp_dir = tempfile::tempdir().unwrap(); 232 | 233 | // Save the original working directory to restore it later 234 | let original_cwd = sys.env_current_dir().unwrap(); 235 | 236 | // . 237 | // ├── a 238 | // │ └── b 239 | // │ └── c 240 | // └── link -> a/b/c (cwd) 241 | sys 242 | .fs_create_dir_all(temp_dir.path().join("a/b/c")) 243 | .unwrap(); 244 | sys 245 | .fs_symlink_dir( 246 | temp_dir.path().join("a/b/c"), 247 | temp_dir.path().join("link"), 248 | ) 249 | .unwrap(); 250 | let cwd = temp_dir.path().join("link"); 251 | sys.env_set_current_dir(&cwd).unwrap(); 252 | 253 | let canonicalized_temp_dir_path = 254 | sys.fs_canonicalize(temp_dir.path()).unwrap(); 255 | 256 | let path = 257 | canonicalize_path_maybe_not_exists(&sys, Path::new(".")).unwrap(); 258 | assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c")); 259 | 260 | let path = 261 | canonicalize_path_maybe_not_exists(&sys, &PathBuf::from("d")).unwrap(); 262 | assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d")); 263 | 264 | let path = 265 | canonicalize_path_maybe_not_exists(&sys, Path::new("./d")).unwrap(); 266 | assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d")); 267 | 268 | let path = 269 | canonicalize_path_maybe_not_exists(&sys, Path::new("d/e")).unwrap(); 270 | assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d/e")); 271 | 272 | let path = 273 | canonicalize_path_maybe_not_exists(&sys, Path::new("./d/e")).unwrap(); 274 | assert_eq!(path, canonicalized_temp_dir_path.join("a/b/c/d/e")); 275 | 276 | // Restore the original working directory 277 | sys.env_set_current_dir(&original_cwd).unwrap(); 278 | } 279 | 280 | #[test] 281 | fn test_atomic_write_file() { 282 | let sys = RealSys; 283 | let temp_dir = tempfile::tempdir().unwrap(); 284 | let path = temp_dir.path().join("a/b/c"); 285 | atomic_write_file_with_retries(&sys, &path, b"data", 0o644).unwrap(); 286 | assert_eq!(sys.fs_read_to_string(&path).unwrap(), "data"); 287 | #[cfg(unix)] 288 | { 289 | use std::os::unix::fs::PermissionsExt; 290 | let file = std::fs::metadata(path).unwrap(); 291 | assert_eq!(file.permissions().mode(), 0o100644); 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.4.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "2.6.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "1.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 28 | 29 | [[package]] 30 | name = "clap" 31 | version = "4.5.41" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" 34 | dependencies = [ 35 | "clap_builder", 36 | ] 37 | 38 | [[package]] 39 | name = "clap_builder" 40 | version = "4.5.41" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" 43 | dependencies = [ 44 | "anstyle", 45 | "clap_lex", 46 | "terminal_size", 47 | ] 48 | 49 | [[package]] 50 | name = "clap_lex" 51 | version = "0.7.5" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 54 | 55 | [[package]] 56 | name = "condtype" 57 | version = "1.3.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" 60 | 61 | [[package]] 62 | name = "deno_error" 63 | version = "0.7.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "dde60bd153886964234c5012d3d9caf788287f28d81fb24a884436904101ef10" 66 | dependencies = [ 67 | "deno_error_macro", 68 | "libc", 69 | "url", 70 | ] 71 | 72 | [[package]] 73 | name = "deno_error_macro" 74 | version = "0.7.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "409f265785bd946d3006756955aaf40b0e4deb25752eae6a990afe54a31cfd83" 77 | dependencies = [ 78 | "proc-macro2", 79 | "quote", 80 | "syn", 81 | ] 82 | 83 | [[package]] 84 | name = "deno_path_util" 85 | version = "0.6.4" 86 | dependencies = [ 87 | "deno_error", 88 | "divan", 89 | "percent-encoding", 90 | "pretty_assertions", 91 | "sys_traits", 92 | "tempfile", 93 | "thiserror", 94 | "url", 95 | ] 96 | 97 | [[package]] 98 | name = "diff" 99 | version = "0.1.13" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 102 | 103 | [[package]] 104 | name = "displaydoc" 105 | version = "0.2.5" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 108 | dependencies = [ 109 | "proc-macro2", 110 | "quote", 111 | "syn", 112 | ] 113 | 114 | [[package]] 115 | name = "divan" 116 | version = "0.1.21" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" 119 | dependencies = [ 120 | "cfg-if", 121 | "clap", 122 | "condtype", 123 | "divan-macros", 124 | "libc", 125 | "regex-lite", 126 | ] 127 | 128 | [[package]] 129 | name = "divan-macros" 130 | version = "0.1.21" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" 133 | dependencies = [ 134 | "proc-macro2", 135 | "quote", 136 | "syn", 137 | ] 138 | 139 | [[package]] 140 | name = "errno" 141 | version = "0.3.10" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 144 | dependencies = [ 145 | "libc", 146 | "windows-sys", 147 | ] 148 | 149 | [[package]] 150 | name = "fastrand" 151 | version = "2.3.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 154 | 155 | [[package]] 156 | name = "form_urlencoded" 157 | version = "1.2.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 160 | dependencies = [ 161 | "percent-encoding", 162 | ] 163 | 164 | [[package]] 165 | name = "getrandom" 166 | version = "0.2.15" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 169 | dependencies = [ 170 | "cfg-if", 171 | "libc", 172 | "wasi", 173 | ] 174 | 175 | [[package]] 176 | name = "icu_collections" 177 | version = "1.5.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 180 | dependencies = [ 181 | "displaydoc", 182 | "yoke", 183 | "zerofrom", 184 | "zerovec", 185 | ] 186 | 187 | [[package]] 188 | name = "icu_locid" 189 | version = "1.5.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 192 | dependencies = [ 193 | "displaydoc", 194 | "litemap", 195 | "tinystr", 196 | "writeable", 197 | "zerovec", 198 | ] 199 | 200 | [[package]] 201 | name = "icu_locid_transform" 202 | version = "1.5.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 205 | dependencies = [ 206 | "displaydoc", 207 | "icu_locid", 208 | "icu_locid_transform_data", 209 | "icu_provider", 210 | "tinystr", 211 | "zerovec", 212 | ] 213 | 214 | [[package]] 215 | name = "icu_locid_transform_data" 216 | version = "1.5.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 219 | 220 | [[package]] 221 | name = "icu_normalizer" 222 | version = "1.5.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 225 | dependencies = [ 226 | "displaydoc", 227 | "icu_collections", 228 | "icu_normalizer_data", 229 | "icu_properties", 230 | "icu_provider", 231 | "smallvec", 232 | "utf16_iter", 233 | "utf8_iter", 234 | "write16", 235 | "zerovec", 236 | ] 237 | 238 | [[package]] 239 | name = "icu_normalizer_data" 240 | version = "1.5.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 243 | 244 | [[package]] 245 | name = "icu_properties" 246 | version = "1.5.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 249 | dependencies = [ 250 | "displaydoc", 251 | "icu_collections", 252 | "icu_locid_transform", 253 | "icu_properties_data", 254 | "icu_provider", 255 | "tinystr", 256 | "zerovec", 257 | ] 258 | 259 | [[package]] 260 | name = "icu_properties_data" 261 | version = "1.5.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 264 | 265 | [[package]] 266 | name = "icu_provider" 267 | version = "1.5.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 270 | dependencies = [ 271 | "displaydoc", 272 | "icu_locid", 273 | "icu_provider_macros", 274 | "stable_deref_trait", 275 | "tinystr", 276 | "writeable", 277 | "yoke", 278 | "zerofrom", 279 | "zerovec", 280 | ] 281 | 282 | [[package]] 283 | name = "icu_provider_macros" 284 | version = "1.5.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 287 | dependencies = [ 288 | "proc-macro2", 289 | "quote", 290 | "syn", 291 | ] 292 | 293 | [[package]] 294 | name = "idna" 295 | version = "1.0.3" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 298 | dependencies = [ 299 | "idna_adapter", 300 | "smallvec", 301 | "utf8_iter", 302 | ] 303 | 304 | [[package]] 305 | name = "idna_adapter" 306 | version = "1.2.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 309 | dependencies = [ 310 | "icu_normalizer", 311 | "icu_properties", 312 | ] 313 | 314 | [[package]] 315 | name = "libc" 316 | version = "0.2.169" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 319 | 320 | [[package]] 321 | name = "linux-raw-sys" 322 | version = "0.4.14" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 325 | 326 | [[package]] 327 | name = "linux-raw-sys" 328 | version = "0.9.4" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 331 | 332 | [[package]] 333 | name = "litemap" 334 | version = "0.7.4" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 337 | 338 | [[package]] 339 | name = "lock_api" 340 | version = "0.4.12" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 343 | dependencies = [ 344 | "autocfg", 345 | "scopeguard", 346 | ] 347 | 348 | [[package]] 349 | name = "once_cell" 350 | version = "1.20.2" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 353 | 354 | [[package]] 355 | name = "parking_lot" 356 | version = "0.12.3" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 359 | dependencies = [ 360 | "lock_api", 361 | "parking_lot_core", 362 | ] 363 | 364 | [[package]] 365 | name = "parking_lot_core" 366 | version = "0.9.10" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 369 | dependencies = [ 370 | "cfg-if", 371 | "libc", 372 | "redox_syscall", 373 | "smallvec", 374 | "windows-targets", 375 | ] 376 | 377 | [[package]] 378 | name = "percent-encoding" 379 | version = "2.3.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 382 | 383 | [[package]] 384 | name = "pretty_assertions" 385 | version = "1.4.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 388 | dependencies = [ 389 | "diff", 390 | "yansi", 391 | ] 392 | 393 | [[package]] 394 | name = "proc-macro2" 395 | version = "1.0.92" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 398 | dependencies = [ 399 | "unicode-ident", 400 | ] 401 | 402 | [[package]] 403 | name = "quote" 404 | version = "1.0.35" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 407 | dependencies = [ 408 | "proc-macro2", 409 | ] 410 | 411 | [[package]] 412 | name = "redox_syscall" 413 | version = "0.5.8" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 416 | dependencies = [ 417 | "bitflags", 418 | ] 419 | 420 | [[package]] 421 | name = "regex-lite" 422 | version = "0.1.6" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" 425 | 426 | [[package]] 427 | name = "rustix" 428 | version = "0.38.42" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 431 | dependencies = [ 432 | "bitflags", 433 | "errno", 434 | "libc", 435 | "linux-raw-sys 0.4.14", 436 | "windows-sys", 437 | ] 438 | 439 | [[package]] 440 | name = "rustix" 441 | version = "1.0.8" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 444 | dependencies = [ 445 | "bitflags", 446 | "errno", 447 | "libc", 448 | "linux-raw-sys 0.9.4", 449 | "windows-sys", 450 | ] 451 | 452 | [[package]] 453 | name = "scopeguard" 454 | version = "1.2.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 457 | 458 | [[package]] 459 | name = "serde" 460 | version = "1.0.216" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 463 | dependencies = [ 464 | "serde_derive", 465 | ] 466 | 467 | [[package]] 468 | name = "serde_derive" 469 | version = "1.0.216" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 472 | dependencies = [ 473 | "proc-macro2", 474 | "quote", 475 | "syn", 476 | ] 477 | 478 | [[package]] 479 | name = "smallvec" 480 | version = "1.13.2" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 483 | 484 | [[package]] 485 | name = "stable_deref_trait" 486 | version = "1.2.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 489 | 490 | [[package]] 491 | name = "syn" 492 | version = "2.0.91" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 495 | dependencies = [ 496 | "proc-macro2", 497 | "quote", 498 | "unicode-ident", 499 | ] 500 | 501 | [[package]] 502 | name = "synstructure" 503 | version = "0.13.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 506 | dependencies = [ 507 | "proc-macro2", 508 | "quote", 509 | "syn", 510 | ] 511 | 512 | [[package]] 513 | name = "sys_traits" 514 | version = "0.1.17" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "4f74a2c95f72e36fa6bd04a40d15623a9904bab1cc2fa6c6135b09d774a65088" 517 | dependencies = [ 518 | "getrandom", 519 | "parking_lot", 520 | "sys_traits_macros", 521 | ] 522 | 523 | [[package]] 524 | name = "sys_traits_macros" 525 | version = "0.1.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" 528 | dependencies = [ 529 | "proc-macro2", 530 | "quote", 531 | "syn", 532 | ] 533 | 534 | [[package]] 535 | name = "tempfile" 536 | version = "3.14.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 539 | dependencies = [ 540 | "cfg-if", 541 | "fastrand", 542 | "once_cell", 543 | "rustix 0.38.42", 544 | "windows-sys", 545 | ] 546 | 547 | [[package]] 548 | name = "terminal_size" 549 | version = "0.4.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 552 | dependencies = [ 553 | "rustix 1.0.8", 554 | "windows-sys", 555 | ] 556 | 557 | [[package]] 558 | name = "thiserror" 559 | version = "2.0.9" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" 562 | dependencies = [ 563 | "thiserror-impl", 564 | ] 565 | 566 | [[package]] 567 | name = "thiserror-impl" 568 | version = "2.0.9" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" 571 | dependencies = [ 572 | "proc-macro2", 573 | "quote", 574 | "syn", 575 | ] 576 | 577 | [[package]] 578 | name = "tinystr" 579 | version = "0.7.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 582 | dependencies = [ 583 | "displaydoc", 584 | "zerovec", 585 | ] 586 | 587 | [[package]] 588 | name = "unicode-ident" 589 | version = "1.0.11" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 592 | 593 | [[package]] 594 | name = "url" 595 | version = "2.5.4" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 598 | dependencies = [ 599 | "form_urlencoded", 600 | "idna", 601 | "percent-encoding", 602 | ] 603 | 604 | [[package]] 605 | name = "utf16_iter" 606 | version = "1.0.5" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 609 | 610 | [[package]] 611 | name = "utf8_iter" 612 | version = "1.0.4" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 615 | 616 | [[package]] 617 | name = "wasi" 618 | version = "0.11.0+wasi-snapshot-preview1" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 621 | 622 | [[package]] 623 | name = "windows-sys" 624 | version = "0.59.0" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 627 | dependencies = [ 628 | "windows-targets", 629 | ] 630 | 631 | [[package]] 632 | name = "windows-targets" 633 | version = "0.52.6" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 636 | dependencies = [ 637 | "windows_aarch64_gnullvm", 638 | "windows_aarch64_msvc", 639 | "windows_i686_gnu", 640 | "windows_i686_gnullvm", 641 | "windows_i686_msvc", 642 | "windows_x86_64_gnu", 643 | "windows_x86_64_gnullvm", 644 | "windows_x86_64_msvc", 645 | ] 646 | 647 | [[package]] 648 | name = "windows_aarch64_gnullvm" 649 | version = "0.52.6" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 652 | 653 | [[package]] 654 | name = "windows_aarch64_msvc" 655 | version = "0.52.6" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 658 | 659 | [[package]] 660 | name = "windows_i686_gnu" 661 | version = "0.52.6" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 664 | 665 | [[package]] 666 | name = "windows_i686_gnullvm" 667 | version = "0.52.6" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 670 | 671 | [[package]] 672 | name = "windows_i686_msvc" 673 | version = "0.52.6" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 676 | 677 | [[package]] 678 | name = "windows_x86_64_gnu" 679 | version = "0.52.6" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 682 | 683 | [[package]] 684 | name = "windows_x86_64_gnullvm" 685 | version = "0.52.6" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 688 | 689 | [[package]] 690 | name = "windows_x86_64_msvc" 691 | version = "0.52.6" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 694 | 695 | [[package]] 696 | name = "write16" 697 | version = "1.0.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 700 | 701 | [[package]] 702 | name = "writeable" 703 | version = "0.5.5" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 706 | 707 | [[package]] 708 | name = "yansi" 709 | version = "0.5.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 712 | 713 | [[package]] 714 | name = "yoke" 715 | version = "0.7.5" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 718 | dependencies = [ 719 | "serde", 720 | "stable_deref_trait", 721 | "yoke-derive", 722 | "zerofrom", 723 | ] 724 | 725 | [[package]] 726 | name = "yoke-derive" 727 | version = "0.7.5" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 730 | dependencies = [ 731 | "proc-macro2", 732 | "quote", 733 | "syn", 734 | "synstructure", 735 | ] 736 | 737 | [[package]] 738 | name = "zerofrom" 739 | version = "0.1.5" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 742 | dependencies = [ 743 | "zerofrom-derive", 744 | ] 745 | 746 | [[package]] 747 | name = "zerofrom-derive" 748 | version = "0.1.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "syn", 755 | "synstructure", 756 | ] 757 | 758 | [[package]] 759 | name = "zerovec" 760 | version = "0.10.4" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 763 | dependencies = [ 764 | "yoke", 765 | "zerofrom", 766 | "zerovec-derive", 767 | ] 768 | 769 | [[package]] 770 | name = "zerovec-derive" 771 | version = "0.10.3" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 774 | dependencies = [ 775 | "proc-macro2", 776 | "quote", 777 | "syn", 778 | ] 779 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. MIT license. 2 | 3 | #![deny(clippy::print_stderr)] 4 | #![deny(clippy::print_stdout)] 5 | #![deny(clippy::unused_async)] 6 | #![deny(clippy::unnecessary_wraps)] 7 | 8 | use deno_error::JsError; 9 | use std::borrow::Cow; 10 | use std::path::Component; 11 | use std::path::Path; 12 | use std::path::PathBuf; 13 | use sys_traits::SystemRandom; 14 | use sys_traits::impls::is_windows; 15 | use thiserror::Error; 16 | use url::Url; 17 | 18 | pub mod fs; 19 | 20 | /// Gets the parent of this url. 21 | pub fn url_parent(url: &Url) -> Url { 22 | let mut url = url.clone(); 23 | // don't use url.segments() because it will strip the leading slash 24 | let mut segments = url.path().split('/').collect::>(); 25 | if segments.iter().all(|s| s.is_empty()) { 26 | return url; 27 | } 28 | if let Some(last) = segments.last() { 29 | if last.is_empty() { 30 | segments.pop(); 31 | } 32 | segments.pop(); 33 | let new_path = format!("{}/", segments.join("/")); 34 | url.set_path(&new_path); 35 | } 36 | url 37 | } 38 | 39 | #[derive(Debug, Error, deno_error::JsError)] 40 | #[class(uri)] 41 | #[error("Could not convert URL to file path.\n URL: {0}")] 42 | pub struct UrlToFilePathError(pub Url); 43 | 44 | /// Attempts to convert a url to a file path. By default, uses the Url 45 | /// crate's `to_file_path()` method, but falls back to try and resolve unix-style 46 | /// paths on Windows. 47 | pub fn url_to_file_path(url: &Url) -> Result { 48 | let result = if url.scheme() != "file" { 49 | Err(()) 50 | } else { 51 | url_to_file_path_inner(url) 52 | }; 53 | match result { 54 | Ok(path) => Ok(path), 55 | Err(()) => Err(UrlToFilePathError(url.clone())), 56 | } 57 | } 58 | 59 | fn url_to_file_path_inner(url: &Url) -> Result { 60 | #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] 61 | return url_to_file_path_real(url); 62 | #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] 63 | url_to_file_path_wasm(url) 64 | } 65 | 66 | #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] 67 | fn url_to_file_path_real(url: &Url) -> Result { 68 | if cfg!(windows) { 69 | match url.to_file_path() { 70 | Ok(path) => Ok(path), 71 | Err(()) => { 72 | // This might be a unix-style path which is used in the tests even on Windows. 73 | // Attempt to see if we can convert it to a `PathBuf`. This code should be removed 74 | // once/if https://github.com/servo/rust-url/issues/730 is implemented. 75 | if url.scheme() == "file" 76 | && url.host().is_none() 77 | && url.port().is_none() 78 | && url.path_segments().is_some() 79 | { 80 | let path_str = url.path(); 81 | match String::from_utf8( 82 | percent_encoding::percent_decode(path_str.as_bytes()).collect(), 83 | ) { 84 | Ok(path_str) => Ok(PathBuf::from(path_str)), 85 | Err(_) => Err(()), 86 | } 87 | } else { 88 | Err(()) 89 | } 90 | } 91 | } 92 | } else { 93 | url.to_file_path() 94 | } 95 | } 96 | 97 | #[cfg(any( 98 | test, 99 | not(any(unix, windows, target_os = "redox", target_os = "wasi")) 100 | ))] 101 | #[allow(clippy::unnecessary_wraps)] 102 | fn url_to_file_path_wasm(url: &Url) -> Result { 103 | fn is_windows_path_segment(url: &str) -> bool { 104 | let mut chars = url.chars(); 105 | 106 | let first_char = chars.next(); 107 | if first_char.is_none() || !first_char.unwrap().is_ascii_alphabetic() { 108 | return false; 109 | } 110 | 111 | if chars.next() != Some(':') { 112 | return false; 113 | } 114 | 115 | chars.next().is_none() 116 | } 117 | 118 | let path_segments = url.path_segments().unwrap().collect::>(); 119 | let mut final_text = String::new(); 120 | let mut is_windows_share = false; 121 | if let Some(host) = url.host_str() { 122 | final_text.push_str("\\\\"); 123 | final_text.push_str(host); 124 | is_windows_share = true; 125 | } 126 | for segment in path_segments.iter() { 127 | if is_windows_share { 128 | final_text.push('\\'); 129 | } else if !final_text.is_empty() { 130 | final_text.push('/'); 131 | } 132 | final_text.push_str( 133 | &percent_encoding::percent_decode_str(segment).decode_utf8_lossy(), 134 | ); 135 | } 136 | if !is_windows_share && !is_windows_path_segment(path_segments[0]) { 137 | final_text = format!("/{}", final_text); 138 | } 139 | Ok(PathBuf::from(final_text)) 140 | } 141 | 142 | /// Normalize all intermediate components of the path (ie. remove "./" and "../" components). 143 | /// Similar to `fs::canonicalize()` but doesn't resolve symlinks. 144 | /// 145 | /// Adapted from Cargo 146 | /// 147 | #[inline] 148 | pub fn normalize_path(path: Cow) -> Cow { 149 | fn should_normalize(path: &Path) -> bool { 150 | if path_has_trailing_separator(path) { 151 | return true; 152 | } 153 | 154 | let mut last_part = None; 155 | for component in path.components() { 156 | match component { 157 | Component::CurDir | Component::ParentDir => { 158 | return true; 159 | } 160 | Component::Prefix(..) | Component::RootDir => { 161 | // ok 162 | } 163 | Component::Normal(component) => { 164 | last_part = Some(component); 165 | } 166 | } 167 | } 168 | 169 | if is_windows() 170 | && let Some(last_part) = last_part 171 | { 172 | let bytes = last_part.as_encoded_bytes(); 173 | if bytes.ends_with(b".") || bytes.ends_with(b" ") { 174 | return true; 175 | } 176 | } 177 | 178 | path_has_cur_dir_separator(path) 179 | } 180 | 181 | fn path_has_trailing_separator(path: &Path) -> bool { 182 | #[cfg(unix)] 183 | let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str()); 184 | #[cfg(windows)] 185 | let raw = path.as_os_str().as_encoded_bytes(); 186 | #[cfg(target_arch = "wasm32")] 187 | let raw = path.to_string_lossy(); 188 | #[cfg(target_arch = "wasm32")] 189 | let raw = raw.as_bytes(); 190 | 191 | if sys_traits::impls::is_windows() { 192 | raw.contains(&b'/') || raw.ends_with(b"\\") 193 | } else { 194 | raw.ends_with(b"/") 195 | } 196 | } 197 | 198 | // Rust normalizes away `Component::CurDir` most of the time 199 | // so we need to explicitly check for it in the bytes 200 | fn path_has_cur_dir_separator(path: &Path) -> bool { 201 | #[cfg(unix)] 202 | let raw = std::os::unix::ffi::OsStrExt::as_bytes(path.as_os_str()); 203 | #[cfg(windows)] 204 | let raw = path.as_os_str().as_encoded_bytes(); 205 | #[cfg(target_arch = "wasm32")] 206 | let raw = path.to_string_lossy(); 207 | #[cfg(target_arch = "wasm32")] 208 | let raw = raw.as_bytes(); 209 | 210 | if raw.ends_with(b"\\.") || raw.ends_with(b"/.") { 211 | return true; 212 | } 213 | 214 | if sys_traits::impls::is_windows() { 215 | for window in raw.windows(3) { 216 | if matches!(window, [b'\\', b'.', b'\\']) { 217 | return true; 218 | } 219 | } 220 | } else { 221 | for window in raw.windows(3) { 222 | if matches!(window, [b'/', b'.', b'/']) { 223 | return true; 224 | } 225 | } 226 | } 227 | 228 | false 229 | } 230 | 231 | fn inner(path: &Path) -> PathBuf { 232 | let mut components = path.components().peekable(); 233 | let mut ret = 234 | if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { 235 | components.next(); 236 | PathBuf::from(c.as_os_str()) 237 | } else { 238 | PathBuf::new() 239 | }; 240 | 241 | for component in components { 242 | match component { 243 | Component::Prefix(..) => unreachable!(), 244 | Component::RootDir => { 245 | ret.push(component.as_os_str()); 246 | } 247 | Component::CurDir => {} 248 | Component::ParentDir => { 249 | ret.pop(); 250 | } 251 | Component::Normal(c) => { 252 | if is_windows() { 253 | let bytes = c.as_encoded_bytes(); 254 | // Strip trailing dots and spaces on Windows 255 | let mut end = bytes.len(); 256 | while end > 0 && (bytes[end - 1] == b'.' || bytes[end - 1] == b' ') 257 | { 258 | end -= 1; 259 | } 260 | if end == bytes.len() { 261 | ret.push(c); 262 | } else if end > 0 { 263 | #[cfg(windows)] 264 | { 265 | use std::os::windows::ffi::{OsStrExt, OsStringExt}; 266 | let wide: Vec = c.encode_wide().collect(); 267 | let trimmed = std::ffi::OsString::from_wide(&wide[..end]); 268 | ret.push(trimmed); 269 | } 270 | // SAFETY: trimmed spaces and dots only 271 | #[cfg(not(windows))] 272 | unsafe { 273 | let trimmed = 274 | std::ffi::OsStr::from_encoded_bytes_unchecked(&bytes[..end]); 275 | ret.push(trimmed); 276 | } 277 | } 278 | } else { 279 | ret.push(c); 280 | } 281 | } 282 | } 283 | } 284 | ret 285 | } 286 | 287 | if should_normalize(&path) { 288 | Cow::Owned(inner(&path)) 289 | } else { 290 | path 291 | } 292 | } 293 | 294 | #[derive(Debug, Clone, Error, deno_error::JsError, PartialEq, Eq)] 295 | #[class(uri)] 296 | #[error("Could not convert path to URL.\n Path: {0}")] 297 | pub struct PathToUrlError(pub PathBuf); 298 | 299 | #[allow(clippy::result_unit_err)] 300 | pub fn url_from_file_path(path: &Path) -> Result { 301 | let path = normalize_path(Cow::Borrowed(path)); 302 | #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] 303 | return Url::from_file_path(&path) 304 | .map_err(|()| PathToUrlError(path.to_path_buf())); 305 | #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] 306 | url_from_file_path_wasm(&path) 307 | .map_err(|()| PathToUrlError(path.to_path_buf())) 308 | } 309 | 310 | #[allow(clippy::result_unit_err)] 311 | pub fn url_from_directory_path(path: &Path) -> Result { 312 | let path = normalize_path(Cow::Borrowed(path)); 313 | #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] 314 | return Url::from_directory_path(&path) 315 | .map_err(|()| PathToUrlError(path.to_path_buf())); 316 | #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] 317 | url_from_directory_path_wasm(&path) 318 | .map_err(|()| PathToUrlError(path.to_path_buf())) 319 | } 320 | 321 | #[cfg(any( 322 | test, 323 | not(any(unix, windows, target_os = "redox", target_os = "wasi")) 324 | ))] 325 | fn url_from_directory_path_wasm(path: &Path) -> Result { 326 | let mut url = url_from_file_path_wasm(path)?; 327 | url.path_segments_mut().unwrap().push(""); 328 | Ok(url) 329 | } 330 | 331 | #[cfg(any( 332 | test, 333 | not(any(unix, windows, target_os = "redox", target_os = "wasi")) 334 | ))] 335 | fn url_from_file_path_wasm(path: &Path) -> Result { 336 | use std::path::Component; 337 | 338 | let original_path = path.to_string_lossy(); 339 | let mut path_str = original_path; 340 | // assume paths containing backslashes are windows paths 341 | if path_str.contains('\\') { 342 | let mut url = Url::parse("file://").unwrap(); 343 | if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) { 344 | if let Some((host, rest)) = next.split_once('\\') 345 | && url.set_host(Some(host)).is_ok() 346 | { 347 | path_str = rest.to_string().into(); 348 | } 349 | } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) { 350 | path_str = next.to_string().into(); 351 | } else if let Some(next) = path_str.strip_prefix(r#"\\"#) 352 | && let Some((host, rest)) = next.split_once('\\') 353 | && url.set_host(Some(host)).is_ok() 354 | { 355 | path_str = rest.to_string().into(); 356 | } 357 | 358 | for component in path_str.split('\\') { 359 | url.path_segments_mut().unwrap().push(component); 360 | } 361 | 362 | Ok(url) 363 | } else { 364 | let mut url = Url::parse("file://").unwrap(); 365 | for component in path.components() { 366 | match component { 367 | Component::RootDir => { 368 | url.path_segments_mut().unwrap().push(""); 369 | } 370 | Component::Normal(segment) => { 371 | url 372 | .path_segments_mut() 373 | .unwrap() 374 | .push(&segment.to_string_lossy()); 375 | } 376 | Component::Prefix(_) | Component::CurDir | Component::ParentDir => { 377 | return Err(()); 378 | } 379 | } 380 | } 381 | 382 | Ok(url) 383 | } 384 | } 385 | 386 | #[cfg(not(windows))] 387 | #[inline] 388 | pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { 389 | path 390 | } 391 | 392 | /// Strips the unc prefix (ex. \\?\) from Windows paths. 393 | #[cfg(windows)] 394 | pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { 395 | use std::path::Component; 396 | use std::path::Prefix; 397 | 398 | let mut components = path.components(); 399 | match components.next() { 400 | Some(Component::Prefix(prefix)) => { 401 | match prefix.kind() { 402 | // \\?\device 403 | Prefix::Verbatim(device) => { 404 | let mut path = PathBuf::new(); 405 | path.push(format!(r"\\{}\", device.to_string_lossy())); 406 | path.extend(components.filter(|c| !matches!(c, Component::RootDir))); 407 | path 408 | } 409 | // \\?\c:\path 410 | Prefix::VerbatimDisk(_) => { 411 | let mut path = PathBuf::new(); 412 | path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); 413 | path.extend(components); 414 | path 415 | } 416 | // \\?\UNC\hostname\share_name\path 417 | Prefix::VerbatimUNC(hostname, share_name) => { 418 | let mut path = PathBuf::new(); 419 | path.push(format!( 420 | r"\\{}\{}\", 421 | hostname.to_string_lossy(), 422 | share_name.to_string_lossy() 423 | )); 424 | path.extend(components.filter(|c| !matches!(c, Component::RootDir))); 425 | path 426 | } 427 | _ => path, 428 | } 429 | } 430 | _ => path, 431 | } 432 | } 433 | 434 | /// Returns true if the input string starts with a sequence of characters 435 | /// that could be a valid URI scheme, like 'https:', 'git+ssh:' or 'data:'. 436 | /// 437 | /// According to RFC 3986 (https://tools.ietf.org/html/rfc3986#section-3.1), 438 | /// a valid scheme has the following format: 439 | /// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 440 | /// 441 | /// We additionally require the scheme to be at least 2 characters long, 442 | /// because otherwise a windows path like c:/foo would be treated as a URL, 443 | /// while no schemes with a one-letter name actually exist. 444 | pub fn specifier_has_uri_scheme(specifier: &str) -> bool { 445 | let mut chars = specifier.chars(); 446 | let mut len = 0usize; 447 | // The first character must be a letter. 448 | match chars.next() { 449 | Some(c) if c.is_ascii_alphabetic() => len += 1, 450 | _ => return false, 451 | } 452 | // Second and following characters must be either a letter, number, 453 | // plus sign, minus sign, or dot. 454 | loop { 455 | match chars.next() { 456 | Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1, 457 | Some(':') if len >= 2 => return true, 458 | _ => return false, 459 | } 460 | } 461 | } 462 | 463 | #[derive(Debug, Clone, Error, JsError, PartialEq, Eq)] 464 | pub enum ResolveUrlOrPathError { 465 | #[error(transparent)] 466 | #[class(inherit)] 467 | UrlParse(url::ParseError), 468 | #[error(transparent)] 469 | #[class(inherit)] 470 | PathToUrl(PathToUrlError), 471 | } 472 | 473 | /// Takes a string representing either an absolute URL or a file path, 474 | /// as it may be passed to deno as a command line argument. 475 | /// The string is interpreted as a URL if it starts with a valid URI scheme, 476 | /// e.g. 'http:' or 'file:' or 'git+ssh:'. If not, it's interpreted as a 477 | /// file path; if it is a relative path it's resolved relative to passed 478 | /// `current_dir`. 479 | pub fn resolve_url_or_path( 480 | specifier: &str, 481 | current_dir: &Path, 482 | ) -> Result { 483 | if specifier_has_uri_scheme(specifier) { 484 | Url::parse(specifier).map_err(ResolveUrlOrPathError::UrlParse) 485 | } else { 486 | resolve_path(specifier, current_dir) 487 | .map_err(ResolveUrlOrPathError::PathToUrl) 488 | } 489 | } 490 | 491 | /// Converts a string representing a relative or absolute path into a 492 | /// ModuleSpecifier. A relative path is considered relative to the passed 493 | /// `current_dir`. 494 | pub fn resolve_path( 495 | path_str: &str, 496 | current_dir: &Path, 497 | ) -> Result { 498 | let path = current_dir.join(path_str); 499 | url_from_file_path(&path) 500 | } 501 | 502 | #[derive(Debug, Error, Clone, PartialEq, Eq, deno_error::JsError)] 503 | pub enum SpecifierError { 504 | // don't make this error a source because it's short 505 | // and that causes unnecessary verbosity 506 | #[class(inherit)] 507 | #[error("invalid URL: {0}")] 508 | InvalidUrl(url::ParseError), 509 | #[class(type)] 510 | #[error("Import \"{specifier}\" not a dependency")] 511 | ImportPrefixMissing { specifier: String }, 512 | } 513 | 514 | /// Given a specifier string and a referring module specifier, try to resolve 515 | /// the target module specifier, erroring if it cannot be resolved. 516 | /// 517 | /// This function is useful for resolving specifiers in situations without an 518 | /// import map. 519 | pub fn resolve_import( 520 | specifier: &str, 521 | referrer: &Url, 522 | ) -> Result { 523 | match Url::parse(specifier) { 524 | // 1. Apply the URL parser to specifier. 525 | // If the result is not failure, return the result. 526 | Ok(url) => Ok(url), 527 | 528 | // 2. If specifier does not start with the character U+002F SOLIDUS (/), 529 | // the two-character sequence U+002E FULL STOP, U+002F SOLIDUS (./), 530 | // or the three-character sequence U+002E FULL STOP, U+002E FULL STOP, 531 | // U+002F SOLIDUS (../), return failure. 532 | Err(url::ParseError::RelativeUrlWithoutBase) 533 | if !(specifier.starts_with('/') 534 | || specifier.starts_with("./") 535 | || specifier.starts_with("../")) => 536 | { 537 | Err(SpecifierError::ImportPrefixMissing { 538 | specifier: specifier.to_string(), 539 | }) 540 | } 541 | 542 | // 3. Return the result of applying the URL parser to specifier with base 543 | // URL as the base URL. 544 | Err(url::ParseError::RelativeUrlWithoutBase) => { 545 | referrer.join(specifier).map_err(SpecifierError::InvalidUrl) 546 | } 547 | 548 | // If parsing the specifier as a URL failed for a different reason than 549 | // it being relative, always return the original error. We don't want to 550 | // return `ImportPrefixMissing` or `InvalidBaseUrl` if the real 551 | // problem lies somewhere else. 552 | Err(err) => Err(SpecifierError::InvalidUrl(err)), 553 | } 554 | } 555 | 556 | pub fn get_atomic_path(sys: &impl SystemRandom, path: &Path) -> PathBuf { 557 | let rand = gen_rand_path_component(sys); 558 | let extension = format!("{rand}.tmp"); 559 | path.with_extension(extension) 560 | } 561 | 562 | fn gen_rand_path_component(sys: &impl SystemRandom) -> String { 563 | use std::fmt::Write; 564 | (0..4).fold(String::with_capacity(8), |mut output, _| { 565 | write!(&mut output, "{:02x}", sys.sys_random_u8().unwrap()).unwrap(); 566 | output 567 | }) 568 | } 569 | 570 | pub fn is_relative_specifier(specifier: &str) -> bool { 571 | let mut specifier_chars = specifier.chars(); 572 | let Some(first_char) = specifier_chars.next() else { 573 | return false; 574 | }; 575 | if first_char != '.' { 576 | return false; 577 | } 578 | let Some(second_char) = specifier_chars.next() else { 579 | return true; 580 | }; 581 | if second_char == '/' { 582 | return true; 583 | } 584 | let Some(third_char) = specifier_chars.next() else { 585 | return second_char == '.'; 586 | }; 587 | second_char == '.' && third_char == '/' 588 | } 589 | 590 | #[cfg(test)] 591 | mod tests { 592 | use super::*; 593 | 594 | #[test] 595 | fn test_url_parent() { 596 | run_test("file:///", "file:///"); 597 | run_test("file:///test", "file:///"); 598 | run_test("file:///test/", "file:///"); 599 | run_test("file:///test/other", "file:///test/"); 600 | run_test("file:///test/other.txt", "file:///test/"); 601 | run_test("file:///test/other/", "file:///test/"); 602 | 603 | fn run_test(url: &str, expected: &str) { 604 | let result = url_parent(&Url::parse(url).unwrap()); 605 | assert_eq!(result.to_string(), expected); 606 | } 607 | } 608 | 609 | #[test] 610 | fn test_url_to_file_path() { 611 | run_success_test("file:///", "/"); 612 | run_success_test("file:///test", "/test"); 613 | run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); 614 | #[cfg(not(debug_assertions))] 615 | run_success_test( 616 | "file:///dir/test%20test/test.txt", 617 | "/dir/test test/test.txt", 618 | ); 619 | 620 | assert_no_panic_url_to_file_path("file:/"); 621 | assert_no_panic_url_to_file_path("file://"); 622 | #[cfg(not(debug_assertions))] 623 | assert_no_panic_url_to_file_path("file://asdf/"); 624 | assert_no_panic_url_to_file_path("file://asdf/66666/a.ts"); 625 | 626 | #[track_caller] 627 | fn run_success_test(url: &str, expected_path: &str) { 628 | let result = url_to_file_path(&Url::parse(url).unwrap()).unwrap(); 629 | assert_eq!(result, PathBuf::from(expected_path)); 630 | } 631 | 632 | #[track_caller] 633 | fn assert_no_panic_url_to_file_path(url: &str) { 634 | let _result = url_to_file_path(&Url::parse(url).unwrap()); 635 | } 636 | } 637 | 638 | #[test] 639 | fn test_url_to_file_path_wasm() { 640 | #[track_caller] 641 | fn convert(path: &str) -> String { 642 | url_to_file_path_wasm(&Url::parse(path).unwrap()) 643 | .unwrap() 644 | .to_string_lossy() 645 | .into_owned() 646 | } 647 | 648 | assert_eq!(convert("file:///a/b/c.json"), "/a/b/c.json"); 649 | assert_eq!(convert("file:///D:/test/other.json"), "D:/test/other.json"); 650 | assert_eq!( 651 | convert("file:///path%20with%20spaces/and%23special%25chars!.json"), 652 | "/path with spaces/and#special%chars!.json", 653 | ); 654 | assert_eq!( 655 | convert("file:///C:/My%20Documents/file.txt"), 656 | "C:/My Documents/file.txt" 657 | ); 658 | assert_eq!( 659 | convert("file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"), 660 | "/a/b/пример.txt" 661 | ); 662 | assert_eq!( 663 | convert("file://server/share/folder/file.txt"), 664 | "\\\\server\\share\\folder\\file.txt" 665 | ); 666 | } 667 | 668 | #[test] 669 | fn test_url_from_file_path_wasm() { 670 | #[track_caller] 671 | fn convert(path: &str) -> String { 672 | url_from_file_path_wasm(Path::new(path)) 673 | .unwrap() 674 | .to_string() 675 | } 676 | 677 | assert_eq!(convert("/a/b/c.json"), "file:///a/b/c.json"); 678 | assert_eq!( 679 | convert("D:\\test\\other.json"), 680 | "file:///D:/test/other.json" 681 | ); 682 | assert_eq!( 683 | convert("/path with spaces/and#special%chars!.json"), 684 | "file:///path%20with%20spaces/and%23special%25chars!.json" 685 | ); 686 | assert_eq!( 687 | convert("C:\\My Documents\\file.txt"), 688 | "file:///C:/My%20Documents/file.txt" 689 | ); 690 | assert_eq!( 691 | convert("/a/b/пример.txt"), 692 | "file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt" 693 | ); 694 | assert_eq!( 695 | convert("\\\\server\\share\\folder\\file.txt"), 696 | "file://server/share/folder/file.txt" 697 | ); 698 | assert_eq!(convert(r#"\\?\UNC\server\share"#), "file://server/share"); 699 | assert_eq!( 700 | convert(r"\\?\cat_pics\subfolder\file.jpg"), 701 | "file:///cat_pics/subfolder/file.jpg" 702 | ); 703 | assert_eq!(convert(r"\\?\cat_pics"), "file:///cat_pics"); 704 | } 705 | 706 | #[test] 707 | fn test_url_from_directory_path_wasm() { 708 | #[track_caller] 709 | fn convert(path: &str) -> String { 710 | url_from_directory_path_wasm(Path::new(path)) 711 | .unwrap() 712 | .to_string() 713 | } 714 | 715 | assert_eq!(convert("/a/b/c"), "file:///a/b/c/"); 716 | assert_eq!(convert("D:\\test\\other"), "file:///D:/test/other/"); 717 | } 718 | 719 | #[cfg(windows)] 720 | #[test] 721 | fn test_strip_unc_prefix() { 722 | use std::path::PathBuf; 723 | 724 | run_test(r"C:\", r"C:\"); 725 | run_test(r"C:\test\file.txt", r"C:\test\file.txt"); 726 | 727 | run_test(r"\\?\C:\", r"C:\"); 728 | run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt"); 729 | 730 | run_test(r"\\.\C:\", r"\\.\C:\"); 731 | run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt"); 732 | 733 | run_test(r"\\?\UNC\localhost\", r"\\localhost"); 734 | run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$"); 735 | run_test( 736 | r"\\?\UNC\localhost\c$\Windows\file.txt", 737 | r"\\localhost\c$\Windows\file.txt", 738 | ); 739 | run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json"); 740 | 741 | run_test(r"\\?\server1", r"\\server1"); 742 | run_test(r"\\?\server1\e$\", r"\\server1\e$\"); 743 | run_test( 744 | r"\\?\server1\e$\test\file.txt", 745 | r"\\server1\e$\test\file.txt", 746 | ); 747 | 748 | fn run_test(input: &str, expected: &str) { 749 | assert_eq!( 750 | super::strip_unc_prefix(PathBuf::from(input)), 751 | PathBuf::from(expected) 752 | ); 753 | } 754 | } 755 | 756 | #[test] 757 | fn test_normalize_path_basic() { 758 | let run_test = run_normalize_path_test; 759 | run_test("a/../b", "b"); 760 | run_test("a/./b/", &PathBuf::from("a").join("b").to_string_lossy()); 761 | run_test( 762 | "a/./b/../c", 763 | &PathBuf::from("a").join("c").to_string_lossy(), 764 | ); 765 | } 766 | 767 | #[cfg(windows)] 768 | #[test] 769 | fn test_normalize_path_win() { 770 | let run_test = run_normalize_path_test; 771 | 772 | run_test("C:\\test\\file.txt", "C:\\test\\file.txt"); 773 | run_test("C:\\test\\./file.txt", "C:\\test\\file.txt"); 774 | run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt"); 775 | run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt"); 776 | run_test( 777 | "C:\\test\\removes_trailing_slash\\", 778 | "C:\\test\\removes_trailing_slash", 779 | ); 780 | run_test("C:\\a\\.\\b\\..\\c", "C:\\a\\c"); 781 | run_test("C:\\test\\.", "C:\\test"); 782 | run_test("C:\\test\\test...", "C:\\test\\test"); 783 | run_test("C:\\test\\test ", "C:\\test\\test"); 784 | } 785 | 786 | #[track_caller] 787 | fn run_normalize_path_test(input: &str, expected: &str) { 788 | assert_eq!( 789 | normalize_path(Cow::Owned(PathBuf::from(input))).to_string_lossy(), 790 | expected 791 | ); 792 | } 793 | 794 | #[test] 795 | fn test_atomic_path() { 796 | let sys = sys_traits::impls::InMemorySys::default(); 797 | sys.set_seed(Some(10)); 798 | let path = Path::new("/a/b/c.txt"); 799 | let atomic_path = get_atomic_path(&sys, path); 800 | assert_eq!(atomic_path.parent().unwrap(), path.parent().unwrap()); 801 | assert_eq!(atomic_path.file_name().unwrap(), "c.3d3d3d3d.tmp"); 802 | } 803 | 804 | #[test] 805 | fn test_specifier_has_uri_scheme() { 806 | let tests = vec![ 807 | ("http://foo.bar/etc", true), 808 | ("HTTP://foo.bar/etc", true), 809 | ("http:ftp:", true), 810 | ("http:", true), 811 | ("hTtP:", true), 812 | ("ftp:", true), 813 | ("mailto:spam@please.me", true), 814 | ("git+ssh://git@github.com/denoland/deno", true), 815 | ("blob:https://whatwg.org/mumbojumbo", true), 816 | ("abc.123+DEF-ghi:", true), 817 | ("abc.123+def-ghi:@", true), 818 | ("", false), 819 | (":not", false), 820 | ("http", false), 821 | ("c:dir", false), 822 | ("X:", false), 823 | ("./http://not", false), 824 | ("1abc://kinda/but/no", false), 825 | ("schluẞ://no/more", false), 826 | ]; 827 | 828 | for (specifier, expected) in tests { 829 | let result = specifier_has_uri_scheme(specifier); 830 | assert_eq!(result, expected); 831 | } 832 | } 833 | 834 | #[test] 835 | fn test_resolve_url_or_path() { 836 | // Absolute URL. 837 | let mut tests: Vec<(&str, String)> = vec![ 838 | ( 839 | "http://deno.land/core/tests/006_url_imports.ts", 840 | "http://deno.land/core/tests/006_url_imports.ts".to_string(), 841 | ), 842 | ( 843 | "https://deno.land/core/tests/006_url_imports.ts", 844 | "https://deno.land/core/tests/006_url_imports.ts".to_string(), 845 | ), 846 | ]; 847 | 848 | // The local path tests assume that the cwd is the deno repo root. Note 849 | // that we can't use `cwd` in miri tests, so we just use `/miri` instead. 850 | let cwd = if cfg!(miri) { 851 | PathBuf::from("/miri") 852 | } else { 853 | std::env::current_dir().unwrap() 854 | }; 855 | let cwd_str = cwd.to_str().unwrap(); 856 | 857 | if cfg!(target_os = "windows") { 858 | // Absolute local path. 859 | let expected_url = "file:///C:/deno/tests/006_url_imports.ts"; 860 | tests.extend(vec![ 861 | ( 862 | r"C:/deno/tests/006_url_imports.ts", 863 | expected_url.to_string(), 864 | ), 865 | ( 866 | r"C:\deno\tests\006_url_imports.ts", 867 | expected_url.to_string(), 868 | ), 869 | ( 870 | r"\\?\C:\deno\tests\006_url_imports.ts", 871 | expected_url.to_string(), 872 | ), 873 | // Not supported: `Url::from_file_path()` fails. 874 | // (r"\\.\C:\deno\tests\006_url_imports.ts", expected_url.to_string()), 875 | // Not supported: `Url::from_file_path()` performs the wrong conversion. 876 | // (r"//./C:/deno/tests/006_url_imports.ts", expected_url.to_string()), 877 | ]); 878 | 879 | // Rooted local path without drive letter. 880 | let expected_url = format!( 881 | "file:///{}:/deno/tests/006_url_imports.ts", 882 | cwd_str.get(..1).unwrap(), 883 | ); 884 | tests.extend(vec![ 885 | (r"/deno/tests/006_url_imports.ts", expected_url.to_string()), 886 | (r"\deno\tests\006_url_imports.ts", expected_url.to_string()), 887 | ( 888 | r"\deno\..\deno\tests\006_url_imports.ts", 889 | expected_url.to_string(), 890 | ), 891 | (r"\deno\.\tests\006_url_imports.ts", expected_url), 892 | ]); 893 | 894 | // Relative local path. 895 | let expected_url = format!( 896 | "file:///{}/tests/006_url_imports.ts", 897 | cwd_str.replace('\\', "/") 898 | ); 899 | tests.extend(vec![ 900 | (r"tests/006_url_imports.ts", expected_url.to_string()), 901 | (r"tests\006_url_imports.ts", expected_url.to_string()), 902 | (r"./tests/006_url_imports.ts", (*expected_url).to_string()), 903 | (r".\tests\006_url_imports.ts", (*expected_url).to_string()), 904 | ]); 905 | 906 | // UNC network path. 907 | let expected_url = "file://server/share/deno/cool"; 908 | tests.extend(vec![ 909 | (r"\\server\share\deno\cool", expected_url.to_string()), 910 | (r"\\server/share/deno/cool", expected_url.to_string()), 911 | // Not supported: `Url::from_file_path()` performs the wrong conversion. 912 | // (r"//server/share/deno/cool", expected_url.to_string()), 913 | ]); 914 | } else { 915 | // Absolute local path. 916 | let expected_url = "file:///deno/tests/006_url_imports.ts"; 917 | tests.extend(vec![ 918 | ("/deno/tests/006_url_imports.ts", expected_url.to_string()), 919 | ("//deno/tests/006_url_imports.ts", expected_url.to_string()), 920 | ]); 921 | 922 | // Relative local path. 923 | let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts"); 924 | tests.extend(vec![ 925 | ("tests/006_url_imports.ts", expected_url.to_string()), 926 | ("./tests/006_url_imports.ts", expected_url.to_string()), 927 | ( 928 | "tests/../tests/006_url_imports.ts", 929 | expected_url.to_string(), 930 | ), 931 | ("tests/./006_url_imports.ts", expected_url), 932 | ]); 933 | } 934 | 935 | for (specifier, expected_url) in tests { 936 | let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string(); 937 | assert_eq!(url, expected_url); 938 | } 939 | } 940 | 941 | #[test] 942 | fn test_resolve_url_or_path_deprecated_error() { 943 | use ResolveUrlOrPathError::*; 944 | use url::ParseError::*; 945 | 946 | let mut tests = vec![ 947 | ("https://eggplant:b/c", UrlParse(InvalidPort)), 948 | ("https://:8080/a/b/c", UrlParse(EmptyHost)), 949 | ]; 950 | if cfg!(target_os = "windows") { 951 | let p = r"\\.\c:/stuff/deno/script.ts"; 952 | tests.push((p, PathToUrl(PathToUrlError(PathBuf::from(p))))); 953 | } 954 | 955 | for (specifier, expected_err) in tests { 956 | let err = 957 | resolve_url_or_path(specifier, &PathBuf::from("/")).unwrap_err(); 958 | assert_eq!(err, expected_err); 959 | } 960 | } 961 | 962 | #[test] 963 | fn test_is_relative_specifier() { 964 | assert!(is_relative_specifier(".")); 965 | assert!(is_relative_specifier("..")); 966 | assert!(is_relative_specifier("../")); 967 | assert!(is_relative_specifier("../test")); 968 | assert!(is_relative_specifier("./")); 969 | assert!(is_relative_specifier("./test")); 970 | assert!(!is_relative_specifier("")); 971 | assert!(!is_relative_specifier("a")); 972 | assert!(!is_relative_specifier(".test")); 973 | assert!(!is_relative_specifier("..test")); 974 | assert!(!is_relative_specifier("/test")); 975 | assert!(!is_relative_specifier("test")); 976 | } 977 | 978 | #[test] 979 | fn test_resolve_import() { 980 | fn run_test(specifier: &str, base: &str, expected: &str) { 981 | let actual = 982 | resolve_import(specifier, &Url::parse(base).unwrap()).unwrap(); 983 | assert_eq!(actual.as_str(), expected); 984 | } 985 | 986 | run_test( 987 | "./test.js", 988 | "https://example.com", 989 | "https://example.com/test.js", 990 | ); 991 | run_test( 992 | "https://deno.land/x/mod.ts", 993 | "https://example.com", 994 | "https://deno.land/x/mod.ts", 995 | ); 996 | run_test( 997 | "../test.js", 998 | "https://example.com/sub", 999 | "https://example.com/test.js", 1000 | ); 1001 | run_test( 1002 | "/test.js", 1003 | "https://example.com/sub/dir/test", 1004 | "https://example.com/test.js", 1005 | ); 1006 | 1007 | match resolve_import("test.js", &Url::parse("https://example.com").unwrap()) 1008 | { 1009 | Err(SpecifierError::ImportPrefixMissing { specifier }) => { 1010 | assert_eq!(specifier, "test.js"); 1011 | } 1012 | _ => unreachable!(), 1013 | } 1014 | } 1015 | } 1016 | --------------------------------------------------------------------------------