├── .github └── workflows │ ├── ci.yml │ └── release-plz.yml ├── .gitignore ├── .rustfmt.toml ├── .vscode └── launch.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── data ├── @babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip ├── edge_case_manifest_state.json ├── pnp-yarn-v3.cjs ├── pnp-yarn-v4.cjs └── test-expectations.json ├── rust-toolchain.toml └── src ├── builtins.rs ├── error.rs ├── fs.rs ├── lib.rs ├── lib_tests.rs ├── main.rs ├── manifest.rs ├── util.rs └── zip.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | - macos-latest 16 | - windows-latest 17 | runs-on: ${{matrix.os}} 18 | steps: 19 | - uses: taiki-e/checkout-action@v1 20 | 21 | - uses: oxc-project/setup-rust@v1.0.0 22 | with: 23 | save-cache: ${{ github.ref_name == 'main' }} 24 | 25 | - run: cargo check 26 | 27 | - run: cargo test 28 | 29 | clippy: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: taiki-e/checkout-action@v1 33 | 34 | - uses: oxc-project/setup-rust@v1.0.0 35 | with: 36 | save-cache: ${{ github.ref_name == 'main' }} 37 | cache-key: clippy 38 | components: clippy 39 | 40 | - run: cargo clippy --all-targets --all-features -- -D warnings 41 | 42 | fmt: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: taiki-e/checkout-action@v1 46 | 47 | - uses: oxc-project/setup-rust@v1.0.0 48 | with: 49 | components: rustfmt 50 | 51 | - run: cargo fmt --check 52 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release Plz 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | release-plz: 17 | name: Release-plz 18 | runs-on: ubuntu-latest 19 | permissions: 20 | pull-requests: write 21 | contents: write 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | persist-credentials: true # required by release-plz 27 | 28 | - name: Run release-plz 29 | id: release-plz 30 | uses: MarcoIeni/release-plz-action@8724d33cd97b8295051102e2e19ca592962238f5 # v0.5.108 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "type": "lldb", 5 | "request": "launch", 6 | "name": "Debug executable 'esfuse-pnp'", 7 | "cargo": { 8 | "args": [ 9 | "build", 10 | "--bin=esfuse-pnp", 11 | "--package=esfuse-pnp" 12 | ], 13 | "filter": { 14 | "name": "esfuse-pnp", 15 | "kind": "bin" 16 | } 17 | }, 18 | "args": ["typescript", "/Users/mael.nison/berry/package.json"], 19 | "cwd": "${workspaceFolder}" 20 | }] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.11.0](https://github.com/yarnpkg/pnp-rs/compare/v0.10.0...v0.11.0) - 2025-07-01 11 | 12 | ### Fixed 13 | 14 | - fix windows failure ([#22](https://github.com/yarnpkg/pnp-rs/pull/22)) 15 | 16 | ### Other 17 | 18 | - add release-plz.yml ([#24](https://github.com/yarnpkg/pnp-rs/pull/24)) 19 | - remove indexmap 20 | - remove `serde_with` ([#32](https://github.com/yarnpkg/pnp-rs/pull/32)) 21 | - remove the unused `Serialize` on `PackageLocator` ([#31](https://github.com/yarnpkg/pnp-rs/pull/31)) 22 | - bump deps ([#30](https://github.com/yarnpkg/pnp-rs/pull/30)) 23 | - use fxhash in zip data structures ([#28](https://github.com/yarnpkg/pnp-rs/pull/28)) 24 | - remove the `lazy_static` crate ([#27](https://github.com/yarnpkg/pnp-rs/pull/27)) 25 | - improve `NODEJS_BUILTINS` ([#26](https://github.com/yarnpkg/pnp-rs/pull/26)) 26 | - remove unnecessary derive `Serialize` on `Error` ([#25](https://github.com/yarnpkg/pnp-rs/pull/25)) 27 | - use fxhash ([#23](https://github.com/yarnpkg/pnp-rs/pull/23)) 28 | - `clippy::result_large_err` for the `Error` type ([#21](https://github.com/yarnpkg/pnp-rs/pull/21)) 29 | - run `cargo clippy --fix` + manual fixes ([#20](https://github.com/yarnpkg/pnp-rs/pull/20)) 30 | - run `cargo fmt` ([#19](https://github.com/yarnpkg/pnp-rs/pull/19)) 31 | - add `cargo check` and `cargo test --all-features` ([#18](https://github.com/yarnpkg/pnp-rs/pull/18)) 32 | - add rust-toolchain.toml ([#17](https://github.com/yarnpkg/pnp-rs/pull/17)) 33 | - disable more 34 | - enable most tests on windows CI 35 | -------------------------------------------------------------------------------- /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 = "adler2" 7 | version = "2.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.5.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 25 | 26 | [[package]] 27 | name = "bit-set" 28 | version = "0.8.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 31 | dependencies = [ 32 | "bit-vec", 33 | ] 34 | 35 | [[package]] 36 | name = "bit-vec" 37 | version = "0.8.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "1.3.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 46 | 47 | [[package]] 48 | name = "bitflags" 49 | version = "2.9.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 52 | 53 | [[package]] 54 | name = "byteorder" 55 | version = "1.5.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 58 | 59 | [[package]] 60 | name = "bytes" 61 | version = "1.10.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 70 | 71 | [[package]] 72 | name = "clean-path" 73 | version = "0.2.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5" 76 | 77 | [[package]] 78 | name = "combine" 79 | version = "4.6.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 82 | dependencies = [ 83 | "bytes", 84 | "memchr", 85 | ] 86 | 87 | [[package]] 88 | name = "concurrent_lru" 89 | version = "0.2.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "7feb5cb312f774e8a24540e27206db4e890f7d488563671d24a16389cf4c2e4e" 92 | dependencies = [ 93 | "once_cell", 94 | ] 95 | 96 | [[package]] 97 | name = "endian-type" 98 | version = "0.1.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 101 | 102 | [[package]] 103 | name = "enum-as-inner" 104 | version = "0.6.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 107 | dependencies = [ 108 | "heck", 109 | "proc-macro2", 110 | "quote", 111 | "syn", 112 | ] 113 | 114 | [[package]] 115 | name = "equivalent" 116 | version = "1.0.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 119 | 120 | [[package]] 121 | name = "fancy-regex" 122 | version = "0.14.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" 125 | dependencies = [ 126 | "bit-set", 127 | "regex-automata", 128 | "regex-syntax", 129 | ] 130 | 131 | [[package]] 132 | name = "futures-core" 133 | version = "0.3.31" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 136 | 137 | [[package]] 138 | name = "futures-macro" 139 | version = "0.3.31" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 142 | dependencies = [ 143 | "proc-macro2", 144 | "quote", 145 | "syn", 146 | ] 147 | 148 | [[package]] 149 | name = "futures-task" 150 | version = "0.3.31" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 153 | 154 | [[package]] 155 | name = "futures-timer" 156 | version = "3.0.3" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 159 | 160 | [[package]] 161 | name = "futures-util" 162 | version = "0.3.31" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 165 | dependencies = [ 166 | "futures-core", 167 | "futures-macro", 168 | "futures-task", 169 | "pin-project-lite", 170 | "pin-utils", 171 | "slab", 172 | ] 173 | 174 | [[package]] 175 | name = "glob" 176 | version = "0.3.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 179 | 180 | [[package]] 181 | name = "hashbrown" 182 | version = "0.15.4" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 185 | 186 | [[package]] 187 | name = "heck" 188 | version = "0.5.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 191 | 192 | [[package]] 193 | name = "indexmap" 194 | version = "2.10.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 197 | dependencies = [ 198 | "equivalent", 199 | "hashbrown", 200 | ] 201 | 202 | [[package]] 203 | name = "itoa" 204 | version = "1.0.15" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 207 | 208 | [[package]] 209 | name = "libc" 210 | version = "0.2.174" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 213 | 214 | [[package]] 215 | name = "mach2" 216 | version = "0.4.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" 219 | dependencies = [ 220 | "libc", 221 | ] 222 | 223 | [[package]] 224 | name = "memchr" 225 | version = "2.7.5" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 228 | 229 | [[package]] 230 | name = "memoffset" 231 | version = "0.7.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 234 | dependencies = [ 235 | "autocfg", 236 | ] 237 | 238 | [[package]] 239 | name = "miniz_oxide" 240 | version = "0.8.9" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 243 | dependencies = [ 244 | "adler2", 245 | ] 246 | 247 | [[package]] 248 | name = "mmap-rs" 249 | version = "0.6.1" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "86968d85441db75203c34deefd0c88032f275aaa85cee19a1dcfff6ae9df56da" 252 | dependencies = [ 253 | "bitflags 1.3.2", 254 | "combine", 255 | "libc", 256 | "mach2", 257 | "nix", 258 | "sysctl", 259 | "thiserror 1.0.69", 260 | "widestring", 261 | "windows", 262 | ] 263 | 264 | [[package]] 265 | name = "nibble_vec" 266 | version = "0.1.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 269 | dependencies = [ 270 | "smallvec", 271 | ] 272 | 273 | [[package]] 274 | name = "nix" 275 | version = "0.26.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 278 | dependencies = [ 279 | "bitflags 1.3.2", 280 | "cfg-if", 281 | "libc", 282 | "memoffset", 283 | "pin-utils", 284 | ] 285 | 286 | [[package]] 287 | name = "once_cell" 288 | version = "1.21.3" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 291 | 292 | [[package]] 293 | name = "path-slash" 294 | version = "0.2.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" 297 | 298 | [[package]] 299 | name = "pathdiff" 300 | version = "0.2.3" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 303 | 304 | [[package]] 305 | name = "pin-project-lite" 306 | version = "0.2.16" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 309 | 310 | [[package]] 311 | name = "pin-utils" 312 | version = "0.1.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 315 | 316 | [[package]] 317 | name = "pnp" 318 | version = "0.11.0" 319 | dependencies = [ 320 | "byteorder", 321 | "clean-path", 322 | "concurrent_lru", 323 | "fancy-regex", 324 | "miniz_oxide", 325 | "mmap-rs", 326 | "path-slash", 327 | "pathdiff", 328 | "radix_trie", 329 | "rstest", 330 | "rustc-hash", 331 | "serde", 332 | "serde_json", 333 | "thiserror 2.0.12", 334 | ] 335 | 336 | [[package]] 337 | name = "proc-macro-crate" 338 | version = "3.3.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 341 | dependencies = [ 342 | "toml_edit", 343 | ] 344 | 345 | [[package]] 346 | name = "proc-macro2" 347 | version = "1.0.95" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 350 | dependencies = [ 351 | "unicode-ident", 352 | ] 353 | 354 | [[package]] 355 | name = "quote" 356 | version = "1.0.40" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 359 | dependencies = [ 360 | "proc-macro2", 361 | ] 362 | 363 | [[package]] 364 | name = "radix_trie" 365 | version = "0.2.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 368 | dependencies = [ 369 | "endian-type", 370 | "nibble_vec", 371 | ] 372 | 373 | [[package]] 374 | name = "regex" 375 | version = "1.11.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 378 | dependencies = [ 379 | "aho-corasick", 380 | "memchr", 381 | "regex-automata", 382 | "regex-syntax", 383 | ] 384 | 385 | [[package]] 386 | name = "regex-automata" 387 | version = "0.4.9" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 390 | dependencies = [ 391 | "aho-corasick", 392 | "memchr", 393 | "regex-syntax", 394 | ] 395 | 396 | [[package]] 397 | name = "regex-syntax" 398 | version = "0.8.5" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 401 | 402 | [[package]] 403 | name = "relative-path" 404 | version = "1.9.3" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 407 | 408 | [[package]] 409 | name = "rstest" 410 | version = "0.25.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" 413 | dependencies = [ 414 | "futures-timer", 415 | "futures-util", 416 | "rstest_macros", 417 | "rustc_version", 418 | ] 419 | 420 | [[package]] 421 | name = "rstest_macros" 422 | version = "0.25.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" 425 | dependencies = [ 426 | "cfg-if", 427 | "glob", 428 | "proc-macro-crate", 429 | "proc-macro2", 430 | "quote", 431 | "regex", 432 | "relative-path", 433 | "rustc_version", 434 | "syn", 435 | "unicode-ident", 436 | ] 437 | 438 | [[package]] 439 | name = "rustc-hash" 440 | version = "2.1.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 443 | 444 | [[package]] 445 | name = "rustc_version" 446 | version = "0.4.1" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 449 | dependencies = [ 450 | "semver", 451 | ] 452 | 453 | [[package]] 454 | name = "ryu" 455 | version = "1.0.20" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 458 | 459 | [[package]] 460 | name = "same-file" 461 | version = "1.0.6" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 464 | dependencies = [ 465 | "winapi-util", 466 | ] 467 | 468 | [[package]] 469 | name = "semver" 470 | version = "1.0.26" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 473 | 474 | [[package]] 475 | name = "serde" 476 | version = "1.0.219" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 479 | dependencies = [ 480 | "serde_derive", 481 | ] 482 | 483 | [[package]] 484 | name = "serde_derive" 485 | version = "1.0.219" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 488 | dependencies = [ 489 | "proc-macro2", 490 | "quote", 491 | "syn", 492 | ] 493 | 494 | [[package]] 495 | name = "serde_json" 496 | version = "1.0.140" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 499 | dependencies = [ 500 | "itoa", 501 | "memchr", 502 | "ryu", 503 | "serde", 504 | ] 505 | 506 | [[package]] 507 | name = "slab" 508 | version = "0.4.10" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 511 | 512 | [[package]] 513 | name = "smallvec" 514 | version = "1.15.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 517 | 518 | [[package]] 519 | name = "syn" 520 | version = "2.0.104" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 523 | dependencies = [ 524 | "proc-macro2", 525 | "quote", 526 | "unicode-ident", 527 | ] 528 | 529 | [[package]] 530 | name = "sysctl" 531 | version = "0.5.5" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" 534 | dependencies = [ 535 | "bitflags 2.9.1", 536 | "byteorder", 537 | "enum-as-inner", 538 | "libc", 539 | "thiserror 1.0.69", 540 | "walkdir", 541 | ] 542 | 543 | [[package]] 544 | name = "thiserror" 545 | version = "1.0.69" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 548 | dependencies = [ 549 | "thiserror-impl 1.0.69", 550 | ] 551 | 552 | [[package]] 553 | name = "thiserror" 554 | version = "2.0.12" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 557 | dependencies = [ 558 | "thiserror-impl 2.0.12", 559 | ] 560 | 561 | [[package]] 562 | name = "thiserror-impl" 563 | version = "1.0.69" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 566 | dependencies = [ 567 | "proc-macro2", 568 | "quote", 569 | "syn", 570 | ] 571 | 572 | [[package]] 573 | name = "thiserror-impl" 574 | version = "2.0.12" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 577 | dependencies = [ 578 | "proc-macro2", 579 | "quote", 580 | "syn", 581 | ] 582 | 583 | [[package]] 584 | name = "toml_datetime" 585 | version = "0.6.11" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 588 | 589 | [[package]] 590 | name = "toml_edit" 591 | version = "0.22.27" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 594 | dependencies = [ 595 | "indexmap", 596 | "toml_datetime", 597 | "winnow", 598 | ] 599 | 600 | [[package]] 601 | name = "unicode-ident" 602 | version = "1.0.18" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 605 | 606 | [[package]] 607 | name = "walkdir" 608 | version = "2.5.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 611 | dependencies = [ 612 | "same-file", 613 | "winapi-util", 614 | ] 615 | 616 | [[package]] 617 | name = "widestring" 618 | version = "1.2.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 621 | 622 | [[package]] 623 | name = "winapi-util" 624 | version = "0.1.9" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 627 | dependencies = [ 628 | "windows-sys", 629 | ] 630 | 631 | [[package]] 632 | name = "windows" 633 | version = "0.48.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 636 | dependencies = [ 637 | "windows-targets 0.48.5", 638 | ] 639 | 640 | [[package]] 641 | name = "windows-sys" 642 | version = "0.59.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 645 | dependencies = [ 646 | "windows-targets 0.52.6", 647 | ] 648 | 649 | [[package]] 650 | name = "windows-targets" 651 | version = "0.48.5" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 654 | dependencies = [ 655 | "windows_aarch64_gnullvm 0.48.5", 656 | "windows_aarch64_msvc 0.48.5", 657 | "windows_i686_gnu 0.48.5", 658 | "windows_i686_msvc 0.48.5", 659 | "windows_x86_64_gnu 0.48.5", 660 | "windows_x86_64_gnullvm 0.48.5", 661 | "windows_x86_64_msvc 0.48.5", 662 | ] 663 | 664 | [[package]] 665 | name = "windows-targets" 666 | version = "0.52.6" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 669 | dependencies = [ 670 | "windows_aarch64_gnullvm 0.52.6", 671 | "windows_aarch64_msvc 0.52.6", 672 | "windows_i686_gnu 0.52.6", 673 | "windows_i686_gnullvm", 674 | "windows_i686_msvc 0.52.6", 675 | "windows_x86_64_gnu 0.52.6", 676 | "windows_x86_64_gnullvm 0.52.6", 677 | "windows_x86_64_msvc 0.52.6", 678 | ] 679 | 680 | [[package]] 681 | name = "windows_aarch64_gnullvm" 682 | version = "0.48.5" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 685 | 686 | [[package]] 687 | name = "windows_aarch64_gnullvm" 688 | version = "0.52.6" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 691 | 692 | [[package]] 693 | name = "windows_aarch64_msvc" 694 | version = "0.48.5" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 697 | 698 | [[package]] 699 | name = "windows_aarch64_msvc" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 703 | 704 | [[package]] 705 | name = "windows_i686_gnu" 706 | version = "0.48.5" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 709 | 710 | [[package]] 711 | name = "windows_i686_gnu" 712 | version = "0.52.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 715 | 716 | [[package]] 717 | name = "windows_i686_gnullvm" 718 | version = "0.52.6" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 721 | 722 | [[package]] 723 | name = "windows_i686_msvc" 724 | version = "0.48.5" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 727 | 728 | [[package]] 729 | name = "windows_i686_msvc" 730 | version = "0.52.6" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 733 | 734 | [[package]] 735 | name = "windows_x86_64_gnu" 736 | version = "0.48.5" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 739 | 740 | [[package]] 741 | name = "windows_x86_64_gnu" 742 | version = "0.52.6" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 745 | 746 | [[package]] 747 | name = "windows_x86_64_gnullvm" 748 | version = "0.48.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 751 | 752 | [[package]] 753 | name = "windows_x86_64_gnullvm" 754 | version = "0.52.6" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 757 | 758 | [[package]] 759 | name = "windows_x86_64_msvc" 760 | version = "0.48.5" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 763 | 764 | [[package]] 765 | name = "windows_x86_64_msvc" 766 | version = "0.52.6" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 769 | 770 | [[package]] 771 | name = "winnow" 772 | version = "0.7.11" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" 775 | dependencies = [ 776 | "memchr", 777 | ] 778 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pnp" 3 | version = "0.11.0" 4 | edition = "2021" 5 | license = "BSD-2-Clause" 6 | description = "Resolution primitives for Yarn PnP" 7 | homepage = "https://yarnpkg.com" 8 | repository = "https://github.com/yarnpkg/pnp-rs" 9 | 10 | [dependencies] 11 | byteorder = "1" 12 | clean-path = "0.2.1" 13 | concurrent_lru = "^0.2" 14 | fancy-regex = { version = "^0.14.0", default-features = false, features = ["std"] } 15 | miniz_oxide = "^0.8" 16 | mmap-rs = { version = "^0.6", optional = true } 17 | path-slash = "0.2.1" 18 | pathdiff = "^0.2" 19 | radix_trie = "0.2.1" 20 | serde = { version = "1", features = ["derive"] } 21 | serde_json = "1" 22 | thiserror = "2" 23 | rustc-hash = "2" 24 | 25 | [dev-dependencies] 26 | rstest = "0.25.0" 27 | 28 | [features] 29 | mmap = ["dep:mmap-rs"] 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2016-present, Yarn Contributors. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pnp-rs` 2 | 3 | This crate implements the Yarn Plug'n'Play [resolution algorithms](https://yarnpkg.com/advanced/pnp-spec) for Rust so that it can be easily reused within Rust-based tools. It also includes utilities allowing to transparently read files from within zip archives. 4 | 5 | ## Install 6 | 7 | ``` 8 | cargo add pnp 9 | ``` 10 | 11 | ## Resolution 12 | 13 | ```rust 14 | fn example() { 15 | let manifest 16 | = load_pnp_manifest(".pnp.cjs").unwrap(); 17 | 18 | let host = ResolutionHost { 19 | find_pnp_manifest: Box::new(move |_| Ok(Some(manifest.clone()))), 20 | ..Default::default() 21 | }; 22 | 23 | let config = ResolutionConfig { 24 | host, 25 | ..Default::default() 26 | }; 27 | 28 | let resolution = resolve_to_unqualified( 29 | "lodash/cloneDeep", 30 | std::path::PathBuf::from("/path/to/index.js"), 31 | &config, 32 | ); 33 | 34 | match resolution { 35 | Ok(Resolution::Resolved(path, subpath)) => { 36 | // path = "/path/to/lodash.zip" 37 | // subpath = "cloneDeep" 38 | }, 39 | Ok(Resolution::Skipped) => { 40 | // This is returned when the PnP resolver decides that it shouldn't 41 | // handle the resolution for this particular specifier. In that case, 42 | // the specifier should be forwarded to the default resolver. 43 | }, 44 | Err(err) => { 45 | // An error happened during the resolution. Falling back to the default 46 | // resolver isn't recommended. 47 | }, 48 | }; 49 | } 50 | ``` 51 | 52 | ## Filesystem utilities 53 | 54 | While PnP only deals with the resolution, not the filesystem, the file maps generated by Yarn rely on virtual filesystem layers for two reasons: 55 | 56 | - [Virtual packages](https://yarnpkg.com/advanced/lexicon#virtual-package), which require a same package to have different paths to account for different set of dependencies (this only happens for packages that list peer dependencies) 57 | 58 | - Zip storage, which Yarn uses so the installed files never have to be unpacked from their archives, leading to faster installs and fewer risks of cache corruption. 59 | 60 | To make it easier to work with these virtual filesystems, the `pnp` crate also includes a `VPath` enum that lets you resolve virtual paths, and a set of zip manipulation utils (`open_zip_via_read` by default, and `open_zip_via_mmap` if the `mmap` feature is enabled). 61 | 62 | ```rust 63 | use pnp::fs::{VPath, open_zip_via_read}; 64 | 65 | fn read_file(p: PathBuf) -> std::io::Result { 66 | match VPath::from(&p).unwrap() { 67 | // The path was virtual and stored within a zip file; we need to read from the zip file 68 | // Note that this opens the zip file every time, which is expensive; we'll see how to optimize that 69 | VPath::Zip(info) => { 70 | open_zip_via_read(info.physical_base_path()).unwrap().read_to_string(&zip_path) 71 | }, 72 | 73 | // The path was virtual but not a zip file; we just need to read from the provided location 74 | VPath::Virtual(info) => { 75 | std::fs::read_to_string(info.physical_base_path()) 76 | }, 77 | 78 | // Nothing special to do, it's a regular path 79 | VPath::Native(p) => { 80 | std::fs::read_to_string(&p) 81 | }, 82 | } 83 | } 84 | ``` 85 | 86 | ## Cache reuse 87 | 88 | Opening and dropping a zip archive for every single file access would be expensive. To avoid that, `pnp-rs` provides an helper class called `LruZipCache` which lets you abstract away the zip opening and closing, and only keep the most recently used archives open. 89 | 90 | ```rust 91 | use pnp::fs::{VPath, LruZipCache, open_zip_via_read}; 92 | 93 | const ZIP_CACHE: Lazy>> = Lazy::new(|| { 94 | // It'll keep the last 50 zip archives open 95 | LruZipCache::new(50, open_zip_via_read_p) 96 | }); 97 | 98 | fn read_file(p: PathBuf) -> std::io::Result { 99 | match VPath::from(&p).unwrap() { 100 | // The path was virtual and stored within a zip file; we need to read from the zip file 101 | VPath::Zip(info) => { 102 | ZIP_CACHE.read_to_string(info.physical_base_path(), &zip_path) 103 | }, 104 | 105 | // The path was virtual but not a zip file; we just need to read from the provided location 106 | VPath::Virtual(info) => { 107 | std::fs::read_to_string(info.physical_base_path()) 108 | }, 109 | 110 | // Nothing special to do, it's a regular path 111 | VPath::Native(p) => { 112 | std::fs::read_to_string(&p) 113 | }, 114 | } 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarnpkg/pnp-rs/21b071ac22c0930750b15f15b476cfeb06892d56/data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip -------------------------------------------------------------------------------- /data/edge_case_manifest_state.json: -------------------------------------------------------------------------------- 1 | { 2 | "__info": [ 3 | "This file is automatically generated. Do not touch it, or risk", 4 | "your modifications being lost." 5 | ], 6 | "dependencyTreeRoots": [ 7 | { 8 | "name": "rspack-link", 9 | "reference": "workspace:." 10 | } 11 | ], 12 | "enableTopLevelFallback": true, 13 | "ignorePatternData": "(^(?:\\.yarn\\/sdks(?:\\/(?!\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)|$))$)", 14 | "fallbackExclusionList": [ 15 | [ 16 | "rspack-link", 17 | [ 18 | "workspace:." 19 | ] 20 | ] 21 | ], 22 | "fallbackPool": [], 23 | "packageRegistryData": [ 24 | [ 25 | null, 26 | [ 27 | [ 28 | null, 29 | { 30 | "packageLocation": "./", 31 | "packageDependencies": [ 32 | ], 33 | "linkType": "SOFT" 34 | } 35 | ] 36 | ] 37 | ], 38 | 39 | [ 40 | "@carbon/icon-helpers", 41 | [ 42 | [ 43 | "npm:10.54.0", 44 | { 45 | "packageLocation": "./.yarn/unplugged/@carbon-icon-helpers-npm-10.54.0-a58f8b7b6c/node_modules/@carbon/icon-helpers/", 46 | "packageDependencies": [ 47 | [ 48 | "@carbon/icon-helpers", 49 | "npm:10.54.0" 50 | ], 51 | [ 52 | "@ibm/telemetry-js", 53 | "npm:1.9.1" 54 | ] 55 | ], 56 | "linkType": "HARD" 57 | } 58 | ] 59 | ] 60 | ], 61 | [ 62 | "@carbon/icons-react", 63 | [ 64 | [ 65 | "npm:11.54.0", 66 | { 67 | "packageLocation": "./.yarn/unplugged/@carbon-icons-react-virtual-379302d360/node_modules/@carbon/icons-react/", 68 | "packageDependencies": [ 69 | [ 70 | "@carbon/icons-react", 71 | "npm:11.54.0" 72 | ] 73 | ], 74 | "linkType": "SOFT" 75 | } 76 | ], 77 | [ 78 | "virtual:ed977161de61e6995bdb8c18ad719dac99ebc9dc1b7317c42e54ab394643509c7ea342abb2b214efc589efcb79dc9deae3ca4092870cbe691d6377887658443c#npm:11.54.0", 79 | { 80 | "packageLocation": "./.yarn/unplugged/@carbon-icons-react-virtual-379302d360/node_modules/@carbon/icons-react/", 81 | "packageDependencies": [ 82 | [ 83 | "@carbon/icons-react", 84 | "virtual:ed977161de61e6995bdb8c18ad719dac99ebc9dc1b7317c42e54ab394643509c7ea342abb2b214efc589efcb79dc9deae3ca4092870cbe691d6377887658443c#npm:11.54.0" 85 | ], 86 | [ 87 | "@carbon/icon-helpers", 88 | "npm:10.54.0" 89 | ], 90 | [ 91 | "@ibm/telemetry-js", 92 | "npm:1.9.1" 93 | ], 94 | [ 95 | "@types/react", 96 | null 97 | ], 98 | [ 99 | "prop-types", 100 | "npm:15.8.1" 101 | ], 102 | [ 103 | "react", 104 | "npm:19.0.0" 105 | ] 106 | ], 107 | "packagePeers": [ 108 | "@types/react", 109 | "react" 110 | ], 111 | "linkType": "HARD" 112 | } 113 | ] 114 | ] 115 | ] 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /data/test-expectations.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "manifest": { 3 | "__info": [], 4 | "dependencyTreeRoots": [{ 5 | "name": "root", 6 | "reference": "workspace:." 7 | }], 8 | "ignorePatternData": null, 9 | "enableTopLevelFallback": false, 10 | "fallbackPool": [], 11 | "fallbackExclusionList": [], 12 | "packageRegistryData": [ 13 | [null, [ 14 | [null, { 15 | "packageLocation": "./", 16 | "packageDependencies": [["test", "npm:1.0.0"]], 17 | "linkType": "SOFT" 18 | }] 19 | ]], 20 | ["root", [ 21 | ["workspace:.", { 22 | "packageLocation": "./", 23 | "packageDependencies": [["test", "npm:1.0.0"]], 24 | "linkType": "SOFT" 25 | }] 26 | ]], 27 | ["workspace-alias-dependency", [ 28 | ["workspace:workspace-alias-dependency", { 29 | "packageLocation": "./workspace-alias-dependency/", 30 | "packageDependencies": [["alias", ["test", "npm:1.0.0"]]], 31 | "linkType": "SOFT" 32 | }] 33 | ]], 34 | ["workspace-self-dependency", [ 35 | ["workspace:workspace-self-dependency", { 36 | "packageLocation": "./workspace-self-dependency/", 37 | "packageDependencies": [["workspace-self-dependency", "workspace:workspace-self-dependency"]], 38 | "linkType": "SOFT" 39 | }] 40 | ]], 41 | ["workspace-unfulfilled-peer-dependency", [ 42 | ["workspace:workspace-unfulfilled-peer-dependency", { 43 | "packageLocation": "./workspace-unfulfilled-peer-dependency/", 44 | "packageDependencies": [["test", null]], 45 | "linkType": "SOFT" 46 | }] 47 | ]], 48 | ["longer", [ 49 | ["workspace:longer", { 50 | "packageLocation": "./longer/", 51 | "packageDependencies": [["test", "npm:2.0.0"]], 52 | "linkType": "SOFT" 53 | }] 54 | ]], 55 | ["long", [ 56 | ["workspace:long", { 57 | "packageLocation": "./long/", 58 | "packageDependencies": [["test", "npm:1.0.0"]], 59 | "linkType": "SOFT" 60 | }] 61 | ]], 62 | ["longerer", [ 63 | ["workspace:longerer", { 64 | "packageLocation": "./longerer/", 65 | "packageDependencies": [["test", "npm:3.0.0"]], 66 | "linkType": "SOFT" 67 | }] 68 | ]], 69 | ["test", [ 70 | ["npm:1.0.0", { 71 | "packageLocation": "./test-1.0.0/", 72 | "packageDependencies": [], 73 | "linkType": "HARD" 74 | }], 75 | ["npm:2.0.0", { 76 | "packageLocation": "./test-2.0.0/", 77 | "packageDependencies": [], 78 | "linkType": "HARD" 79 | }], 80 | ["npm:3.0.0", { 81 | "packageLocation": "./test-3.0.0/", 82 | "packageDependencies": [], 83 | "linkType": "HARD" 84 | }] 85 | ]] 86 | ] 87 | }, 88 | "tests": [{ 89 | "it": "should allow a package to import one of its dependencies", 90 | "imported": "test", 91 | "importer": "/path/to/project/", 92 | "expected": "/path/to/project/test-1.0.0/" 93 | }, { 94 | "it": "should allow a package to import itself, if specified in its own dependencies", 95 | "imported": "workspace-self-dependency", 96 | "importer": "/path/to/project/workspace-self-dependency/", 97 | "expected": "/path/to/project/workspace-self-dependency/" 98 | }, { 99 | "it": "should allow a package to import an aliased dependency", 100 | "imported": "alias", 101 | "importer": "/path/to/project/workspace-alias-dependency/", 102 | "expected": "/path/to/project/test-1.0.0/" 103 | }, { 104 | "it": "shouldn't allow a package to import something that isn't one of its dependencies", 105 | "imported": "missing-dependency", 106 | "importer": "/path/to/project/", 107 | "expected": "error!" 108 | }, { 109 | "it": "shouldn't accidentally discard the trailing slash from the package locations", 110 | "imported": "test", 111 | "importer": "/path/to/project/long/", 112 | "expected": "/path/to/project/test-1.0.0/" 113 | }, { 114 | "it": "should throw an exception when trying to access an unfulfilled peer dependency", 115 | "imported": "test", 116 | "importer": "/path/to/project/workspace-unfulfilled-peer-dependency/", 117 | "expected": "error!" 118 | }] 119 | }, { 120 | "manifest": { 121 | "__info": [], 122 | "dependencyTreeRoots": [{ 123 | "name": "root", 124 | "reference": "workspace:." 125 | }], 126 | "ignorePatternData": null, 127 | "enableTopLevelFallback": true, 128 | "fallbackPool": [ 129 | ["test-2", "npm:1.0.0"], 130 | ["alias", ["test-1", "npm:1.0.0"]] 131 | ], 132 | "fallbackExclusionList": [[ 133 | "workspace-no-fallbacks", 134 | ["workspace:workspace-no-fallbacks"] 135 | ]], 136 | "packageRegistryData": [ 137 | [null, [ 138 | [null, { 139 | "packageLocation": "./", 140 | "packageDependencies": [["test-1", "npm:1.0.0"]], 141 | "linkType": "SOFT" 142 | }] 143 | ]], 144 | ["root", [ 145 | ["workspace:.", { 146 | "packageLocation": "./", 147 | "packageDependencies": [["test-1", "npm:1.0.0"]], 148 | "linkType": "SOFT" 149 | }] 150 | ]], 151 | ["workspace-no-fallbacks", [ 152 | ["workspace:workspace-no-fallbacks", { 153 | "packageLocation": "./workspace-no-fallbacks/", 154 | "packageDependencies": [], 155 | "linkType": "SOFT" 156 | }] 157 | ]], 158 | ["workspace-with-fallbacks", [ 159 | ["workspace:workspace-with-fallbacks", { 160 | "packageLocation": "./workspace-with-fallbacks/", 161 | "packageDependencies": [], 162 | "linkType": "SOFT" 163 | }] 164 | ]], 165 | ["workspace-unfulfilled-peer-dependency", [ 166 | ["workspace:workspace-unfulfilled-peer-dependency", { 167 | "packageLocation": "./workspace-unfulfilled-peer-dependency/", 168 | "packageDependencies": [ 169 | ["test-1", null], 170 | ["test-2", null] 171 | ], 172 | "linkType": "SOFT" 173 | }] 174 | ]], 175 | ["test-1", [ 176 | ["npm:1.0.0", { 177 | "packageLocation": "./test-1/", 178 | "packageDependencies": [], 179 | "linkType": "HARD" 180 | }] 181 | ]], 182 | ["test-2", [ 183 | ["npm:1.0.0", { 184 | "packageLocation": "./test-2/", 185 | "packageDependencies": [], 186 | "linkType": "HARD" 187 | }] 188 | ]] 189 | ] 190 | }, 191 | "tests": [{ 192 | "it": "should allow resolution coming from the fallback pool if enableTopLevelFallback is set to true", 193 | "imported": "test-1", 194 | "importer": "/path/to/project/", 195 | "expected": "/path/to/project/test-1/" 196 | }, { 197 | "it": "should allow the fallback pool to contain aliases", 198 | "imported": "alias", 199 | "importer": "/path/to/project/", 200 | "expected": "/path/to/project/test-1/" 201 | }, { 202 | "it": "shouldn't use the fallback pool when the importer package is listed in fallbackExclusionList", 203 | "imported": "test-1", 204 | "importer": "/path/to/project/workspace-no-fallbacks/", 205 | "expected": "error!" 206 | }, { 207 | "it": "should implicitly use the top-level package dependencies as part of the fallback pool", 208 | "imported": "test-2", 209 | "importer": "/path/to/project/workspace-with-fallbacks/", 210 | "expected": "/path/to/project/test-2/" 211 | }, { 212 | "it": "should throw an error if a resolution isn't in in the package dependencies, nor inside the fallback pool", 213 | "imported": "test-3", 214 | "importer": "/path/to/project/workspace-with-fallbacks/", 215 | "expected": "error!" 216 | }, { 217 | "it": "should use the top-level fallback if a dependency is missing because of an unfulfilled peer dependency", 218 | "imported": "test-1", 219 | "importer": "/path/to/project/workspace-unfulfilled-peer-dependency/", 220 | "expected": "/path/to/project/test-1/" 221 | }, { 222 | "it": "should use the fallback pool if a dependency is missing because of an unfulfilled peer dependency", 223 | "imported": "test-2", 224 | "importer": "/path/to/project/workspace-unfulfilled-peer-dependency/", 225 | "expected": "/path/to/project/test-2/" 226 | }] 227 | }, { 228 | "manifest": { 229 | "__info": [], 230 | "dependencyTreeRoots": [{ 231 | "name": "root", 232 | "reference": "workspace:." 233 | }], 234 | "ignorePatternData": null, 235 | "enableTopLevelFallback": false, 236 | "fallbackPool": [ 237 | ["test", "npm:1.0.0"] 238 | ], 239 | "fallbackExclusionList": [], 240 | "packageRegistryData": [ 241 | [null, [ 242 | [null, { 243 | "packageLocation": "./", 244 | "packageDependencies": [], 245 | "linkType": "SOFT" 246 | }] 247 | ]], 248 | ["root", [ 249 | ["workspace:.", { 250 | "packageLocation": "./", 251 | "packageDependencies": [], 252 | "linkType": "SOFT" 253 | }] 254 | ]], 255 | ["test", [ 256 | ["npm:1.0.0", { 257 | "packageLocation": "./test-1/", 258 | "packageDependencies": [], 259 | "linkType": "HARD" 260 | }] 261 | ]] 262 | ] 263 | }, 264 | "tests": [{ 265 | "it": "should ignore the fallback pool if enableTopLevelFallback is set to false", 266 | "imported": "test", 267 | "importer": "/path/to/project/", 268 | "expected": "error!" 269 | }] 270 | }, { 271 | "manifest": { 272 | "__info": [], 273 | "dependencyTreeRoots": [{ 274 | "name": "root", 275 | "reference": "workspace:." 276 | }], 277 | "ignorePatternData": "^not-a-workspace(/|$)", 278 | "enableTopLevelFallback": false, 279 | "fallbackPool": [], 280 | "fallbackExclusionList": [], 281 | "packageRegistryData": [ 282 | [null, [ 283 | [null, { 284 | "packageLocation": "./", 285 | "packageDependencies": [], 286 | "linkType": "SOFT" 287 | }] 288 | ]], 289 | ["root", [ 290 | ["workspace:.", { 291 | "packageLocation": "./", 292 | "packageDependencies": [["test", "npm:1.0.0"]], 293 | "linkType": "SOFT" 294 | }] 295 | ]], 296 | ["test", [ 297 | ["npm:1.0.0", { 298 | "packageLocation": "./test/", 299 | "packageDependencies": [], 300 | "linkType": "HARD" 301 | }] 302 | ]] 303 | ] 304 | }, 305 | "tests": [{ 306 | "it": "shouldn't go through PnP when trying to resolve dependencies from packages covered by ignorePatternData", 307 | "imported": "test", 308 | "importer": "/path/to/project/not-a-workspace/", 309 | "expected": "test" 310 | }] 311 | }, { 312 | "manifest": { 313 | "__info": [ 314 | "This file is automatically generated. Do not touch it, or risk", 315 | "your modifications being lost." 316 | ], 317 | "dependencyTreeRoots": [{ 318 | "name": "root", 319 | "reference": "workspace:." 320 | }], 321 | "enableTopLevelFallback": true, 322 | "ignorePatternData": null, 323 | "fallbackExclusionList": [], 324 | "fallbackPool": [], 325 | "packageRegistryData": [ 326 | [null, [ 327 | [null, { 328 | "packageLocation": "./", 329 | "packageDependencies": [ 330 | ["root", "workspace:."], 331 | ["pad-left", "npm:2.1.0"] 332 | ], 333 | "linkType": "SOFT" 334 | }] 335 | ]], 336 | ["my-project", [ 337 | ["workspace:.", { 338 | "packageLocation": "./", 339 | "packageDependencies": [ 340 | ["root", "workspace:."], 341 | ["pad-left", "npm:2.1.0"] 342 | ], 343 | "linkType": "SOFT" 344 | }] 345 | ]], 346 | ["pad-left", [ 347 | ["npm:2.1.0", { 348 | "packageLocation": "../yarn/global/cache/pad-left-npm-2.1.0-ffe13d2d40-10c0.zip/node_modules/pad-left/", 349 | "packageDependencies": [ 350 | ["pad-left", "npm:2.1.0"], 351 | ["repeat-string", "npm:1.6.1"] 352 | ], 353 | "linkType": "HARD" 354 | }] 355 | ]], 356 | ["repeat-string", [ 357 | ["npm:1.6.1", { 358 | "packageLocation": "../yarn/global/cache/repeat-string-npm-1.6.1-bc8e388655-10c0.zip/node_modules/repeat-string/", 359 | "packageDependencies": [ 360 | ["repeat-string", "npm:1.6.1"] 361 | ], 362 | "linkType": "HARD" 363 | }] 364 | ]] 365 | ] 366 | }, 367 | "tests": [{ 368 | "it": "should resolve global packages", 369 | "imported": "pad-left", 370 | "importer": "/path/to/project/", 371 | "expected": "/path/to/yarn/global/cache/pad-left-npm-2.1.0-ffe13d2d40-10c0.zip/node_modules/pad-left/" 372 | }, { 373 | "it": "should resolve global packages from third-party dependencies", 374 | "imported": "repeat-string", 375 | "importer": "/path/to/yarn/global/cache/pad-left-npm-2.1.0-ffe13d2d40-10c0.zip/node_modules/pad-left/", 376 | "expected": "/path/to/yarn/global/cache/repeat-string-npm-1.6.1-bc8e388655-10c0.zip/node_modules/repeat-string/" 377 | }] 378 | }] 379 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.88.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /src/builtins.rs: -------------------------------------------------------------------------------- 1 | pub const NODEJS_BUILTINS: &[&str] = &[ 2 | "assert", 3 | "assert/strict", 4 | "async_hooks", 5 | "buffer", 6 | "child_process", 7 | "cluster", 8 | "console", 9 | "constants", 10 | "crypto", 11 | "dgram", 12 | "diagnostics_channel", 13 | "dns", 14 | "dns/promises", 15 | "domain", 16 | "events", 17 | "fs", 18 | "fs/promises", 19 | "http", 20 | "http2", 21 | "https", 22 | "inspector", 23 | "module", 24 | "net", 25 | "os", 26 | "path", 27 | "path/posix", 28 | "path/win32", 29 | "perf_hooks", 30 | "process", 31 | "punycode", 32 | "querystring", 33 | "readline", 34 | "readline/promises", 35 | "repl", 36 | "stream", 37 | "stream/consumers", 38 | "stream/promises", 39 | "stream/web", 40 | "string_decoder", 41 | "sys", 42 | "timers", 43 | "timers/promises", 44 | "tls", 45 | "trace_events", 46 | "tty", 47 | "url", 48 | "util", 49 | "util/types", 50 | "v8", 51 | "vm", 52 | "worker_threads", 53 | "zlib", 54 | ]; 55 | 56 | pub fn is_nodejs_builtin(s: &str) -> bool { 57 | NODEJS_BUILTINS.binary_search(&s).is_ok() 58 | } 59 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use thiserror::Error; 4 | 5 | use crate::PackageLocator; 6 | 7 | #[derive(Debug, Clone, PartialEq, Error)] 8 | pub enum Error { 9 | #[error(transparent)] 10 | BadSpecifier(Box), 11 | 12 | #[error(transparent)] 13 | FailedManifestHydration(Box), 14 | 15 | #[error(transparent)] 16 | MissingPeerDependency(Box), 17 | 18 | #[error(transparent)] 19 | UndeclaredDependency(Box), 20 | 21 | #[error(transparent)] 22 | MissingDependency(Box), 23 | } 24 | 25 | #[derive(Debug, Clone, PartialEq, Error)] 26 | #[error("{message}")] 27 | pub struct BadSpecifier { 28 | pub message: String, 29 | pub specifier: String, 30 | } 31 | 32 | #[derive(Debug, Clone, PartialEq, Error)] 33 | #[error("{message}")] 34 | pub struct FailedManifestHydration { 35 | pub message: String, 36 | pub manifest_path: PathBuf, 37 | } 38 | 39 | #[derive(Debug, Clone, PartialEq, Error)] 40 | #[error("{message}")] 41 | pub struct MissingPeerDependency { 42 | pub message: String, 43 | pub request: String, 44 | 45 | pub dependency_name: String, 46 | 47 | pub issuer_locator: PackageLocator, 48 | pub issuer_path: PathBuf, 49 | 50 | pub broken_ancestors: Vec, 51 | } 52 | 53 | #[derive(Debug, Clone, PartialEq, Error)] 54 | #[error("{message}")] 55 | pub struct UndeclaredDependency { 56 | pub message: String, 57 | pub request: String, 58 | 59 | pub dependency_name: String, 60 | 61 | pub issuer_locator: PackageLocator, 62 | pub issuer_path: PathBuf, 63 | } 64 | 65 | #[derive(Debug, Clone, PartialEq, Error)] 66 | #[error("{message}")] 67 | pub struct MissingDependency { 68 | pub message: String, 69 | pub request: String, 70 | 71 | pub dependency_locator: PackageLocator, 72 | pub dependency_name: String, 73 | 74 | pub issuer_locator: PackageLocator, 75 | pub issuer_path: PathBuf, 76 | } 77 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::{ 3 | path::{Path, PathBuf}, 4 | str::Utf8Error, 5 | }; 6 | 7 | use crate::zip::Zip; 8 | 9 | #[derive(Clone, Debug, PartialEq, Eq)] 10 | pub enum FileType { 11 | File, 12 | Directory, 13 | } 14 | 15 | #[derive(Clone, Debug, Deserialize, PartialEq)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct ZipInfo { 18 | pub base_path: String, 19 | pub virtual_segments: Option<(String, String)>, 20 | pub zip_path: String, 21 | } 22 | 23 | #[derive(Clone, Debug, Deserialize, PartialEq)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct VirtualInfo { 26 | pub base_path: String, 27 | pub virtual_segments: (String, String), 28 | } 29 | 30 | pub trait VPathInfo { 31 | fn physical_base_path(&self) -> PathBuf; 32 | } 33 | 34 | impl VPathInfo for ZipInfo { 35 | fn physical_base_path(&self) -> PathBuf { 36 | match &self.virtual_segments { 37 | None => PathBuf::from(&self.base_path), 38 | Some(segments) => PathBuf::from(&self.base_path).join(&segments.1), 39 | } 40 | } 41 | } 42 | 43 | impl VPathInfo for VirtualInfo { 44 | fn physical_base_path(&self) -> PathBuf { 45 | PathBuf::from(&self.base_path).join(&self.virtual_segments.1) 46 | } 47 | } 48 | 49 | #[derive(Clone, Debug, Deserialize, PartialEq)] 50 | #[serde(untagged)] 51 | pub enum VPath { 52 | Zip(ZipInfo), 53 | Virtual(VirtualInfo), 54 | Native(PathBuf), 55 | } 56 | 57 | impl VPath { 58 | pub fn from(p: &Path) -> std::io::Result { 59 | vpath(p) 60 | } 61 | } 62 | 63 | #[derive(thiserror::Error, Debug)] 64 | pub enum Error { 65 | #[error("Entry not found")] 66 | EntryNotFound, 67 | 68 | #[error("Unsupported compression")] 69 | UnsupportedCompression, 70 | 71 | #[error("Decompression error")] 72 | DecompressionError, 73 | 74 | #[error(transparent)] 75 | Utf8Error(#[from] Utf8Error), 76 | 77 | #[error(transparent)] 78 | IOError(#[from] std::io::Error), 79 | } 80 | 81 | #[cfg(feature = "mmap")] 82 | pub fn open_zip_via_mmap>(p: P) -> Result, std::io::Error> { 83 | let file = std::fs::File::open(p)?; 84 | 85 | let mmap_builder = 86 | mmap_rs::MmapOptions::new(file.metadata().unwrap().len().try_into().unwrap()).unwrap(); 87 | 88 | let mmap = unsafe { mmap_builder.with_file(&file, 0).map().unwrap() }; 89 | 90 | let zip = Zip::new(mmap).map_err(|_| std::io::Error::other("Failed to read the zip file"))?; 91 | 92 | Ok(zip) 93 | } 94 | 95 | #[cfg(feature = "mmap")] 96 | pub fn open_zip_via_mmap_p(p: &Path) -> Result, std::io::Error> { 97 | open_zip_via_mmap(p) 98 | } 99 | 100 | pub fn open_zip_via_read>(p: P) -> Result>, std::io::Error> { 101 | let data = std::fs::read(p)?; 102 | 103 | let zip = Zip::new(data).map_err(|_| std::io::Error::other("Failed to read the zip file"))?; 104 | 105 | Ok(zip) 106 | } 107 | 108 | pub fn open_zip_via_read_p(p: &Path) -> Result>, std::io::Error> { 109 | open_zip_via_read(p) 110 | } 111 | 112 | pub trait ZipCache 113 | where 114 | Storage: AsRef<[u8]> + Send + Sync, 115 | { 116 | fn act, F: FnOnce(&Zip) -> T>( 117 | &self, 118 | p: P, 119 | cb: F, 120 | ) -> Result; 121 | 122 | fn file_type, S: AsRef>( 123 | &self, 124 | zip_path: P, 125 | sub: S, 126 | ) -> Result; 127 | fn read, S: AsRef>( 128 | &self, 129 | zip_path: P, 130 | sub: S, 131 | ) -> Result, std::io::Error>; 132 | fn read_to_string, S: AsRef>( 133 | &self, 134 | zip_path: P, 135 | sub: S, 136 | ) -> Result; 137 | } 138 | 139 | #[derive(Debug)] 140 | pub struct LruZipCache 141 | where 142 | Storage: AsRef<[u8]> + Send + Sync, 143 | { 144 | lru: concurrent_lru::sharded::LruCache>, 145 | open: fn(&Path) -> std::io::Result>, 146 | } 147 | 148 | impl LruZipCache 149 | where 150 | Storage: AsRef<[u8]> + Send + Sync, 151 | { 152 | pub fn new(n: u64, open: fn(&Path) -> std::io::Result>) -> LruZipCache { 153 | LruZipCache { lru: concurrent_lru::sharded::LruCache::new(n), open } 154 | } 155 | } 156 | 157 | impl ZipCache for LruZipCache 158 | where 159 | Storage: AsRef<[u8]> + Send + Sync, 160 | { 161 | fn act, F: FnOnce(&Zip) -> T>( 162 | &self, 163 | p: P, 164 | cb: F, 165 | ) -> Result { 166 | let zip = self.lru.get_or_try_init(p.as_ref().to_path_buf(), 1, |p| (self.open)(p))?; 167 | 168 | Ok(cb(zip.value())) 169 | } 170 | 171 | fn file_type, S: AsRef>( 172 | &self, 173 | zip_path: P, 174 | p: S, 175 | ) -> Result { 176 | self.act(zip_path, |zip| zip.file_type(p.as_ref()))? 177 | } 178 | 179 | fn read, S: AsRef>( 180 | &self, 181 | zip_path: P, 182 | p: S, 183 | ) -> Result, std::io::Error> { 184 | self.act(zip_path, |zip| zip.read(p.as_ref()))? 185 | } 186 | 187 | fn read_to_string, S: AsRef>( 188 | &self, 189 | zip_path: P, 190 | p: S, 191 | ) -> Result { 192 | self.act(zip_path, |zip| zip.read_to_string(p.as_ref()))? 193 | } 194 | } 195 | 196 | fn vpath(p: &Path) -> std::io::Result { 197 | let Some(p_str) = p.as_os_str().to_str() else { 198 | return Ok(VPath::Native(p.to_path_buf())); 199 | }; 200 | 201 | let normalized_path = crate::util::normalize_path(p_str); 202 | 203 | // We remove potential leading slashes to avoid __virtual__ accidentally removing them 204 | let normalized_relative_path = normalized_path.strip_prefix('/').unwrap_or(&normalized_path); 205 | 206 | let mut segment_it = normalized_relative_path.split('/'); 207 | 208 | // `split` returns [""] if the path is empty; we need to remove it 209 | if normalized_relative_path.is_empty() { 210 | segment_it.next(); 211 | } 212 | 213 | let mut base_items: Vec<&str> = Vec::new(); 214 | 215 | let mut virtual_items: Option> = None; 216 | let mut internal_items: Option> = None; 217 | let mut zip_items: Option> = None; 218 | 219 | while let Some(segment) = segment_it.next() { 220 | if let Some(zip_segments) = &mut zip_items { 221 | zip_segments.push(segment); 222 | continue; 223 | } 224 | 225 | if segment == "__virtual__" && virtual_items.is_none() { 226 | let mut acc_segments = Vec::with_capacity(3); 227 | 228 | acc_segments.push(segment); 229 | 230 | // We just skip the arbitrary hash, it doesn't matter what it is 231 | if let Some(hash_segment) = segment_it.next() { 232 | acc_segments.push(hash_segment); 233 | } 234 | 235 | // We retrieve the depth 236 | if let Some(depth_segment) = segment_it.next() { 237 | let depth = depth_segment.parse::(); 238 | 239 | acc_segments.push(depth_segment); 240 | 241 | // We extract the backward segments from the base ones 242 | if let Ok(depth) = depth { 243 | let parent_segments = 244 | base_items.split_off(base_items.len().saturating_sub(depth)); 245 | 246 | acc_segments.splice(0..0, parent_segments); 247 | } 248 | } 249 | 250 | virtual_items = Some(acc_segments); 251 | internal_items = Some(vec![]); 252 | 253 | continue; 254 | } 255 | 256 | if segment.len() > 4 && segment.ends_with(".zip") { 257 | zip_items = Some(vec![]); 258 | } 259 | 260 | if let Some(virtual_segments) = &mut virtual_items { 261 | virtual_segments.push(segment); 262 | } 263 | 264 | if let Some(internal_segments) = &mut internal_items { 265 | internal_segments.push(segment); 266 | } else { 267 | base_items.push(segment); 268 | } 269 | } 270 | 271 | let mut base_path = base_items.join("/"); 272 | 273 | // Don't forget to add back the leading slash we removed earlier 274 | if normalized_relative_path != normalized_path { 275 | base_path.insert(0, '/'); 276 | } 277 | 278 | let virtual_info = match (virtual_items, internal_items) { 279 | (Some(virtual_segments), Some(internal_segments)) => { 280 | Some((virtual_segments.join("/"), internal_segments.join("/"))) 281 | } 282 | 283 | _ => None, 284 | }; 285 | 286 | if let Some(zip_segments) = zip_items { 287 | if !zip_segments.is_empty() { 288 | return Ok(VPath::Zip(ZipInfo { 289 | base_path, 290 | virtual_segments: virtual_info, 291 | zip_path: zip_segments.join("/"), 292 | })); 293 | } 294 | } 295 | 296 | if let Some(virtual_info) = virtual_info { 297 | return Ok(VPath::Virtual(VirtualInfo { base_path, virtual_segments: virtual_info })); 298 | } 299 | 300 | Ok(VPath::Native(PathBuf::from(base_path))) 301 | } 302 | 303 | #[cfg(test)] 304 | mod tests { 305 | use rstest::rstest; 306 | use std::path::PathBuf; 307 | 308 | use crate::util; 309 | 310 | use super::*; 311 | 312 | #[test] 313 | fn test_zip_type_api() { 314 | let zip = open_zip_via_read(PathBuf::from( 315 | "data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip", 316 | )) 317 | .unwrap(); 318 | 319 | assert_eq!(zip.file_type("node_modules").unwrap(), FileType::Directory); 320 | assert_eq!(zip.file_type("node_modules/").unwrap(), FileType::Directory); 321 | } 322 | 323 | #[test] 324 | #[should_panic(expected = "Kind(NotFound)")] 325 | fn test_zip_type_api_not_exist_dir_with_slash() { 326 | let zip = open_zip_via_read(PathBuf::from( 327 | "data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip", 328 | )) 329 | .unwrap(); 330 | 331 | zip.file_type("not_exists/").unwrap(); 332 | } 333 | 334 | #[test] 335 | #[should_panic(expected = "Kind(NotFound)")] 336 | fn test_zip_type_api_not_exist_dir_without_slash() { 337 | let zip = open_zip_via_read(PathBuf::from( 338 | "data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip", 339 | )) 340 | .unwrap(); 341 | 342 | zip.file_type("not_exists").unwrap(); 343 | } 344 | 345 | #[test] 346 | fn test_zip_list() { 347 | let zip = open_zip_via_read(PathBuf::from( 348 | "data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip", 349 | )) 350 | .unwrap(); 351 | 352 | let mut dirs: Vec<&String> = zip.dirs.iter().collect(); 353 | let mut files: Vec<&String> = zip.files.keys().collect(); 354 | 355 | dirs.sort(); 356 | files.sort(); 357 | 358 | assert_eq!( 359 | dirs, 360 | vec![ 361 | "node_modules/", 362 | "node_modules/@babel/", 363 | "node_modules/@babel/plugin-syntax-dynamic-import/", 364 | "node_modules/@babel/plugin-syntax-dynamic-import/lib/", 365 | ] 366 | ); 367 | 368 | assert_eq!( 369 | files, 370 | vec![ 371 | "node_modules/@babel/plugin-syntax-dynamic-import/LICENSE", 372 | "node_modules/@babel/plugin-syntax-dynamic-import/README.md", 373 | "node_modules/@babel/plugin-syntax-dynamic-import/lib/index.js", 374 | "node_modules/@babel/plugin-syntax-dynamic-import/package.json", 375 | ] 376 | ); 377 | } 378 | 379 | #[test] 380 | fn test_zip_read() { 381 | let zip = open_zip_via_read(PathBuf::from( 382 | "data/@babel-plugin-syntax-dynamic-import-npm-7.8.3-fb9ff5634a-8.zip", 383 | )) 384 | .unwrap(); 385 | 386 | let res = zip 387 | .read_to_string("node_modules/@babel/plugin-syntax-dynamic-import/package.json") 388 | .unwrap(); 389 | 390 | assert_eq!( 391 | res, 392 | "{\n \"name\": \"@babel/plugin-syntax-dynamic-import\",\n \"version\": \"7.8.3\",\n \"description\": \"Allow parsing of import()\",\n \"repository\": \"https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-dynamic-import\",\n \"license\": \"MIT\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"main\": \"lib/index.js\",\n \"keywords\": [\n \"babel-plugin\"\n ],\n \"dependencies\": {\n \"@babel/helper-plugin-utils\": \"^7.8.0\"\n },\n \"peerDependencies\": {\n \"@babel/core\": \"^7.0.0-0\"\n },\n \"devDependencies\": {\n \"@babel/core\": \"^7.8.0\"\n }\n}\n" 393 | ); 394 | } 395 | 396 | #[rstest] 397 | #[case(".zip", None)] 398 | #[case("foo", None)] 399 | #[case("foo.zip", None)] 400 | #[case("foo.zip/bar", Some(VPath::Zip(ZipInfo { 401 | base_path: "foo.zip".into(), 402 | virtual_segments: None, 403 | zip_path: "bar".into(), 404 | })))] 405 | #[case("foo.zip/bar/baz", Some(VPath::Zip(ZipInfo { 406 | base_path: "foo.zip".into(), 407 | virtual_segments: None, 408 | zip_path: "bar/baz".into(), 409 | })))] 410 | #[case("/a/b/c/foo.zip", None)] 411 | #[case("./a/b/c/foo.zip", None)] 412 | #[case("./a/b/__virtual__/foo-abcdef/0/c/d", Some(VPath::Virtual(VirtualInfo { 413 | base_path: "a/b".into(), 414 | virtual_segments: ("__virtual__/foo-abcdef/0/c/d".into(), "c/d".into()), 415 | })))] 416 | #[case("./a/b/__virtual__/foo-abcdef/1/c/d", Some(VPath::Virtual(VirtualInfo { 417 | base_path: "a".into(), 418 | virtual_segments: ("b/__virtual__/foo-abcdef/1/c/d".into(), "c/d".into()), 419 | })))] 420 | #[case("./a/b/__virtual__/foo-abcdef/0/c/foo.zip/bar", Some(VPath::Zip(ZipInfo { 421 | base_path: "a/b".into(), 422 | virtual_segments: Some(("__virtual__/foo-abcdef/0/c/foo.zip".into(), "c/foo.zip".into())), 423 | zip_path: "bar".into(), 424 | })))] 425 | #[case("./a/b/__virtual__/foo-abcdef/1/c/foo.zip/bar", Some(VPath::Zip(ZipInfo { 426 | base_path: "a".into(), 427 | virtual_segments: Some(("b/__virtual__/foo-abcdef/1/c/foo.zip".into(), "c/foo.zip".into())), 428 | zip_path: "bar".into(), 429 | })))] 430 | #[case("/a/b/__virtual__/foo-abcdef/1/c/foo.zip/bar", Some(VPath::Zip(ZipInfo { 431 | base_path: "/a".into(), 432 | virtual_segments: Some(("b/__virtual__/foo-abcdef/1/c/foo.zip".into(), "c/foo.zip".into())), 433 | zip_path: "bar".into(), 434 | })))] 435 | #[case("/a/b/__virtual__/foo-abcdef/2/c/foo.zip/bar", Some(VPath::Zip(ZipInfo { 436 | base_path: "/".into(), 437 | virtual_segments: Some(("a/b/__virtual__/foo-abcdef/2/c/foo.zip".into(), "c/foo.zip".into())), 438 | zip_path: "bar".into(), 439 | })))] 440 | #[case("/__virtual__/foo-abcdef/2/c/foo.zip/bar", Some(VPath::Zip(ZipInfo { 441 | base_path: "/".into(), 442 | virtual_segments: Some(("__virtual__/foo-abcdef/2/c/foo.zip".into(), "c/foo.zip".into())), 443 | zip_path: "bar".into(), 444 | })))] 445 | #[case("./a/b/c/.zip", None)] 446 | #[case("./a/b/c/foo.zipp", None)] 447 | #[case("./a/b/c/foo.zip/bar/baz/qux.zip", Some(VPath::Zip(ZipInfo { 448 | base_path: "a/b/c/foo.zip".into(), 449 | virtual_segments: None, 450 | zip_path: "bar/baz/qux.zip".into(), 451 | })))] 452 | #[case("./a/b/c/foo.zip-bar.zip", None)] 453 | #[case("./a/b/c/foo.zip-bar.zip/bar/baz/qux.zip", Some(VPath::Zip(ZipInfo { 454 | base_path: "a/b/c/foo.zip-bar.zip".into(), 455 | virtual_segments: None, 456 | zip_path: "bar/baz/qux.zip".into(), 457 | })))] 458 | #[case("./a/b/c/foo.zip-bar/foo.zip-bar/foo.zip-bar.zip/d", Some(VPath::Zip(ZipInfo { 459 | base_path: "a/b/c/foo.zip-bar/foo.zip-bar/foo.zip-bar.zip".into(), 460 | virtual_segments: None, 461 | zip_path: "d".into(), 462 | })))] 463 | fn test_path_to_pnp(#[case] input: &str, #[case] expected: Option) { 464 | let expectation: VPath = match &expected { 465 | Some(p) => p.clone(), 466 | None => VPath::Native(PathBuf::from(util::normalize_path(input))), 467 | }; 468 | 469 | match vpath(&PathBuf::from(input)) { 470 | Ok(res) => { 471 | assert_eq!(res, expectation, "input='{input:?}'"); 472 | } 473 | Err(err) => { 474 | panic!("{input:?}: {err}"); 475 | } 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod fs; 2 | 3 | mod builtins; 4 | mod error; 5 | mod manifest; 6 | mod util; 7 | mod zip; 8 | 9 | use std::{ 10 | collections::hash_map::Entry, 11 | path::{Path, PathBuf}, 12 | sync::OnceLock, 13 | }; 14 | 15 | use fancy_regex::Regex; 16 | 17 | pub use crate::{ 18 | error::{ 19 | BadSpecifier, Error, FailedManifestHydration, MissingDependency, MissingPeerDependency, 20 | UndeclaredDependency, 21 | }, 22 | manifest::{Manifest, PackageDependency, PackageInformation, PackageLocator}, 23 | }; 24 | 25 | #[derive(Debug)] 26 | pub enum Resolution { 27 | Resolved(PathBuf, Option), 28 | Skipped, 29 | } 30 | 31 | pub struct ResolutionHost { 32 | #[allow(clippy::type_complexity)] 33 | pub find_pnp_manifest: Box Result, Error>>, 34 | } 35 | 36 | impl Default for ResolutionHost { 37 | fn default() -> ResolutionHost { 38 | ResolutionHost { find_pnp_manifest: Box::new(find_pnp_manifest) } 39 | } 40 | } 41 | 42 | #[derive(Default)] 43 | pub struct ResolutionConfig { 44 | pub host: ResolutionHost, 45 | } 46 | 47 | fn parse_scoped_package_name(specifier: &str) -> Option<(String, Option)> { 48 | let mut segments = specifier.splitn(3, '/'); 49 | 50 | let scope = segments.next()?; 51 | 52 | let name = segments.next()?; 53 | 54 | let package_name = specifier[..scope.len() + name.len() + 1].to_string(); 55 | 56 | let subpath = segments.next().map(|v| v.to_string()); 57 | 58 | Some((package_name, subpath)) 59 | } 60 | 61 | fn parse_global_package_name(specifier: &str) -> Option<(String, Option)> { 62 | let mut segments = specifier.splitn(2, '/'); 63 | 64 | let name = segments.next()?; 65 | 66 | let package_name = name.to_string(); 67 | 68 | let subpath = segments.next().map(|v| v.to_string()); 69 | 70 | Some((package_name, subpath)) 71 | } 72 | 73 | pub fn parse_bare_identifier(specifier: &str) -> Result<(String, Option), Error> { 74 | let name = match specifier.starts_with("@") { 75 | true => parse_scoped_package_name(specifier), 76 | false => parse_global_package_name(specifier), 77 | }; 78 | 79 | name.ok_or_else(|| { 80 | Error::BadSpecifier(Box::new(BadSpecifier { 81 | message: String::from("Invalid specifier"), 82 | specifier: specifier.to_string(), 83 | })) 84 | }) 85 | } 86 | 87 | pub fn find_closest_pnp_manifest_path(path: &Path) -> Option { 88 | for p in path.ancestors() { 89 | let pnp_path = p.join(".pnp.cjs"); 90 | if pnp_path.exists() { 91 | return Some(pnp_path); 92 | } 93 | } 94 | None 95 | } 96 | 97 | pub fn load_pnp_manifest>(p: P) -> Result { 98 | let manifest_content = std::fs::read_to_string(p.as_ref()).map_err(|err| { 99 | Error::FailedManifestHydration(Box::new(FailedManifestHydration { 100 | message: format!( 101 | "We failed to read the content of the manifest.\n\nOriginal error: {err}" 102 | ), 103 | manifest_path: p.as_ref().to_path_buf(), 104 | })) 105 | })?; 106 | 107 | static RE: OnceLock = OnceLock::new(); 108 | 109 | let manifest_match = 110 | RE.get_or_init(|| { 111 | Regex::new( 112 | "(const[ \\r\\n]+RAW_RUNTIME_STATE[ \\r\\n]*=[ \\r\\n]*|hydrateRuntimeState\\(JSON\\.parse\\()'" 113 | ) 114 | .unwrap() 115 | }) 116 | .find(&manifest_content) 117 | .unwrap_or_default() 118 | .ok_or_else(|| Error::FailedManifestHydration(Box::new(FailedManifestHydration { 119 | message: String::from("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?"), 120 | manifest_path: p.as_ref().to_path_buf(), 121 | })))?; 122 | 123 | let iter = manifest_content.chars().skip(manifest_match.end()); 124 | let mut json_string = String::default(); 125 | let mut escaped = false; 126 | 127 | for c in iter { 128 | match c { 129 | '\'' if !escaped => { 130 | break; 131 | } 132 | '\\' if !escaped => { 133 | escaped = true; 134 | } 135 | _ => { 136 | escaped = false; 137 | json_string.push(c); 138 | } 139 | } 140 | } 141 | 142 | let mut manifest: Manifest = serde_json::from_str(&json_string.to_owned()) 143 | .map_err(|err| Error::FailedManifestHydration(Box::new(FailedManifestHydration { 144 | message: format!("We failed to parse the PnP data payload as proper JSON; Did you manually edit the file?\n\nOriginal error: {err}"), 145 | manifest_path: p.as_ref().to_path_buf(), 146 | })))?; 147 | 148 | init_pnp_manifest(&mut manifest, p.as_ref()); 149 | 150 | Ok(manifest) 151 | } 152 | 153 | pub fn init_pnp_manifest>(manifest: &mut Manifest, p: P) { 154 | manifest.manifest_path = p.as_ref().to_path_buf(); 155 | 156 | manifest.manifest_dir = p.as_ref().parent().expect("Should have a parent directory").to_owned(); 157 | 158 | for (name, ranges) in manifest.package_registry_data.iter_mut() { 159 | for (reference, info) in ranges.iter_mut() { 160 | let package_location = manifest.manifest_dir.join(info.package_location.clone()); 161 | 162 | let normalized_location = util::normalize_path(package_location.to_string_lossy()); 163 | 164 | info.package_location = PathBuf::from(normalized_location); 165 | 166 | if !info.discard_from_lookup { 167 | manifest.location_trie.insert( 168 | &info.package_location, 169 | PackageLocator { name: name.clone(), reference: reference.clone() }, 170 | ); 171 | } 172 | } 173 | } 174 | 175 | let top_level_pkg = manifest 176 | .package_registry_data 177 | .get("") 178 | .expect("Assertion failed: Should have a top-level name key") 179 | .get("") 180 | .expect("Assertion failed: Should have a top-level range key"); 181 | 182 | for (name, dependency) in &top_level_pkg.package_dependencies { 183 | if let Entry::Vacant(entry) = manifest.fallback_pool.entry(name.clone()) { 184 | entry.insert(dependency.clone()); 185 | } 186 | } 187 | } 188 | 189 | pub fn find_pnp_manifest(parent: &Path) -> Result, Error> { 190 | find_closest_pnp_manifest_path(parent).map_or(Ok(None), |p| Ok(Some(load_pnp_manifest(p)?))) 191 | } 192 | 193 | pub fn is_dependency_tree_root<'a>(manifest: &'a Manifest, locator: &'a PackageLocator) -> bool { 194 | manifest.dependency_tree_roots.contains(locator) 195 | } 196 | 197 | pub fn find_locator<'a, P: AsRef>( 198 | manifest: &'a Manifest, 199 | path: &P, 200 | ) -> Option<&'a PackageLocator> { 201 | let rel_path = pathdiff::diff_paths(path, &manifest.manifest_dir) 202 | .expect("Assertion failed: Provided path should be absolute"); 203 | 204 | if let Some(regex) = &manifest.ignore_pattern_data { 205 | if regex.0.is_match(&util::normalize_path(rel_path.to_string_lossy())).unwrap() { 206 | return None; 207 | } 208 | } 209 | 210 | manifest.location_trie.get_ancestor_value(&path) 211 | } 212 | 213 | pub fn get_package<'a>( 214 | manifest: &'a Manifest, 215 | locator: &PackageLocator, 216 | ) -> Result<&'a PackageInformation, Error> { 217 | let references = manifest 218 | .package_registry_data 219 | .get(&locator.name) 220 | .expect("Should have an entry in the package registry"); 221 | 222 | let info = 223 | references.get(&locator.reference).expect("Should have an entry in the package registry"); 224 | 225 | Ok(info) 226 | } 227 | 228 | pub fn is_excluded_from_fallback(manifest: &Manifest, locator: &PackageLocator) -> bool { 229 | if let Some(references) = manifest.fallback_exclusion_list.get(&locator.name) { 230 | references.contains(&locator.reference) 231 | } else { 232 | false 233 | } 234 | } 235 | 236 | pub fn find_broken_peer_dependencies( 237 | _dependency: &str, 238 | _initial_package: &PackageLocator, 239 | ) -> Vec { 240 | [].to_vec() 241 | } 242 | 243 | pub fn resolve_to_unqualified_via_manifest>( 244 | manifest: &Manifest, 245 | specifier: &str, 246 | parent: P, 247 | ) -> Result { 248 | let (ident, module_path) = parse_bare_identifier(specifier)?; 249 | 250 | if let Some(parent_locator) = find_locator(manifest, &parent) { 251 | let parent_pkg = get_package(manifest, parent_locator)?; 252 | 253 | let mut reference_or_alias: Option = None; 254 | let mut is_set = false; 255 | 256 | if !is_set { 257 | if let Some(Some(binding)) = parent_pkg.package_dependencies.get(&ident) { 258 | reference_or_alias = Some(binding.clone()); 259 | is_set = true; 260 | } 261 | } 262 | 263 | if !is_set 264 | && manifest.enable_top_level_fallback 265 | && !is_excluded_from_fallback(manifest, parent_locator) 266 | { 267 | if let Some(fallback_resolution) = manifest.fallback_pool.get(&ident) { 268 | reference_or_alias = fallback_resolution.clone(); 269 | is_set = true; 270 | } 271 | } 272 | 273 | if !is_set { 274 | let message = if builtins::is_nodejs_builtin(specifier) { 275 | if is_dependency_tree_root(manifest, parent_locator) { 276 | format!( 277 | "Your application tried to access {dependency_name}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since {dependency_name} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: ${issuer_path}", 278 | dependency_name = &ident, 279 | via = if ident != specifier { 280 | format!(" (via \"{}\")", &specifier) 281 | } else { 282 | String::from("") 283 | }, 284 | issuer_path = parent.as_ref().to_string_lossy(), 285 | ) 286 | } else { 287 | format!( 288 | "${issuer_locator_name} tried to access {dependency_name}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since {dependency_name} isn't otherwise declared in ${issuer_locator_name}'s dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: ${issuer_path}", 289 | issuer_locator_name = &parent_locator.name, 290 | dependency_name = &ident, 291 | via = if ident != specifier { 292 | format!(" (via \"{}\")", &specifier) 293 | } else { 294 | String::from("") 295 | }, 296 | issuer_path = parent.as_ref().to_string_lossy(), 297 | ) 298 | } 299 | } else if is_dependency_tree_root(manifest, parent_locator) { 300 | format!( 301 | "Your application tried to access {dependency_name}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_path}", 302 | dependency_name = &ident, 303 | via = if ident != specifier { 304 | format!(" (via \"{}\")", &specifier) 305 | } else { 306 | String::from("") 307 | }, 308 | issuer_path = parent.as_ref().to_string_lossy(), 309 | ) 310 | } else { 311 | format!( 312 | "{issuer_locator_name} tried to access {dependency_name}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})", 313 | issuer_locator_name = &parent_locator.name, 314 | issuer_locator_reference = &parent_locator.reference, 315 | dependency_name = &ident, 316 | via = if ident != specifier { 317 | format!(" (via \"{}\")", &specifier) 318 | } else { 319 | String::from("") 320 | }, 321 | issuer_path = parent.as_ref().to_string_lossy(), 322 | ) 323 | }; 324 | 325 | return Err(Error::UndeclaredDependency(Box::new(UndeclaredDependency { 326 | message, 327 | request: specifier.to_string(), 328 | dependency_name: ident, 329 | issuer_locator: parent_locator.clone(), 330 | issuer_path: parent.as_ref().to_path_buf(), 331 | }))); 332 | } 333 | 334 | if let Some(resolution) = reference_or_alias { 335 | let dependency_pkg = match resolution { 336 | PackageDependency::Reference(reference) => { 337 | get_package(manifest, &PackageLocator { name: ident, reference }) 338 | } 339 | PackageDependency::Alias(name, reference) => { 340 | get_package(manifest, &PackageLocator { name, reference }) 341 | } 342 | }?; 343 | 344 | Ok(Resolution::Resolved(dependency_pkg.package_location.clone(), module_path)) 345 | } else { 346 | let broken_ancestors = find_broken_peer_dependencies(specifier, parent_locator); 347 | 348 | let message = if is_dependency_tree_root(manifest, parent_locator) { 349 | format!( 350 | "Your application tried to access {dependency_name} (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_path}", 351 | dependency_name = &ident, 352 | via = if ident != specifier { 353 | format!(" (via \"{}\")", &specifier) 354 | } else { 355 | String::from("") 356 | }, 357 | issuer_path = parent.as_ref().to_string_lossy(), 358 | ) 359 | } else if !broken_ancestors.is_empty() 360 | && broken_ancestors.iter().all(|locator| is_dependency_tree_root(manifest, locator)) 361 | { 362 | format!( 363 | "{issuer_locator_name} tried to access {dependency_name} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})", 364 | issuer_locator_name = &parent_locator.name, 365 | issuer_locator_reference = &parent_locator.reference, 366 | dependency_name = &ident, 367 | via = if ident != specifier { 368 | format!(" (via \"{}\")", &specifier) 369 | } else { 370 | String::from("") 371 | }, 372 | issuer_path = parent.as_ref().to_string_lossy(), 373 | ) 374 | } else { 375 | format!( 376 | "{issuer_locator_name} tried to access {dependency_name} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound.\n\nRequired package: {dependency_name}{via}\nRequired by: {issuer_locator_name}@{issuer_locator_reference} (via {issuer_path})", 377 | issuer_locator_name = &parent_locator.name, 378 | issuer_locator_reference = &parent_locator.reference, 379 | dependency_name = &ident, 380 | via = if ident != specifier { 381 | format!(" (via \"{}\")", &specifier) 382 | } else { 383 | String::from("") 384 | }, 385 | issuer_path = parent.as_ref().to_string_lossy(), 386 | ) 387 | }; 388 | 389 | Err(Error::MissingPeerDependency(Box::new(MissingPeerDependency { 390 | message, 391 | request: specifier.to_string(), 392 | dependency_name: ident, 393 | issuer_locator: parent_locator.clone(), 394 | issuer_path: parent.as_ref().to_path_buf(), 395 | broken_ancestors: [].to_vec(), 396 | }))) 397 | } 398 | } else { 399 | Ok(Resolution::Skipped) 400 | } 401 | } 402 | 403 | pub fn resolve_to_unqualified>( 404 | specifier: &str, 405 | parent: P, 406 | config: &ResolutionConfig, 407 | ) -> Result { 408 | if let Some(manifest) = (config.host.find_pnp_manifest)(parent.as_ref())? { 409 | resolve_to_unqualified_via_manifest(&manifest, specifier, &parent) 410 | } else { 411 | Ok(Resolution::Skipped) 412 | } 413 | } 414 | 415 | #[cfg(test)] 416 | mod lib_tests; 417 | -------------------------------------------------------------------------------- /src/lib_tests.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::{Manifest, Resolution}; 4 | 5 | #[derive(Deserialize)] 6 | struct Test { 7 | it: String, 8 | imported: String, 9 | importer: String, 10 | expected: String, 11 | } 12 | 13 | #[derive(Deserialize)] 14 | struct TestSuite { 15 | manifest: Manifest, 16 | tests: Vec, 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use std::{fs, path::PathBuf}; 22 | 23 | use super::*; 24 | use crate::{ 25 | ResolutionConfig, ResolutionHost, init_pnp_manifest, load_pnp_manifest, 26 | parse_bare_identifier, resolve_to_unqualified, resolve_to_unqualified_via_manifest, 27 | }; 28 | 29 | #[test] 30 | fn example() { 31 | let manifest = load_pnp_manifest("data/pnp-yarn-v3.cjs").unwrap(); 32 | 33 | let host = 34 | ResolutionHost { find_pnp_manifest: Box::new(move |_| Ok(Some(manifest.clone()))) }; 35 | 36 | let config = ResolutionConfig { host }; 37 | 38 | let resolution = resolve_to_unqualified( 39 | "lodash/cloneDeep", 40 | std::path::PathBuf::from("/path/to/file"), 41 | &config, 42 | ); 43 | 44 | match resolution { 45 | Ok(Resolution::Resolved(_path, _subpath)) => { 46 | // path = "/path/to/lodash.zip" 47 | // subpath = "cloneDeep" 48 | } 49 | Ok(Resolution::Skipped) => { 50 | // This is returned when the PnP resolver decides that it shouldn't 51 | // handle the resolution for this particular specifier. In that case, 52 | // the specifier should be forwarded to the default resolver. 53 | } 54 | Err(_err) => { 55 | // An error happened during the resolution. Falling back to the default 56 | // resolver isn't recommended. 57 | } 58 | }; 59 | } 60 | 61 | #[test] 62 | fn test_load_pnp_manifest() { 63 | load_pnp_manifest("data/pnp-yarn-v3.cjs") 64 | .expect("Assertion failed: Expected to load the .pnp.cjs file generated by Yarn 3"); 65 | 66 | load_pnp_manifest("data/pnp-yarn-v4.cjs") 67 | .expect("Assertion failed: Expected to load the .pnp.cjs file generated by Yarn 4"); 68 | } 69 | 70 | #[test] 71 | fn test_resolve_unqualified() { 72 | let expectations_path = std::env::current_dir() 73 | .expect("Assertion failed: Expected a valid current working directory") 74 | .join("data/test-expectations.json"); 75 | 76 | let manifest_content = fs::read_to_string(&expectations_path) 77 | .expect("Assertion failed: Expected the expectations to be found"); 78 | 79 | let mut test_suites: Vec = serde_json::from_str(&manifest_content) 80 | .expect("Assertion failed: Expected the expectations to be loaded"); 81 | 82 | for test_suite in test_suites.iter_mut() { 83 | let manifest = &mut test_suite.manifest; 84 | init_pnp_manifest(manifest, PathBuf::from("/path/to/project/.pnp.cjs")); 85 | 86 | for test in test_suite.tests.iter() { 87 | let specifier = &test.imported; 88 | let parent = &PathBuf::from(&test.importer).join("fooo"); 89 | 90 | let manifest_copy = manifest.clone(); 91 | 92 | let host = ResolutionHost { 93 | find_pnp_manifest: Box::new(move |_| Ok(Some(manifest_copy.clone()))), 94 | }; 95 | 96 | let config = ResolutionConfig { host }; 97 | 98 | let resolution = resolve_to_unqualified(specifier, parent, &config); 99 | 100 | match resolution { 101 | Ok(Resolution::Resolved(path, _subpath)) => { 102 | assert_eq!(path.to_string_lossy(), test.expected, "{}", test.it); 103 | } 104 | Ok(Resolution::Skipped) => { 105 | assert_eq!(specifier, &test.expected, "{}", test.it); 106 | } 107 | Err(err) => { 108 | assert_eq!(test.expected, "error!", "{}: {err}", test.it); 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | #[test] 116 | fn test_edge_case_one_pkg_cached_and_unplugged() { 117 | let manifest = { 118 | let manifest_json_path = 119 | std::env::current_dir().unwrap().join("./data/edge_case_manifest_state.json"); 120 | let manifest_content = fs::read_to_string(&manifest_json_path).unwrap(); 121 | let mut manifest = serde_json::from_str::(&manifest_content).unwrap(); 122 | init_pnp_manifest(&mut manifest, manifest_json_path); 123 | manifest 124 | }; 125 | 126 | let issuer = std::env::current_dir().unwrap(). 127 | join("data/.yarn/unplugged/@carbon-icons-react-virtual-379302d360/node_modules/@carbon/icons-react/es/"); 128 | 129 | let resolution = 130 | resolve_to_unqualified_via_manifest(&manifest, "@carbon/icon-helpers", &issuer) 131 | .unwrap(); 132 | 133 | match resolution { 134 | Resolution::Resolved(resolved, _) => { 135 | assert!(resolved.ends_with(".yarn/unplugged/@carbon-icon-helpers-npm-10.54.0-a58f8b7b6c/node_modules/@carbon/icon-helpers")) 136 | } 137 | _ => { 138 | panic!("Unexpected resolve failed"); 139 | } 140 | } 141 | } 142 | 143 | #[test] 144 | fn test_parse_single_package_name() { 145 | let parsed = parse_bare_identifier("pkg"); 146 | assert_eq!(parsed, Ok(("pkg".to_string(), None))); 147 | } 148 | 149 | #[test] 150 | fn test_parse_scoped_package_name() { 151 | let parsed = parse_bare_identifier("@scope/pkg"); 152 | assert_eq!(parsed, Ok(("@scope/pkg".to_string(), None))); 153 | } 154 | 155 | #[test] 156 | fn test_parse_package_name_with_long_subpath() { 157 | let parsed = parse_bare_identifier("pkg/a/b/c/index.js"); 158 | assert_eq!(parsed, Ok(("pkg".to_string(), Some("a/b/c/index.js".to_string())))); 159 | } 160 | 161 | #[test] 162 | fn test_parse_scoped_package_with_long_subpath() { 163 | let parsed = parse_bare_identifier("@scope/pkg/a/b/c/index.js"); 164 | assert_eq!(parsed, Ok(("@scope/pkg".to_string(), Some("a/b/c/index.js".to_string())))); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use pnp::{Resolution, ResolutionConfig}; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | let mut args = std::env::args(); 6 | 7 | // Skip the program name 8 | args.next(); 9 | 10 | let specifier = args.next().expect("A specifier must be provided"); 11 | 12 | let parent = args.next().map(PathBuf::from).expect("A parent url must be provided"); 13 | 14 | println!("specifier = {specifier}"); 15 | println!("parent = {parent:?}"); 16 | 17 | let resolution = pnp::resolve_to_unqualified( 18 | &specifier, 19 | &parent, 20 | &ResolutionConfig { ..Default::default() }, 21 | ); 22 | 23 | match resolution { 24 | Ok(res) => match res { 25 | Resolution::Resolved(p, subpath) => { 26 | println!("result = Package ({p:?}, {subpath:?})"); 27 | } 28 | Resolution::Skipped => { 29 | println!("result = Skipped"); 30 | } 31 | }, 32 | Err(err) => { 33 | println!("{err}"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use rustc_hash::{FxHashMap, FxHashSet}; 4 | use serde::{Deserialize, de::Deserializer}; 5 | 6 | use crate::util::{RegexDef, Trie}; 7 | 8 | #[derive(Clone, Debug, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Manifest { 11 | #[serde(skip_deserializing)] 12 | pub manifest_dir: PathBuf, 13 | 14 | #[serde(skip_deserializing)] 15 | pub manifest_path: PathBuf, 16 | 17 | #[serde(skip_deserializing)] 18 | pub location_trie: Trie, 19 | 20 | pub enable_top_level_fallback: bool, 21 | pub ignore_pattern_data: Option, 22 | 23 | // dependencyTreeRoots: [{ 24 | // "name": "@app/monorepo", 25 | // "workspace:." 26 | // }] 27 | pub dependency_tree_roots: FxHashSet, 28 | 29 | // fallbackPool: [[ 30 | // "@app/monorepo", 31 | // "workspace:.", 32 | // ]] 33 | #[serde(deserialize_with = "deserialize_package_dependencies")] 34 | pub fallback_pool: FxHashMap>, 35 | 36 | // fallbackExclusionList: [[ 37 | // "@app/server", 38 | // ["workspace:sources/server"], 39 | // ]] 40 | #[serde(deserialize_with = "deserialize_fallback_exclusion_list")] 41 | pub fallback_exclusion_list: FxHashMap>, 42 | 43 | // packageRegistryData: [ 44 | // [null, [ 45 | // [null, { 46 | // ... 47 | // }] 48 | // }] 49 | // ] 50 | #[serde(deserialize_with = "deserialize_package_registry_data")] 51 | pub package_registry_data: FxHashMap>, 52 | } 53 | 54 | #[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Deserialize)] 55 | pub struct PackageLocator { 56 | pub name: String, 57 | pub reference: String, 58 | } 59 | 60 | #[derive(Clone, Debug, Deserialize)] 61 | #[serde(rename_all = "camelCase")] 62 | pub struct PackageInformation { 63 | pub package_location: PathBuf, 64 | 65 | #[serde(default)] 66 | pub discard_from_lookup: bool, 67 | 68 | #[serde(deserialize_with = "deserialize_package_dependencies")] 69 | pub package_dependencies: FxHashMap>, 70 | } 71 | 72 | #[derive(Clone, Debug, Deserialize)] 73 | #[serde(untagged)] 74 | pub enum PackageDependency { 75 | Reference(String), 76 | Alias(String, String), 77 | } 78 | 79 | fn deserialize_fallback_exclusion_list<'de, D>( 80 | deserializer: D, 81 | ) -> Result>, D::Error> 82 | where 83 | D: Deserializer<'de>, 84 | { 85 | #[derive(Debug, Deserialize)] 86 | struct Item(String, FxHashSet); 87 | 88 | let mut map = FxHashMap::default(); 89 | for item in Vec::::deserialize(deserializer)? { 90 | map.insert(item.0, item.1); 91 | } 92 | Ok(map) 93 | } 94 | 95 | fn deserialize_package_dependencies<'de, D>( 96 | deserializer: D, 97 | ) -> Result>, D::Error> 98 | where 99 | D: Deserializer<'de>, 100 | { 101 | #[derive(Debug, Deserialize)] 102 | struct Item(String, Option); 103 | 104 | let mut map = FxHashMap::default(); 105 | for item in Vec::::deserialize(deserializer)? { 106 | map.insert(item.0, item.1); 107 | } 108 | Ok(map) 109 | } 110 | 111 | fn deserialize_package_registry_data<'de, D>( 112 | deserializer: D, 113 | ) -> Result>, D::Error> 114 | where 115 | D: Deserializer<'de>, 116 | { 117 | #[derive(Debug, Deserialize)] 118 | struct Item(Option, Vec<(Option, PackageInformation)>); 119 | 120 | let mut map = FxHashMap::default(); 121 | for item in Vec::::deserialize(deserializer)? { 122 | let key = item.0.unwrap_or_else(|| "".to_string()); 123 | let value = FxHashMap::from_iter( 124 | item.1.into_iter().map(|(k, v)| (k.unwrap_or_else(|| "".to_string()), v)), 125 | ); 126 | map.insert(key, value); 127 | } 128 | Ok(map) 129 | } 130 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use fancy_regex::Regex; 2 | use serde::{Deserialize, Deserializer, de::Error}; 3 | use std::borrow::Cow; 4 | 5 | use path_slash::PathBufExt; 6 | use std::path::{Path, PathBuf}; 7 | 8 | #[derive(Debug, Default, Clone)] 9 | pub struct Trie { 10 | inner: radix_trie::Trie, 11 | } 12 | 13 | impl Trie { 14 | fn key>(&self, key: &P) -> String { 15 | let mut p = normalize_path(key.as_ref().to_string_lossy()); 16 | 17 | if !p.ends_with('/') { 18 | p.push('/'); 19 | } 20 | 21 | p 22 | } 23 | 24 | pub fn get_ancestor_value>(&self, key: &P) -> Option<&T> { 25 | self.inner.get_ancestor_value(&self.key(&key)).map(|t| &t.1) 26 | } 27 | 28 | pub fn insert>(&mut self, key: P, value: T) { 29 | let k = self.key(&key); 30 | let p = PathBuf::from(k.clone()); 31 | 32 | self.inner.insert(k, (p, value)).map(|t| t.1); 33 | } 34 | } 35 | 36 | pub fn normalize_path>(original: P) -> String { 37 | let original_str = original.as_ref(); 38 | 39 | let p = PathBuf::from(original_str); 40 | let mut str = clean_path::clean(p).to_slash_lossy().to_string(); 41 | 42 | if original_str.ends_with('/') && !str.ends_with('/') { 43 | str.push('/'); 44 | } 45 | 46 | str 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | 53 | #[test] 54 | fn test_normalize_path() { 55 | assert_eq!(normalize_path(""), "."); 56 | assert_eq!(normalize_path("/"), "/"); 57 | assert_eq!(normalize_path("foo"), "foo"); 58 | assert_eq!(normalize_path("foo/bar"), "foo/bar"); 59 | assert_eq!(normalize_path("foo//bar"), "foo/bar"); 60 | assert_eq!(normalize_path("foo/./bar"), "foo/bar"); 61 | assert_eq!(normalize_path("foo/../bar"), "bar"); 62 | assert_eq!(normalize_path("foo/bar/.."), "foo"); 63 | assert_eq!(normalize_path("foo/../../bar"), "../bar"); 64 | assert_eq!(normalize_path("../foo/../../bar"), "../../bar"); 65 | assert_eq!(normalize_path("./foo"), "foo"); 66 | assert_eq!(normalize_path("../foo"), "../foo"); 67 | assert_eq!(normalize_path("/foo/bar"), "/foo/bar"); 68 | assert_eq!(normalize_path("/foo/bar/"), "/foo/bar/"); 69 | } 70 | } 71 | 72 | fn strip_slash_escape(str: &str) -> String { 73 | let mut res = String::default(); 74 | res.reserve_exact(str.len()); 75 | 76 | let mut iter = str.chars().peekable(); 77 | let mut escaped = false; 78 | 79 | while let Some(c) = iter.next() { 80 | if !escaped && c == '\\' { 81 | if iter.peek() == Some(&'/') { 82 | continue; 83 | } 84 | 85 | escaped = true; 86 | } else { 87 | escaped = false; 88 | } 89 | 90 | res.push(c); 91 | } 92 | 93 | res 94 | } 95 | 96 | #[derive(Clone, Debug)] 97 | pub struct RegexDef(pub Regex); 98 | 99 | impl<'de> Deserialize<'de> for RegexDef { 100 | fn deserialize(d: D) -> Result 101 | where 102 | D: Deserializer<'de>, 103 | { 104 | let s = >::deserialize(d)?; 105 | 106 | match strip_slash_escape(s.as_ref()).parse() { 107 | Ok(regex) => Ok(RegexDef(regex)), 108 | Err(err) => Err(D::Error::custom(err)), 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/zip.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | io::{Cursor, Read}, 4 | }; 5 | 6 | use byteorder::{LittleEndian, ReadBytesExt}; 7 | use rustc_hash::{FxHashMap, FxHashSet}; 8 | 9 | use crate::fs::FileType; 10 | use crate::util; 11 | 12 | #[derive(Debug)] 13 | pub enum Compression { 14 | Uncompressed, 15 | Deflate, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Entry { 20 | pub compression: Compression, 21 | pub offset: usize, 22 | pub size: usize, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub struct Zip 27 | where 28 | T: AsRef<[u8]>, 29 | { 30 | storage: T, 31 | pub files: FxHashMap, 32 | pub dirs: FxHashSet, 33 | } 34 | 35 | impl Zip 36 | where 37 | T: AsRef<[u8]>, 38 | { 39 | pub fn new(storage: T) -> Result, Box> { 40 | let mut zip = Zip { storage, files: Default::default(), dirs: Default::default() }; 41 | 42 | for (name, maybe_entry) in list_zip_entries(zip.storage.as_ref())? { 43 | let name = util::normalize_path(name); 44 | let segments: Vec<&str> = name.split('/').collect(); 45 | 46 | for t in 1..segments.len() - 1 { 47 | let dir = segments[0..t].to_vec().join("/"); 48 | zip.dirs.insert(dir + "/"); 49 | } 50 | 51 | if let Some(entry) = maybe_entry { 52 | zip.files.insert(name, entry); 53 | } else { 54 | zip.dirs.insert(name); 55 | } 56 | } 57 | 58 | Ok(zip) 59 | } 60 | 61 | pub fn file_type(&self, p: &str) -> Result { 62 | if self.is_dir(p) { 63 | Ok(FileType::Directory) 64 | } else if self.files.contains_key(p) { 65 | Ok(FileType::File) 66 | } else { 67 | Err(std::io::Error::from(std::io::ErrorKind::NotFound)) 68 | } 69 | } 70 | 71 | fn is_dir(&self, p: &str) -> bool { 72 | if p.ends_with('/') { self.dirs.contains(p) } else { self.dirs.contains(&format!("{p}/")) } 73 | } 74 | 75 | pub fn read(&self, p: &str) -> Result, std::io::Error> { 76 | let entry = self.files.get(p).ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?; 77 | 78 | let data = self.storage.as_ref(); 79 | let slice = &data[entry.offset..entry.offset + entry.size]; 80 | 81 | match entry.compression { 82 | Compression::Deflate => { 83 | let decompressed_data = miniz_oxide::inflate::decompress_to_vec(slice) 84 | .map_err(|_| std::io::Error::other("Error during decompression"))?; 85 | 86 | Ok(decompressed_data) 87 | } 88 | 89 | Compression::Uncompressed => Ok(slice.to_vec()), 90 | } 91 | } 92 | 93 | pub fn read_to_string(&self, p: &str) -> Result { 94 | let data = self.read(p)?; 95 | 96 | Ok(io_bytes_to_str(data.as_slice())?.to_string()) 97 | } 98 | } 99 | 100 | fn io_bytes_to_str(vec: &[u8]) -> Result<&str, std::io::Error> { 101 | std::str::from_utf8(vec).map_err(|_| make_io_utf8_error()) 102 | } 103 | 104 | fn make_io_utf8_error() -> std::io::Error { 105 | std::io::Error::new(std::io::ErrorKind::InvalidData, "File did not contain valid UTF-8") 106 | } 107 | 108 | pub fn list_zip_entries(data: &[u8]) -> Result>, Box> { 109 | let mut zip_entries = FxHashMap::default(); 110 | let mut cursor = Cursor::new(data); 111 | 112 | let central_directory_offset = find_central_directory_offset(&mut cursor)?; 113 | cursor.set_position(central_directory_offset); 114 | 115 | while let Some(entry) = read_central_file_header(&mut cursor)? { 116 | let entry_name = entry.0; 117 | let entry_data = entry.1; 118 | 119 | zip_entries.insert(entry_name, entry_data); 120 | } 121 | 122 | Ok(zip_entries) 123 | } 124 | 125 | fn find_central_directory_offset(cursor: &mut Cursor<&[u8]>) -> Result> { 126 | cursor.set_position(cursor.get_ref().len() as u64 - 22); 127 | while cursor.position() > 0 { 128 | let signature = cursor.read_u32::()?; 129 | if signature == 0x06054b50 { 130 | cursor.set_position(cursor.position() + 12); 131 | let central_directory_offset = cursor.read_u32::()? as u64; 132 | return Ok(central_directory_offset); 133 | } 134 | cursor.set_position(cursor.position() - 5); 135 | } 136 | Err("End of central directory record not found.".into()) 137 | } 138 | 139 | #[expect(clippy::type_complexity)] 140 | fn read_central_file_header( 141 | cursor: &mut Cursor<&[u8]>, 142 | ) -> Result)>, Box> { 143 | let signature = cursor.read_u32::()?; 144 | if signature != 0x02014b50 { 145 | return Ok(None); 146 | } 147 | 148 | cursor.set_position(cursor.position() + 4); // skip version made by and version needed to extract 149 | cursor.set_position(cursor.position() + 2); // skip general purpose bit flag 150 | 151 | let compression_method = cursor.read_u16::()?; 152 | cursor.set_position(cursor.position() + 4); // skip last mod time and date 153 | 154 | let compression = match compression_method { 155 | 0 => Ok(Compression::Uncompressed), 156 | 8 => Ok(Compression::Deflate), 157 | _ => Err("Oh no"), 158 | } 159 | .unwrap(); 160 | 161 | let _crc32 = cursor.read_u32::()?; 162 | let compressed_size = cursor.read_u32::()? as u64; 163 | let _uncompressed_size = cursor.read_u32::()? as u64; 164 | 165 | let file_name_length = cursor.read_u16::()? as usize; 166 | let extra_field_length = cursor.read_u16::()? as usize; 167 | let comment_length = cursor.read_u16::()? as usize; 168 | 169 | let _disk_number_start = cursor.read_u16::()?; 170 | let _internal_file_attributes = cursor.read_u16::()?; 171 | let _external_file_attributes = cursor.read_u32::()?; 172 | let local_header_offset = cursor.read_u32::()? as u64; 173 | 174 | let mut file_name_bytes = vec![0; file_name_length]; 175 | cursor.read_exact(&mut file_name_bytes)?; 176 | let file_name = String::from_utf8(file_name_bytes)?; 177 | 178 | if file_name.ends_with('/') { 179 | return Ok(Some((file_name, None))); 180 | } 181 | 182 | cursor.set_position(cursor.position() + extra_field_length as u64 + comment_length as u64); 183 | 184 | let mut local_file_header_cursor = cursor.clone(); 185 | local_file_header_cursor.set_position(local_header_offset + 26); 186 | 187 | let local_file_header_file_name_length = 188 | local_file_header_cursor.read_u16::()? as usize; 189 | let local_file_header_extra_field_length = 190 | local_file_header_cursor.read_u16::()? as usize; 191 | let file_data_offset = local_header_offset 192 | + 30 193 | + local_file_header_file_name_length as u64 194 | + local_file_header_extra_field_length as u64; 195 | 196 | let entry = Entry { 197 | compression, 198 | offset: file_data_offset.try_into()?, 199 | size: compressed_size.try_into()?, 200 | }; 201 | 202 | Ok(Some((file_name, Some(entry)))) 203 | } 204 | --------------------------------------------------------------------------------