From 3f7fd02263509ad1ba657da55a3db16f786c06dd Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sun, 11 Jan 2026 13:31:40 +0800 Subject: [PATCH] feat: initial fetcher implementation --- Cargo.lock | 1055 +++++++++++++++++- default.nix | 15 + flake.lock | 15 + flake.nix | 4 + nix-js/Cargo.toml | 13 +- nix-js/build.rs | 1 + nix-js/runtime-ts/build.mjs | 2 +- nix-js/runtime-ts/src/builtins/attrs.ts | 8 +- nix-js/runtime-ts/src/builtins/conversion.ts | 47 +- nix-js/runtime-ts/src/builtins/io.ts | 210 +++- nix-js/runtime-ts/src/operators.ts | 44 + nix-js/runtime-ts/src/thunk.ts | 29 +- nix-js/runtime-ts/src/types/global.d.ts | 54 + nix-js/src/error.rs | 17 +- nix-js/src/fetcher/archive.rs | 214 ++++ nix-js/src/fetcher/cache.rs | 277 +++++ nix-js/src/fetcher/download.rs | 63 ++ nix-js/src/fetcher/git.rs | 303 +++++ nix-js/src/fetcher/hg.rs | 196 ++++ nix-js/src/fetcher/mod.rs | 240 ++++ nix-js/src/fetcher/nar.rs | 126 +++ nix-js/src/ir/utils.rs | 2 +- nix-js/src/lib.rs | 1 + nix-js/src/runtime.rs | 7 +- 24 files changed, 2898 insertions(+), 45 deletions(-) create mode 100644 default.nix create mode 100644 nix-js/src/fetcher/archive.rs create mode 100644 nix-js/src/fetcher/cache.rs create mode 100644 nix-js/src/fetcher/download.rs create mode 100644 nix-js/src/fetcher/git.rs create mode 100644 nix-js/src/fetcher/hg.rs create mode 100644 nix-js/src/fetcher/mod.rs create mode 100644 nix-js/src/fetcher/nar.rs diff --git a/Cargo.lock b/Cargo.lock index 567d169..f1746ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -41,6 +52,21 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -53,6 +79,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64-simd" version = "0.8.0" @@ -150,12 +182,37 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "calendrical_calculations" version = "0.2.3" @@ -198,6 +255,8 @@ version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -222,6 +281,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "ciborium" version = "0.2.2" @@ -249,6 +314,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -294,6 +369,12 @@ dependencies = [ "error-code", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.8.0" @@ -342,6 +423,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -444,6 +540,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "deno_core" version = "0.376.0" @@ -476,7 +578,7 @@ dependencies = [ "smallvec", "sourcemap", "static_assertions", - "thiserror", + "thiserror 2.0.17", "tokio", "url", "v8", @@ -528,7 +630,7 @@ dependencies = [ "strum_macros", "syn", "syn-match", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -540,7 +642,7 @@ dependencies = [ "deno_error", "percent-encoding", "sys_traits", - "thiserror", + "thiserror 2.0.17", "url", ] @@ -555,6 +657,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -586,6 +708,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -620,6 +743,27 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -682,12 +826,34 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -824,6 +990,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -831,9 +1010,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -906,6 +1087,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -915,6 +1105,107 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "icu_calendar" version = "2.1.1" @@ -1079,6 +1370,31 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1129,6 +1445,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.83" @@ -1171,6 +1497,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1200,9 +1537,36 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] [[package]] name = "memchr" @@ -1241,6 +1605,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1271,7 +1636,7 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", "libc", ] @@ -1280,10 +1645,13 @@ name = "nix-js" version = "0.1.0" dependencies = [ "anyhow", + "bzip2", "criterion", "deno_core", "deno_error", "derive_more", + "dirs", + "flate2", "hashbrown 0.16.1", "hex", "itertools 0.14.0", @@ -1291,12 +1659,18 @@ dependencies = [ "nix-js-macros", "petgraph", "regex", + "reqwest", "rnix", "rustyline", + "serde", + "serde_json", "sha2", "string-interner", + "tar", "tempfile", - "thiserror", + "thiserror 2.0.17", + "xz2", + "zip", ] [[package]] @@ -1327,9 +1701,15 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand", + "rand 0.8.5", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1360,6 +1740,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "outref" version = "0.5.2" @@ -1384,7 +1770,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1395,6 +1781,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1445,6 +1841,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -1484,6 +1886,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.36" @@ -1503,6 +1920,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -1540,7 +2012,27 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1549,6 +2041,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rayon" version = "1.11.0" @@ -1578,6 +2079,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.11.1" @@ -1607,6 +2128,46 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "resb" version = "0.1.1" @@ -1617,6 +2178,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rnix" version = "0.12.0" @@ -1686,6 +2261,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1714,6 +2324,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + [[package]] name = "same-file" version = "1.0.6" @@ -1779,6 +2395,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_v8" version = "0.285.0" @@ -1789,10 +2417,21 @@ dependencies = [ "num-bigint", "serde", "smallvec", - "thiserror", + "thiserror 2.0.17", "v8", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1820,6 +2459,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.11" @@ -1918,6 +2563,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.104" @@ -1940,6 +2591,15 @@ dependencies = [ "syn", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1977,6 +2637,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -1984,7 +2655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix 1.1.3", "windows-sys 0.61.2", @@ -2029,13 +2700,33 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2049,6 +2740,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "timezone_provider" version = "0.1.2" @@ -2082,6 +2792,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -2110,6 +2835,86 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -2146,6 +2951,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -2219,6 +3030,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2247,6 +3067,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -2286,7 +3119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a10e6b67c951a84de7029487e0e0a496860dae49f6699edd279d5ff35b8fbf54" dependencies = [ "deno_error", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -2299,6 +3132,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "6.0.3" @@ -2348,6 +3200,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2384,6 +3245,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2417,6 +3293,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2429,6 +3311,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2441,6 +3329,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2465,6 +3359,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2477,6 +3377,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2489,6 +3395,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2501,6 +3413,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2540,6 +3458,25 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yoke" version = "0.8.1" @@ -2604,6 +3541,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerotrie" version = "0.2.3" @@ -2638,6 +3595,36 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.17", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "1.0.6" @@ -2656,3 +3643,43 @@ dependencies = [ "resb", "serde", ] + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..484ea46 --- /dev/null +++ b/default.nix @@ -0,0 +1,15 @@ +let + lockFile = builtins.fromJSON (builtins.readFile ./flake.lock); + flake-compat-node = lockFile.nodes.${lockFile.nodes.root.inputs.flake-compat}; + flake-compat = builtins.fetchTarball { + inherit (flake-compat-node.locked) url; + sha256 = flake-compat-node.locked.narHash; + }; + + flake = ( + import flake-compat { + src = ./.; + } + ); +in + flake.defaultNix diff --git a/flake.lock b/flake.lock index c5994ce..27e2ee0 100644 --- a/flake.lock +++ b/flake.lock @@ -21,6 +21,20 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1751685974, + "narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=", + "rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1", + "type": "tarball", + "url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" + } + }, "nixpkgs": { "locked": { "lastModified": 1767116409, @@ -40,6 +54,7 @@ "root": { "inputs": { "fenix": "fenix", + "flake-compat": "flake-compat", "nixpkgs": "nixpkgs" } }, diff --git a/flake.nix b/flake.nix index 5ffbdf5..e4dfbce 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,10 @@ nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; fenix.url = "github:nix-community/fenix"; fenix.inputs.nixpkgs.follows = "nixpkgs"; + flake-compat = { + url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"; + flake = false; + }; }; outputs = { nixpkgs, fenix, ... }: let diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 950b3cd..682dd40 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -28,12 +28,23 @@ deno_error = "0.7" sha2 = "0.10" hex = "0.4" +# Fetcher dependencies +reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false } +tar = "0.4" +flate2 = "1.0" +xz2 = "0.1" +bzip2 = "0.5" +zip = "2.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +dirs = "5.0" +tempfile = "3.24" + rnix = "0.12" nix-js-macros = { path = "../nix-js-macros" } [dev-dependencies] -tempfile = "3.24" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] diff --git a/nix-js/build.rs b/nix-js/build.rs index ceca203..580cfe3 100644 --- a/nix-js/build.rs +++ b/nix-js/build.rs @@ -14,6 +14,7 @@ fn main() { println!("cargo::rerun-if-changed=runtime-ts/src"); println!("cargo::rerun-if-changed=runtime-ts/package.json"); println!("cargo::rerun-if-changed=runtime-ts/tsconfig.json"); + println!("cargo::rerun-if-changed=runtime-ts/build.mjs"); if !runtime_ts_dir.join("node_modules").exists() { println!("Installing npm dependencies..."); diff --git a/nix-js/runtime-ts/build.mjs b/nix-js/runtime-ts/build.mjs index 52eec08..1e6c26c 100644 --- a/nix-js/runtime-ts/build.mjs +++ b/nix-js/runtime-ts/build.mjs @@ -4,5 +4,5 @@ await esbuild.build({ entryPoints: ["src/index.ts"], outfile: "dist/runtime.js", bundle: true, - minify: true, + // minify: true, }); diff --git a/nix-js/runtime-ts/src/builtins/attrs.ts b/nix-js/runtime-ts/src/builtins/attrs.ts index d8ebf7b..bfb0784 100644 --- a/nix-js/runtime-ts/src/builtins/attrs.ts +++ b/nix-js/runtime-ts/src/builtins/attrs.ts @@ -22,11 +22,15 @@ export const hasAttr = export const mapAttrs = (f: NixValue) => (attrs: NixValue): NixAttrs => { - const new_attrs: NixAttrs = {}; const forced_attrs = forceAttrs(attrs); const forced_f = forceFunction(f); + const new_attrs: NixAttrs = {}; for (const key in forced_attrs) { - new_attrs[key] = forceFunction(forced_f(key))(forced_attrs[key]); + Object.defineProperty(new_attrs, key, { + get: () => forceFunction(forced_f(key))(forced_attrs[key]), + enumerable: true, + configurable: true, + }); } return new_attrs; }; diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index bc24848..675329d 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -12,8 +12,51 @@ import { } from "../string-context"; import { forceFunction } from "../type-assert"; -export const fromJSON = (e: NixValue): never => { - throw new Error("Not implemented: fromJSON"); +const convertJsonToNix = (json: unknown): NixValue => { + if (json === null) { + return null; + } + if (typeof json === "boolean") { + return json; + } + if (typeof json === "number") { + if (Number.isInteger(json)) { + return BigInt(json); + } + return json; + } + if (typeof json === "string") { + return json; + } + if (Array.isArray(json)) { + return json.map(convertJsonToNix); + } + if (typeof json === "object") { + const result: Record = {}; + for (const [key, value] of Object.entries(json)) { + result[key] = convertJsonToNix(value); + } + return result; + } + throw new TypeError(`unsupported JSON value type: ${typeof json}`); +}; + +export const fromJSON = (e: NixValue): NixValue => { + const str = force(e); + if (typeof str !== "string" && !isStringWithContext(str)) { + throw new TypeError( + `builtins.fromJSON: expected a string, got ${typeName(str)}`, + ); + } + const jsonStr = isStringWithContext(str) ? str.value : str; + try { + const parsed = JSON.parse(jsonStr); + return convertJsonToNix(parsed); + } catch (err) { + throw new SyntaxError( + `builtins.fromJSON: ${err instanceof Error ? err.message : String(err)}`, + ); + } }; export const fromTOML = (e: NixValue): never => { diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index f1c1040..fa1a483 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -3,8 +3,9 @@ * Implemented via Rust ops exposed through deno_core */ -import { forceString } from "../type-assert"; -import type { NixValue } from "../types"; +import { forceAttrs, forceBool, forceString } from "../type-assert"; +import type { NixValue, NixAttrs } from "../types"; +import { force } from "../thunk"; // Declare Deno.core.ops global (provided by deno_core runtime) @@ -33,24 +34,209 @@ export const fetchClosure = (args: NixValue): never => { throw new Error("Not implemented: fetchClosure"); }; -export const fetchMercurial = (args: NixValue): never => { - throw new Error("Not implemented: fetchMercurial"); +interface FetchUrlResult { + store_path: string; + hash: string; +} + +interface FetchTarballResult { + store_path: string; + hash: string; +} + +interface FetchGitResult { + out_path: string; + rev: string; + short_rev: string; + rev_count: number; + last_modified: number; + last_modified_date: string; + submodules: boolean; + nar_hash: string | null; +} + +interface FetchHgResult { + out_path: string; + branch: string; + rev: string; + short_rev: string; + rev_count: number; +} + +const normalizeUrlInput = ( + args: NixValue, +): { url: string; hash?: string; name?: string; executable?: boolean } => { + const forced = force(args); + if (typeof forced === "string") { + return { url: forced }; + } + const attrs = forceAttrs(args); + const url = forceString(attrs.url); + const hash = + "sha256" in attrs + ? forceString(attrs.sha256) + : "hash" in attrs + ? forceString(attrs.hash) + : undefined; + const name = "name" in attrs ? forceString(attrs.name) : undefined; + const executable = "executable" in attrs ? forceBool(attrs.executable) : false; + return { url, hash, name, executable }; }; -export const fetchGit = (args: NixValue): never => { - throw new Error("Not implemented: fetchGit"); +export const fetchurl = (args: NixValue): string => { + const { url, hash, name, executable } = normalizeUrlInput(args); + const result: FetchUrlResult = Deno.core.ops.op_fetch_url( + url, + hash ?? null, + name ?? null, + executable ?? false, + ); + return result.store_path; }; -export const fetchTarball = (args: NixValue): never => { - throw new Error("Not implemented: fetchTarball"); +export const fetchTarball = (args: NixValue): string => { + const { url, hash, name } = normalizeUrlInput(args); + const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball( + url, + hash ?? null, + name ?? null, + ); + return result.store_path; }; -export const fetchTree = (args: NixValue): never => { - throw new Error("Not implemented: fetchTree"); +export const fetchGit = (args: NixValue): NixAttrs => { + const forced = force(args); + if (typeof forced === "string") { + const result: FetchGitResult = Deno.core.ops.op_fetch_git( + forced, + null, + null, + false, + false, + false, + null, + ); + return { + outPath: result.out_path, + rev: result.rev, + shortRev: result.short_rev, + revCount: BigInt(result.rev_count), + lastModified: BigInt(result.last_modified), + lastModifiedDate: result.last_modified_date, + submodules: result.submodules, + narHash: result.nar_hash, + }; + } + const attrs = forceAttrs(args); + const url = forceString(attrs.url); + const gitRef = "ref" in attrs ? forceString(attrs.ref) : null; + const rev = "rev" in attrs ? forceString(attrs.rev) : null; + const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false; + const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false; + const allRefs = "allRefs" in attrs ? forceBool(attrs.allRefs) : false; + const name = "name" in attrs ? forceString(attrs.name) : null; + + const result: FetchGitResult = Deno.core.ops.op_fetch_git( + url, + gitRef, + rev, + shallow, + submodules, + allRefs, + name, + ); + + return { + outPath: result.out_path, + rev: result.rev, + shortRev: result.short_rev, + revCount: BigInt(result.rev_count), + lastModified: BigInt(result.last_modified), + lastModifiedDate: result.last_modified_date, + submodules: result.submodules, + narHash: result.nar_hash, + }; }; -export const fetchurl = (args: NixValue): never => { - throw new Error("Not implemented: fetchurl"); +export const fetchMercurial = (args: NixValue): NixAttrs => { + const attrs = forceAttrs(args); + const url = forceString(attrs.url); + const rev = "rev" in attrs ? forceString(attrs.rev) : null; + const name = "name" in attrs ? forceString(attrs.name) : null; + + const result: FetchHgResult = Deno.core.ops.op_fetch_hg(url, rev, name); + + return { + outPath: result.out_path, + branch: result.branch, + rev: result.rev, + shortRev: result.short_rev, + revCount: BigInt(result.rev_count), + }; +}; + +export const fetchTree = (args: NixValue): NixAttrs => { + const attrs = forceAttrs(args); + const type = "type" in attrs ? forceString(attrs.type) : "auto"; + + switch (type) { + case "git": + return fetchGit(args); + case "hg": + case "mercurial": + return fetchMercurial(args); + case "tarball": + return { outPath: fetchTarball(args) }; + case "file": + return { outPath: fetchurl(args) }; + case "path": { + const path = forceString(attrs.path); + return { outPath: path }; + } + case "github": + case "gitlab": + case "sourcehut": + return fetchGitForge(type, attrs); + case "auto": + default: + return autoDetectAndFetch(attrs); + } +}; + +const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => { + const owner = forceString(attrs.owner); + const repo = forceString(attrs.repo); + const rev = + "rev" in attrs + ? forceString(attrs.rev) + : "ref" in attrs + ? forceString(attrs.ref) + : "HEAD"; + + const baseUrls: Record = { + github: "https://github.com", + gitlab: "https://gitlab.com", + sourcehut: "https://git.sr.ht", + }; + + const url = `${baseUrls[forge]}/${owner}/${repo}`; + return fetchGit({ ...attrs, url, rev }); +}; + +const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => { + const url = forceString(attrs.url); + if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) { + return fetchGit(attrs); + } + if ( + url.endsWith(".tar.gz") || + url.endsWith(".tar.xz") || + url.endsWith(".tar.bz2") || + url.endsWith(".tgz") + ) { + return { outPath: fetchTarball(attrs) }; + } + return { outPath: fetchurl(attrs) }; }; export const readDir = (path: NixValue): never => { diff --git a/nix-js/runtime-ts/src/operators.ts b/nix-js/runtime-ts/src/operators.ts index 6e7e0ca..d0b1c35 100644 --- a/nix-js/runtime-ts/src/operators.ts +++ b/nix-js/runtime-ts/src/operators.ts @@ -13,11 +13,21 @@ import { mergeContexts, mkStringWithContext, } from "./string-context"; +import { coerceToString, StringCoercionMode } from "./builtins/conversion"; const isNixString = (v: unknown): v is NixString => { return typeof v === "string" || isStringWithContext(v); }; +const canCoerceToString = (v: NixValue): boolean => { + const forced = force(v); + if (isNixString(forced)) return true; + if (typeof forced === "object" && forced !== null && !Array.isArray(forced)) { + if ("outPath" in forced || "__toString" in forced) return true; + } + return false; +}; + /** * Operator object exported as Nix.op * All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq) @@ -40,6 +50,12 @@ export const op = { return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB)); } + if (canCoerceToString(a) && canCoerceToString(b)) { + const strA = coerceToString(a, StringCoercionMode.Interpolation, false); + const strB = coerceToString(b, StringCoercionMode.Interpolation, false); + return strA + strB; + } + const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b)); return (numA as any) + (numB as any); }, @@ -79,6 +95,34 @@ export const op = { return av === Number(bv); } + if (Array.isArray(av) && Array.isArray(bv)) { + if (av.length !== bv.length) return false; + for (let i = 0; i < av.length; i++) { + if (!op.eq(av[i], bv[i])) return false; + } + return true; + } + + if ( + typeof av === "object" && + av !== null && + !Array.isArray(av) && + typeof bv === "object" && + bv !== null && + !Array.isArray(bv) && + !isNixString(av) && + !isNixString(bv) + ) { + const keysA = Object.keys(av); + const keysB = Object.keys(bv); + if (keysA.length !== keysB.length) return false; + for (const key of keysA) { + if (!(key in bv)) return false; + if (!op.eq((av as NixAttrs)[key], (bv as NixAttrs)[key])) return false; + } + return true; + } + return av === bv; }, neq: (a: NixValue, b: NixValue): boolean => { diff --git a/nix-js/runtime-ts/src/thunk.ts b/nix-js/runtime-ts/src/thunk.ts index c5553a7..e1d8e16 100644 --- a/nix-js/runtime-ts/src/thunk.ts +++ b/nix-js/runtime-ts/src/thunk.ts @@ -16,6 +16,11 @@ export const IS_THUNK = Symbol("is_thunk"); * * A thunk wraps a function that produces a value when called. * Once evaluated, the result is cached to avoid recomputation. + * + * Thunk states: + * - Unevaluated: func is defined, result is undefined + * - Evaluating (blackhole): func is undefined, result is undefined + * - Evaluated: func is undefined, result is defined */ export class NixThunk implements NixThunkInterface { [key: symbol]: any; @@ -43,23 +48,37 @@ export const isThunk = (value: unknown): value is NixThunkInterface => { * If the value is a thunk, evaluate it and cache the result * If already evaluated or not a thunk, return as-is * + * Uses "blackhole" detection to catch infinite recursion: + * - Before evaluating, set func to undefined (entering blackhole state) + * - If we encounter a thunk with func=undefined and result=undefined, it's a blackhole + * * @param value - Value to force (may be a thunk) * @returns The forced/evaluated value + * @throws Error if infinite recursion is detected */ export const force = (value: NixValue): NixStrictValue => { if (!isThunk(value)) { return value; } - // Already evaluated - return cached result + // Check if already evaluated or in blackhole state if (value.func === undefined) { - return value.result!; + // Blackhole: func is undefined but result is also undefined + if (value.result === undefined) { + throw new Error("infinite recursion encountered (blackhole)"); + } + // Already evaluated - return cached result + return value.result; } - // Evaluate and cache - const result = force(value.func()); - value.result = result; + // Save func and enter blackhole state BEFORE calling func() + const func = value.func; value.func = undefined; + // result stays undefined - this is the blackhole state + + // Evaluate and cache + const result = force(func()); + value.result = result; return result; }; diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index b97f068..88a42ee 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -1,5 +1,34 @@ import type { NixRuntime } from ".."; +interface FetchUrlResult { + store_path: string; + hash: string; +} + +interface FetchTarballResult { + store_path: string; + hash: string; +} + +interface FetchGitResult { + out_path: string; + rev: string; + short_rev: string; + rev_count: number; + last_modified: number; + last_modified_date: string; + submodules: boolean; + nar_hash: string | null; +} + +interface FetchHgResult { + out_path: string; + branch: string; + rev: string; + short_rev: string; + rev_count: number; +} + declare global { var Nix: NixRuntime; namespace Deno { @@ -18,6 +47,31 @@ declare global { hash_mode: string, name: string, ): string; + function op_fetch_url( + url: string, + expected_hash: string | null, + name: string | null, + executable: boolean, + ): FetchUrlResult; + function op_fetch_tarball( + url: string, + expected_hash: string | null, + name: string | null, + ): FetchTarballResult; + function op_fetch_git( + url: string, + ref: string | null, + rev: string | null, + shallow: boolean, + submodules: boolean, + all_refs: boolean, + name: string | null, + ): FetchGitResult; + function op_fetch_hg( + url: string, + rev: string | null, + name: string | null, + ): FetchHgResult; } } } diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index a56d1a6..9f7da7b 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -9,8 +9,14 @@ pub enum ErrorKind { ParseError(String), #[error("error occurred during downgrade stage: {0}")] DowngradeError(String), - #[error("error occurred during evaluation stage: {0}")] - EvalError(String), + #[error( + "error occurred during evaluation stage: {msg}{}", + backtrace.as_ref().map_or("".into(), |backtrace| format!("\nBacktrace: {backtrace}")) + )] + EvalError { + msg: String, + backtrace: Option, + }, #[error("internal error occurred: {0}")] InternalError(String), #[error("{0}")] @@ -114,8 +120,11 @@ impl Error { pub fn downgrade_error(msg: String) -> Self { Self::new(ErrorKind::DowngradeError(msg)) } - pub fn eval_error(msg: String) -> Self { - Self::new(ErrorKind::EvalError(msg)) + pub fn eval_error(msg: String, backtrace: Option) -> Self { + Self::new(ErrorKind::EvalError { + msg, + backtrace + }) } pub fn internal(msg: String) -> Self { Self::new(ErrorKind::InternalError(msg)) diff --git a/nix-js/src/fetcher/archive.rs b/nix-js/src/fetcher/archive.rs new file mode 100644 index 0000000..15d1ff1 --- /dev/null +++ b/nix-js/src/fetcher/archive.rs @@ -0,0 +1,214 @@ +use std::fs::{self, File}; +use std::io::Cursor; +use std::path::PathBuf; + +use flate2::read::GzDecoder; + +#[derive(Debug, Clone, Copy)] +pub enum ArchiveFormat { + TarGz, + TarXz, + TarBz2, + Tar, + Zip, +} + +impl ArchiveFormat { + pub fn detect(url: &str, data: &[u8]) -> Self { + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { + return ArchiveFormat::TarGz; + } + if url.ends_with(".tar.xz") || url.ends_with(".txz") { + return ArchiveFormat::TarXz; + } + if url.ends_with(".tar.bz2") || url.ends_with(".tbz2") { + return ArchiveFormat::TarBz2; + } + if url.ends_with(".tar") { + return ArchiveFormat::Tar; + } + if url.ends_with(".zip") { + return ArchiveFormat::Zip; + } + + if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b { + return ArchiveFormat::TarGz; + } + if data.len() >= 6 && &data[0..6] == b"\xfd7zXZ\x00" { + return ArchiveFormat::TarXz; + } + if data.len() >= 3 && &data[0..3] == b"BZh" { + return ArchiveFormat::TarBz2; + } + if data.len() >= 4 && &data[0..4] == b"PK\x03\x04" { + return ArchiveFormat::Zip; + } + + ArchiveFormat::TarGz + } +} + +pub fn extract_archive(data: &[u8], dest: &PathBuf) -> Result { + let format = ArchiveFormat::detect("", data); + + let temp_dir = dest.join("_extract_temp"); + fs::create_dir_all(&temp_dir)?; + + match format { + ArchiveFormat::TarGz => extract_tar_gz(data, &temp_dir)?, + ArchiveFormat::TarXz => extract_tar_xz(data, &temp_dir)?, + ArchiveFormat::TarBz2 => extract_tar_bz2(data, &temp_dir)?, + ArchiveFormat::Tar => extract_tar(data, &temp_dir)?, + ArchiveFormat::Zip => extract_zip(data, &temp_dir)?, + } + + strip_single_toplevel(&temp_dir, dest) +} + +fn extract_tar_gz(data: &[u8], dest: &PathBuf) -> Result<(), ArchiveError> { + let decoder = GzDecoder::new(Cursor::new(data)); + let mut archive = tar::Archive::new(decoder); + archive.unpack(dest)?; + Ok(()) +} + +fn extract_tar_xz(data: &[u8], dest: &PathBuf) -> Result<(), ArchiveError> { + let decoder = xz2::read::XzDecoder::new(Cursor::new(data)); + let mut archive = tar::Archive::new(decoder); + archive.unpack(dest)?; + Ok(()) +} + +fn extract_tar_bz2(data: &[u8], dest: &PathBuf) -> Result<(), ArchiveError> { + let decoder = bzip2::read::BzDecoder::new(Cursor::new(data)); + let mut archive = tar::Archive::new(decoder); + archive.unpack(dest)?; + Ok(()) +} + +fn extract_tar(data: &[u8], dest: &PathBuf) -> Result<(), ArchiveError> { + let mut archive = tar::Archive::new(Cursor::new(data)); + archive.unpack(dest)?; + Ok(()) +} + +fn extract_zip(data: &[u8], dest: &PathBuf) -> Result<(), ArchiveError> { + let cursor = Cursor::new(data); + let mut archive = zip::ZipArchive::new(cursor)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = dest.join(file.mangled_name()); + + if file.is_dir() { + fs::create_dir_all(&outpath)?; + } else { + if let Some(parent) = outpath.parent() { + fs::create_dir_all(parent)?; + } + let mut outfile = File::create(&outpath)?; + std::io::copy(&mut file, &mut outfile)?; + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } + + Ok(()) +} + +fn strip_single_toplevel(temp_dir: &PathBuf, dest: &PathBuf) -> Result { + let entries: Vec<_> = fs::read_dir(temp_dir)? + .filter_map(|e| e.ok()) + .filter(|e| !e.file_name().to_string_lossy().starts_with('.')) + .collect(); + + let source_dir = if entries.len() == 1 && entries[0].file_type()?.is_dir() { + entries[0].path() + } else { + temp_dir.clone() + }; + + let final_dest = dest.join("content"); + if final_dest.exists() { + fs::remove_dir_all(&final_dest)?; + } + + if source_dir == *temp_dir { + fs::rename(temp_dir, &final_dest)?; + } else { + copy_dir_recursive(&source_dir, &final_dest)?; + fs::remove_dir_all(temp_dir)?; + } + + Ok(final_dest) +} + +fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), std::io::Error> { + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dst.join(entry.file_name()); + let metadata = fs::symlink_metadata(&path)?; + + if metadata.is_symlink() { + let target = fs::read_link(&path)?; + #[cfg(unix)] + { + std::os::unix::fs::symlink(&target, &dest_path)?; + } + #[cfg(windows)] + { + if target.is_dir() { + std::os::windows::fs::symlink_dir(&target, &dest_path)?; + } else { + std::os::windows::fs::symlink_file(&target, &dest_path)?; + } + } + } else if metadata.is_dir() { + copy_dir_recursive(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path)?; + } + } + + Ok(()) +} + +#[derive(Debug)] +pub enum ArchiveError { + IoError(std::io::Error), + ZipError(zip::result::ZipError), + UnsupportedFormat(String), +} + +impl std::fmt::Display for ArchiveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArchiveError::IoError(e) => write!(f, "I/O error: {}", e), + ArchiveError::ZipError(e) => write!(f, "ZIP error: {}", e), + ArchiveError::UnsupportedFormat(fmt) => write!(f, "Unsupported archive format: {}", fmt), + } + } +} + +impl std::error::Error for ArchiveError {} + +impl From for ArchiveError { + fn from(e: std::io::Error) -> Self { + ArchiveError::IoError(e) + } +} + +impl From for ArchiveError { + fn from(e: zip::result::ZipError) -> Self { + ArchiveError::ZipError(e) + } +} diff --git a/nix-js/src/fetcher/cache.rs b/nix-js/src/fetcher/cache.rs new file mode 100644 index 0000000..a651af5 --- /dev/null +++ b/nix-js/src/fetcher/cache.rs @@ -0,0 +1,277 @@ +use std::fs::{self, File}; +use std::io::Write; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use super::archive::ArchiveError; + +#[derive(Debug)] +pub enum CacheError { + Io(std::io::Error), + Archive(ArchiveError), + Json(serde_json::Error), +} + +impl std::fmt::Display for CacheError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CacheError::Io(e) => write!(f, "I/O error: {}", e), + CacheError::Archive(e) => write!(f, "Archive error: {}", e), + CacheError::Json(e) => write!(f, "JSON error: {}", e), + } + } +} + +impl std::error::Error for CacheError {} + +impl From for CacheError { + fn from(e: std::io::Error) -> Self { + CacheError::Io(e) + } +} + +impl From for CacheError { + fn from(e: ArchiveError) -> Self { + CacheError::Archive(e) + } +} + +impl From for CacheError { + fn from(e: serde_json::Error) -> Self { + CacheError::Json(e) + } +} + +#[derive(Debug)] +pub struct FetcherCache { + base_dir: PathBuf, +} + +#[derive(Serialize, Deserialize)] +struct CacheMetadata { + url: String, + hash: String, + name: String, +} + +impl FetcherCache { + pub fn new() -> Result { + let base_dir = dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from("/tmp")) + .join("nix-js") + .join("fetchers"); + + fs::create_dir_all(&base_dir)?; + + Ok(Self { base_dir }) + } + + fn url_cache_dir(&self) -> PathBuf { + self.base_dir.join("url") + } + + fn tarball_cache_dir(&self) -> PathBuf { + self.base_dir.join("tarball") + } + + fn git_cache_dir(&self) -> PathBuf { + self.base_dir.join("git") + } + + fn hg_cache_dir(&self) -> PathBuf { + self.base_dir.join("hg") + } + + fn hash_key(url: &str) -> String { + crate::nix_hash::sha256_hex(url) + } + + pub fn get_url(&self, url: &str, expected_hash: &str) -> Option { + let cache_dir = self.url_cache_dir(); + let key = Self::hash_key(url); + let meta_path = cache_dir.join(format!("{}.meta", key)); + let data_path = cache_dir.join(format!("{}.data", key)); + + if !meta_path.exists() || !data_path.exists() { + return None; + } + + let meta: CacheMetadata = serde_json::from_str(&fs::read_to_string(&meta_path).ok()?).ok()?; + + if meta.hash == expected_hash { + Some(data_path) + } else { + None + } + } + + pub fn put_url( + &self, + url: &str, + hash: &str, + data: &[u8], + name: &str, + executable: bool, + ) -> Result { + let cache_dir = self.url_cache_dir(); + fs::create_dir_all(&cache_dir)?; + + let key = Self::hash_key(url); + let meta_path = cache_dir.join(format!("{}.meta", key)); + let data_path = cache_dir.join(format!("{}.data", key)); + + let mut file = File::create(&data_path)?; + file.write_all(data)?; + + #[cfg(unix)] + if executable { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&data_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&data_path, perms)?; + } + + let meta = CacheMetadata { + url: url.to_string(), + hash: hash.to_string(), + name: name.to_string(), + }; + fs::write(&meta_path, serde_json::to_string(&meta)?)?; + + let store_path = self.make_store_path(hash, name); + if !store_path.exists() { + fs::create_dir_all(store_path.parent().unwrap_or(&store_path))?; + fs::copy(&data_path, &store_path)?; + + #[cfg(unix)] + if executable { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&store_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&store_path, perms)?; + } + } + + Ok(store_path) + } + + pub fn get_tarball(&self, url: &str, expected_hash: &str) -> Option { + let cache_dir = self.tarball_cache_dir(); + let key = Self::hash_key(url); + let meta_path = cache_dir.join(&key).join(".meta"); + let data_dir = cache_dir.join(&key); + + if !meta_path.exists() || !data_dir.exists() { + return None; + } + + let meta: CacheMetadata = serde_json::from_str(&fs::read_to_string(&meta_path).ok()?).ok()?; + + if meta.hash == expected_hash { + Some(self.make_store_path(&meta.hash, &meta.name)) + } else { + None + } + } + + pub fn put_tarball( + &self, + url: &str, + hash: &str, + data: &[u8], + name: &str, + ) -> Result { + let cache_dir = self.tarball_cache_dir(); + let key = Self::hash_key(url); + let extract_dir = cache_dir.join(&key); + + fs::create_dir_all(&extract_dir)?; + + let extracted_path = super::archive::extract_archive(data, &extract_dir)?; + + let meta = CacheMetadata { + url: url.to_string(), + hash: hash.to_string(), + name: name.to_string(), + }; + fs::write(extract_dir.join(".meta"), serde_json::to_string(&meta)?)?; + + let store_path = self.make_store_path(hash, name); + if !store_path.exists() { + fs::create_dir_all(store_path.parent().unwrap_or(&store_path))?; + copy_dir_recursive(&extracted_path, &store_path)?; + } + + Ok(store_path) + } + + pub fn put_tarball_from_extracted( + &self, + url: &str, + hash: &str, + extracted_path: &PathBuf, + name: &str, + ) -> Result { + let cache_dir = self.tarball_cache_dir(); + let key = Self::hash_key(url); + let cache_entry_dir = cache_dir.join(&key); + + fs::create_dir_all(&cache_entry_dir)?; + + let cached_content = cache_entry_dir.join("content"); + if !cached_content.exists() { + copy_dir_recursive(extracted_path, &cached_content)?; + } + + let meta = CacheMetadata { + url: url.to_string(), + hash: hash.to_string(), + name: name.to_string(), + }; + fs::write(cache_entry_dir.join(".meta"), serde_json::to_string(&meta)?)?; + + let store_path = self.make_store_path(hash, name); + if !store_path.exists() { + fs::create_dir_all(store_path.parent().unwrap_or(&store_path))?; + copy_dir_recursive(extracted_path, &store_path)?; + } + + Ok(store_path) + } + + pub fn get_git_bare(&self, url: &str) -> PathBuf { + let key = Self::hash_key(url); + self.git_cache_dir().join(key) + } + + pub fn get_hg_bare(&self, url: &str) -> PathBuf { + let key = Self::hash_key(url); + self.hg_cache_dir().join(key) + } + + pub fn make_store_path(&self, hash: &str, name: &str) -> PathBuf { + let short_hash = &hash[..32.min(hash.len())]; + self.base_dir + .join("store") + .join(format!("{}-{}", short_hash, name)) + } +} + +fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), std::io::Error> { + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let dest_path = dst.join(entry.file_name()); + + if path.is_dir() { + copy_dir_recursive(&path, &dest_path)?; + } else { + fs::copy(&path, &dest_path)?; + } + } + + Ok(()) +} diff --git a/nix-js/src/fetcher/download.rs b/nix-js/src/fetcher/download.rs new file mode 100644 index 0000000..2295dc9 --- /dev/null +++ b/nix-js/src/fetcher/download.rs @@ -0,0 +1,63 @@ +use reqwest::blocking::Client; +use std::time::Duration; + +pub struct Downloader { + client: Client, +} + +impl Downloader { + pub fn new() -> Self { + let client = Client::builder() + .timeout(Duration::from_secs(300)) + .user_agent("nix-js/0.1") + .build() + .expect("Failed to create HTTP client"); + + Self { client } + } + + pub fn download(&self, url: &str) -> Result, DownloadError> { + let response = self + .client + .get(url) + .send() + .map_err(|e| DownloadError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(DownloadError::HttpError { + url: url.to_string(), + status: response.status().as_u16(), + }); + } + + response + .bytes() + .map(|b| b.to_vec()) + .map_err(|e| DownloadError::NetworkError(e.to_string())) + } +} + +impl Default for Downloader { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +pub enum DownloadError { + NetworkError(String), + HttpError { url: String, status: u16 }, +} + +impl std::fmt::Display for DownloadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DownloadError::NetworkError(msg) => write!(f, "Network error: {}", msg), + DownloadError::HttpError { url, status } => { + write!(f, "HTTP error {} for URL: {}", status, url) + } + } + } +} + +impl std::error::Error for DownloadError {} diff --git a/nix-js/src/fetcher/git.rs b/nix-js/src/fetcher/git.rs new file mode 100644 index 0000000..830870c --- /dev/null +++ b/nix-js/src/fetcher/git.rs @@ -0,0 +1,303 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +use super::cache::FetcherCache; +use super::FetchGitResult; + +pub fn fetch_git( + cache: &FetcherCache, + url: &str, + git_ref: Option<&str>, + rev: Option<&str>, + _shallow: bool, + submodules: bool, + all_refs: bool, + name: &str, +) -> Result { + let bare_repo = cache.get_git_bare(url); + + if !bare_repo.exists() { + clone_bare(url, &bare_repo)?; + } else { + fetch_repo(&bare_repo, all_refs)?; + } + + let target_rev = resolve_rev(&bare_repo, git_ref, rev)?; + let checkout_dir = checkout_rev(&bare_repo, &target_rev, submodules, name, cache)?; + + let rev_count = get_rev_count(&bare_repo, &target_rev)?; + let last_modified = get_last_modified(&bare_repo, &target_rev)?; + let last_modified_date = format_timestamp(last_modified); + + let short_rev = if target_rev.len() >= 7 { + target_rev[..7].to_string() + } else { + target_rev.clone() + }; + + Ok(FetchGitResult { + out_path: checkout_dir.to_string_lossy().to_string(), + rev: target_rev, + short_rev, + rev_count, + last_modified, + last_modified_date, + submodules, + nar_hash: None, + }) +} + +fn clone_bare(url: &str, dest: &PathBuf) -> Result<(), GitError> { + fs::create_dir_all(dest.parent().unwrap_or(dest))?; + + let output = Command::new("git") + .args(["clone", "--bare", url]) + .arg(dest) + .output()?; + + if !output.status.success() { + return Err(GitError::CommandFailed { + operation: "clone".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) +} + +fn fetch_repo(repo: &PathBuf, all_refs: bool) -> Result<(), GitError> { + let mut args = vec!["fetch", "--prune"]; + if all_refs { + args.push("--all"); + } + + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output()?; + + if !output.status.success() { + return Err(GitError::CommandFailed { + operation: "fetch".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) +} + +fn resolve_rev(repo: &PathBuf, git_ref: Option<&str>, rev: Option<&str>) -> Result { + if let Some(rev) = rev { + return Ok(rev.to_string()); + } + + let ref_to_resolve = git_ref.unwrap_or("HEAD"); + + let output = Command::new("git") + .args(["rev-parse", ref_to_resolve]) + .current_dir(repo) + .output()?; + + if !output.status.success() { + let output = Command::new("git") + .args(["rev-parse", &format!("refs/heads/{}", ref_to_resolve)]) + .current_dir(repo) + .output()?; + + if !output.status.success() { + let output = Command::new("git") + .args(["rev-parse", &format!("refs/tags/{}", ref_to_resolve)]) + .current_dir(repo) + .output()?; + + if !output.status.success() { + return Err(GitError::CommandFailed { + operation: "rev-parse".to_string(), + message: format!("Could not resolve ref: {}", ref_to_resolve), + }); + } + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn checkout_rev( + bare_repo: &PathBuf, + rev: &str, + submodules: bool, + name: &str, + cache: &FetcherCache, +) -> Result { + let hash = crate::nix_hash::sha256_hex(&format!("{}:{}", bare_repo.display(), rev)); + let checkout_dir = cache.make_store_path(&hash, name); + + if checkout_dir.exists() { + return Ok(checkout_dir); + } + + fs::create_dir_all(&checkout_dir)?; + + let output = Command::new("git") + .args(["--work-tree", checkout_dir.to_str().unwrap_or(".")]) + .arg("checkout") + .arg(rev) + .arg("--") + .arg(".") + .current_dir(bare_repo) + .output()?; + + if !output.status.success() { + fs::remove_dir_all(&checkout_dir)?; + return Err(GitError::CommandFailed { + operation: "checkout".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + if submodules { + let output = Command::new("git") + .args(["submodule", "update", "--init", "--recursive"]) + .current_dir(&checkout_dir) + .output()?; + + if !output.status.success() { + eprintln!( + "Warning: failed to initialize submodules: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } + + let git_dir = checkout_dir.join(".git"); + if git_dir.exists() { + fs::remove_dir_all(&git_dir)?; + } + + Ok(checkout_dir) +} + +fn get_rev_count(repo: &PathBuf, rev: &str) -> Result { + let output = Command::new("git") + .args(["rev-list", "--count", rev]) + .current_dir(repo) + .output()?; + + if !output.status.success() { + return Ok(0); + } + + let count_str = String::from_utf8_lossy(&output.stdout); + count_str.trim().parse().unwrap_or(0).pipe(Ok) +} + +fn get_last_modified(repo: &PathBuf, rev: &str) -> Result { + let output = Command::new("git") + .args(["log", "-1", "--format=%ct", rev]) + .current_dir(repo) + .output()?; + + if !output.status.success() { + return Ok(0); + } + + let ts_str = String::from_utf8_lossy(&output.stdout); + ts_str.trim().parse().unwrap_or(0).pipe(Ok) +} + +fn format_timestamp(ts: u64) -> String { + use std::time::{Duration, UNIX_EPOCH}; + + let datetime = UNIX_EPOCH + Duration::from_secs(ts); + let secs = datetime + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + let days_since_epoch = secs / 86400; + let remaining_secs = secs % 86400; + let hours = remaining_secs / 3600; + let minutes = (remaining_secs % 3600) / 60; + let seconds = remaining_secs % 60; + + let (year, month, day) = days_to_ymd(days_since_epoch); + + format!( + "{:04}{:02}{:02}{:02}{:02}{:02}", + year, month, day, hours, minutes, seconds + ) +} + +fn days_to_ymd(days: u64) -> (u64, u64, u64) { + let mut y = 1970; + let mut remaining = days as i64; + + loop { + let days_in_year = if is_leap_year(y) { 366 } else { 365 }; + if remaining < days_in_year { + break; + } + remaining -= days_in_year; + y += 1; + } + + let days_in_months: [i64; 12] = if is_leap_year(y) { + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + let mut m = 1; + for days_in_month in days_in_months.iter() { + if remaining < *days_in_month { + break; + } + remaining -= *days_in_month; + m += 1; + } + + (y, m, (remaining + 1) as u64) +} + +fn is_leap_year(y: u64) -> bool { + (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) +} + +trait Pipe: Sized { + fn pipe(self, f: F) -> R + where + F: FnOnce(Self) -> R, + { + f(self) + } +} + +impl Pipe for T {} + +#[derive(Debug)] +pub enum GitError { + IoError(std::io::Error), + CommandFailed { operation: String, message: String }, +} + +impl std::fmt::Display for GitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GitError::IoError(e) => write!(f, "I/O error: {}", e), + GitError::CommandFailed { operation, message } => { + write!(f, "Git {} failed: {}", operation, message) + } + } + } +} + +impl std::error::Error for GitError {} + +impl From for GitError { + fn from(e: std::io::Error) -> Self { + GitError::IoError(e) + } +} diff --git a/nix-js/src/fetcher/hg.rs b/nix-js/src/fetcher/hg.rs new file mode 100644 index 0000000..05561a5 --- /dev/null +++ b/nix-js/src/fetcher/hg.rs @@ -0,0 +1,196 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +use super::cache::FetcherCache; +use super::FetchHgResult; + +pub fn fetch_hg( + cache: &FetcherCache, + url: &str, + rev: Option<&str>, + name: &str, +) -> Result { + let bare_repo = cache.get_hg_bare(url); + + if !bare_repo.exists() { + clone_repo(url, &bare_repo)?; + } else { + pull_repo(&bare_repo)?; + } + + let target_rev = rev.unwrap_or("tip").to_string(); + let resolved_rev = resolve_rev(&bare_repo, &target_rev)?; + let branch = get_branch(&bare_repo, &resolved_rev)?; + + let checkout_dir = checkout_rev(&bare_repo, &resolved_rev, name, cache)?; + + let rev_count = get_rev_count(&bare_repo, &resolved_rev)?; + + let short_rev = if resolved_rev.len() >= 12 { + resolved_rev[..12].to_string() + } else { + resolved_rev.clone() + }; + + Ok(FetchHgResult { + out_path: checkout_dir.to_string_lossy().to_string(), + branch, + rev: resolved_rev, + short_rev, + rev_count, + }) +} + +fn clone_repo(url: &str, dest: &PathBuf) -> Result<(), HgError> { + fs::create_dir_all(dest.parent().unwrap_or(dest))?; + + let output = Command::new("hg") + .args(["clone", "-U", url]) + .arg(dest) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + return Err(HgError::CommandFailed { + operation: "clone".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) +} + +fn pull_repo(repo: &PathBuf) -> Result<(), HgError> { + let output = Command::new("hg") + .args(["pull"]) + .current_dir(repo) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + return Err(HgError::CommandFailed { + operation: "pull".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + Ok(()) +} + +fn resolve_rev(repo: &PathBuf, rev: &str) -> Result { + let output = Command::new("hg") + .args(["log", "-r", rev, "--template", "{node}"]) + .current_dir(repo) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + return Err(HgError::CommandFailed { + operation: "log".to_string(), + message: format!( + "Could not resolve rev '{}': {}", + rev, + String::from_utf8_lossy(&output.stderr) + ), + }); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn get_branch(repo: &PathBuf, rev: &str) -> Result { + let output = Command::new("hg") + .args(["log", "-r", rev, "--template", "{branch}"]) + .current_dir(repo) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + return Ok("default".to_string()); + } + + let branch = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if branch.is_empty() { + Ok("default".to_string()) + } else { + Ok(branch) + } +} + +fn checkout_rev( + bare_repo: &PathBuf, + rev: &str, + name: &str, + cache: &FetcherCache, +) -> Result { + let hash = crate::nix_hash::sha256_hex(&format!("{}:{}", bare_repo.display(), rev)); + let checkout_dir = cache.make_store_path(&hash, name); + + if checkout_dir.exists() { + return Ok(checkout_dir); + } + + fs::create_dir_all(&checkout_dir)?; + + let output = Command::new("hg") + .args(["archive", "-r", rev]) + .arg(&checkout_dir) + .current_dir(bare_repo) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + fs::remove_dir_all(&checkout_dir)?; + return Err(HgError::CommandFailed { + operation: "archive".to_string(), + message: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + let hg_archival = checkout_dir.join(".hg_archival.txt"); + if hg_archival.exists() { + fs::remove_file(&hg_archival)?; + } + + Ok(checkout_dir) +} + +fn get_rev_count(repo: &PathBuf, rev: &str) -> Result { + let output = Command::new("hg") + .args(["log", "-r", &format!("0::{}", rev), "--template", "x"]) + .current_dir(repo) + .env("HGPLAIN", "") + .output()?; + + if !output.status.success() { + return Ok(0); + } + + Ok(output.stdout.len() as u64) +} + +#[derive(Debug)] +pub enum HgError { + IoError(std::io::Error), + CommandFailed { operation: String, message: String }, +} + +impl std::fmt::Display for HgError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HgError::IoError(e) => write!(f, "I/O error: {}", e), + HgError::CommandFailed { operation, message } => { + write!(f, "Mercurial {} failed: {}", operation, message) + } + } + } +} + +impl std::error::Error for HgError {} + +impl From for HgError { + fn from(e: std::io::Error) -> Self { + HgError::IoError(e) + } +} diff --git a/nix-js/src/fetcher/mod.rs b/nix-js/src/fetcher/mod.rs new file mode 100644 index 0000000..b00bff4 --- /dev/null +++ b/nix-js/src/fetcher/mod.rs @@ -0,0 +1,240 @@ +mod archive; +mod cache; +mod download; +mod git; +mod hg; +mod nar; + +pub use cache::FetcherCache; +pub use download::Downloader; + +use std::path::PathBuf; + +use deno_core::op2; +use serde::Serialize; + +use crate::runtime::NixError; + +#[derive(Serialize)] +pub struct FetchUrlResult { + pub store_path: String, + pub hash: String, +} + +#[derive(Serialize)] +pub struct FetchTarballResult { + pub store_path: String, + pub hash: String, +} + +#[derive(Serialize)] +pub struct FetchGitResult { + pub out_path: String, + pub rev: String, + pub short_rev: String, + pub rev_count: u64, + pub last_modified: u64, + pub last_modified_date: String, + pub submodules: bool, + pub nar_hash: Option, +} + +#[derive(Serialize)] +pub struct FetchHgResult { + pub out_path: String, + pub branch: String, + pub rev: String, + pub short_rev: String, + pub rev_count: u64, +} + +#[op2] +#[serde] +pub fn op_fetch_url( + #[string] url: String, + #[string] expected_hash: Option, + #[string] name: Option, + executable: bool, +) -> Result { + let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let downloader = Downloader::new(); + + let file_name = name.unwrap_or_else(|| { + url.rsplit('/') + .next() + .unwrap_or("download") + .to_string() + }); + + if let Some(ref hash) = expected_hash { + if let Some(cached) = cache.get_url(&url, hash) { + return Ok(FetchUrlResult { + store_path: cached.to_string_lossy().to_string(), + hash: hash.clone(), + }); + } + } + + let data = downloader + .download(&url) + .map_err(|e| NixError::from(e.to_string()))?; + + let hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(&data)); + + if let Some(ref expected) = expected_hash { + let normalized_expected = normalize_hash(expected); + if hash != normalized_expected { + return Err(NixError::from(format!( + "hash mismatch for '{}': expected {}, got {}", + url, normalized_expected, hash + ))); + } + } + + let store_path = cache + .put_url(&url, &hash, &data, &file_name, executable) + .map_err(|e| NixError::from(e.to_string()))?; + + Ok(FetchUrlResult { + store_path: store_path.to_string_lossy().to_string(), + hash, + }) +} + +#[op2] +#[serde] +pub fn op_fetch_tarball( + #[string] url: String, + #[string] expected_hash: Option, + #[string] name: Option, +) -> Result { + let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let downloader = Downloader::new(); + + let dir_name = name.unwrap_or_else(|| "source".to_string()); + + let is_nar_hash = expected_hash + .as_ref() + .map(|h| h.starts_with("sha256-")) + .unwrap_or(false); + + if let Some(ref hash) = expected_hash { + let normalized = normalize_hash(hash); + if let Some(cached) = cache.get_tarball(&url, &normalized) { + return Ok(FetchTarballResult { + store_path: cached.to_string_lossy().to_string(), + hash: normalized, + }); + } + } + + let data = downloader + .download(&url) + .map_err(|e| NixError::from(e.to_string()))?; + + let temp_dir = tempfile::tempdir().map_err(|e| NixError::from(e.to_string()))?; + let extracted_path = archive::extract_archive(&data, &temp_dir.path().to_path_buf()) + .map_err(|e| NixError::from(e.to_string()))?; + + let nar_hash = nar::compute_nar_hash(&extracted_path) + .map_err(|e| NixError::from(e.to_string()))?; + + if let Some(ref expected) = expected_hash { + let normalized_expected = normalize_hash(expected); + let hash_to_compare = if is_nar_hash { &nar_hash } else { &nar_hash }; + + if *hash_to_compare != normalized_expected { + return Err(NixError::from(format!( + "hash mismatch for '{}': expected {}, got {}", + url, normalized_expected, hash_to_compare + ))); + } + } + + let store_path = cache + .put_tarball_from_extracted(&url, &nar_hash, &extracted_path, &dir_name) + .map_err(|e| NixError::from(e.to_string()))?; + + Ok(FetchTarballResult { + store_path: store_path.to_string_lossy().to_string(), + hash: nar_hash, + }) +} + +#[op2] +#[serde] +pub fn op_fetch_git( + #[string] url: String, + #[string] git_ref: Option, + #[string] rev: Option, + shallow: bool, + submodules: bool, + all_refs: bool, + #[string] name: Option, +) -> Result { + let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let dir_name = name.unwrap_or_else(|| "source".to_string()); + + git::fetch_git(&cache, &url, git_ref.as_deref(), rev.as_deref(), shallow, submodules, all_refs, &dir_name) + .map_err(|e| NixError::from(e.to_string())) +} + +#[op2] +#[serde] +pub fn op_fetch_hg( + #[string] url: String, + #[string] rev: Option, + #[string] name: Option, +) -> Result { + let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?; + let dir_name = name.unwrap_or_else(|| "source".to_string()); + + hg::fetch_hg(&cache, &url, rev.as_deref(), &dir_name) + .map_err(|e| NixError::from(e.to_string())) +} + +fn normalize_hash(hash: &str) -> String { + if hash.starts_with("sha256-") { + if let Some(b64) = hash.strip_prefix("sha256-") { + if let Ok(bytes) = base64_decode(b64) { + return hex::encode(bytes); + } + } + } + hash.to_string() +} + +fn base64_decode(input: &str) -> Result, String> { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + let input = input.trim_end_matches('='); + let mut output = Vec::with_capacity(input.len() * 3 / 4); + + let mut buffer = 0u32; + let mut bits = 0; + + for c in input.bytes() { + let value = ALPHABET.iter().position(|&x| x == c) + .ok_or_else(|| format!("Invalid base64 character: {}", c as char))?; + + buffer = (buffer << 6) | (value as u32); + bits += 6; + + if bits >= 8 { + bits -= 8; + output.push((buffer >> bits) as u8); + buffer &= (1 << bits) - 1; + } + } + + Ok(output) +} + +pub fn register_ops() -> Vec { + vec![ + op_fetch_url(), + op_fetch_tarball(), + op_fetch_git(), + op_fetch_hg(), + ] +} diff --git a/nix-js/src/fetcher/nar.rs b/nix-js/src/fetcher/nar.rs new file mode 100644 index 0000000..19009ef --- /dev/null +++ b/nix-js/src/fetcher/nar.rs @@ -0,0 +1,126 @@ +use sha2::{Digest, Sha256}; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +pub fn compute_nar_hash(path: &Path) -> Result { + let mut hasher = Sha256::new(); + dump_path(&mut hasher, path)?; + Ok(hex::encode(hasher.finalize())) +} + +fn dump_path(sink: &mut W, path: &Path) -> io::Result<()> { + write_string(sink, "nix-archive-1")?; + write_string(sink, "(")?; + dump_entry(sink, path)?; + write_string(sink, ")")?; + Ok(()) +} + +fn dump_entry(sink: &mut W, path: &Path) -> io::Result<()> { + let metadata = fs::symlink_metadata(path)?; + + if metadata.is_symlink() { + let target = fs::read_link(path)?; + write_string(sink, "type")?; + write_string(sink, "symlink")?; + write_string(sink, "target")?; + write_string(sink, &target.to_string_lossy())?; + } else if metadata.is_file() { + write_string(sink, "type")?; + write_string(sink, "regular")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if metadata.permissions().mode() & 0o111 != 0 { + write_string(sink, "executable")?; + write_string(sink, "")?; + } + } + + let contents = fs::read(path)?; + write_string(sink, "contents")?; + write_contents(sink, &contents)?; + } else if metadata.is_dir() { + write_string(sink, "type")?; + write_string(sink, "directory")?; + + let mut entries: Vec<_> = fs::read_dir(path)? + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .collect(); + entries.sort(); + + for name in entries { + write_string(sink, "entry")?; + write_string(sink, "(")?; + write_string(sink, "name")?; + write_string(sink, &name)?; + write_string(sink, "node")?; + write_string(sink, "(")?; + dump_entry(sink, &path.join(&name))?; + write_string(sink, ")")?; + write_string(sink, ")")?; + } + } + + Ok(()) +} + +fn write_string(sink: &mut W, s: &str) -> io::Result<()> { + let bytes = s.as_bytes(); + let len = bytes.len() as u64; + + sink.write_all(&len.to_le_bytes())?; + sink.write_all(bytes)?; + + let padding = (8 - (len % 8)) % 8; + for _ in 0..padding { + sink.write_all(&[0])?; + } + + Ok(()) +} + +fn write_contents(sink: &mut W, contents: &[u8]) -> io::Result<()> { + let len = contents.len() as u64; + + sink.write_all(&len.to_le_bytes())?; + sink.write_all(contents)?; + + let padding = (8 - (len % 8)) % 8; + for _ in 0..padding { + sink.write_all(&[0])?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_simple_file() { + let temp = TempDir::new().unwrap(); + let file_path = temp.path().join("test.txt"); + fs::write(&file_path, "hello").unwrap(); + + let hash = compute_nar_hash(&file_path).unwrap(); + assert!(!hash.is_empty()); + assert_eq!(hash.len(), 64); + } + + #[test] + fn test_directory() { + let temp = TempDir::new().unwrap(); + fs::write(temp.path().join("a.txt"), "aaa").unwrap(); + fs::write(temp.path().join("b.txt"), "bbb").unwrap(); + + let hash = compute_nar_hash(temp.path()).unwrap(); + assert!(!hash.is_empty()); + assert_eq!(hash.len(), 64); + } +} diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 62d4cc9..8682c61 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -134,7 +134,7 @@ pub fn downgrade_inherit( }; match stcs.entry(ident) { Entry::Occupied(occupied) => { - return Err(Error::eval_error(format!( + return Err(Error::downgrade_error(format!( "attribute '{}' already defined", format_symbol(ctx.get_sym(*occupied.key())) ))); diff --git a/nix-js/src/lib.rs b/nix-js/src/lib.rs index 7411391..f4d4fe2 100644 --- a/nix-js/src/lib.rs +++ b/nix-js/src/lib.rs @@ -3,6 +3,7 @@ mod codegen; pub mod context; pub mod error; +mod fetcher; mod ir; mod nix_hash; mod runtime; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 47bcacd..90c3df5 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -23,7 +23,7 @@ pub(crate) trait RuntimeCtx: 'static { fn runtime_extension() -> Extension { const ESM: &[ExtensionFileSource] = &deno_core::include_js_files!(nix_runtime dir "runtime-ts/dist", "runtime.js"); - let ops = vec![ + let mut ops = vec![ op_import::(), op_read_file(), op_path_exists(), @@ -33,6 +33,7 @@ fn runtime_extension() -> Extension { op_output_path_name(), op_make_fixed_output_path(), ]; + ops.extend(crate::fetcher::register_ops()); Extension { name: "nix_runtime", @@ -69,7 +70,7 @@ mod private { } } } -use private::NixError; +pub(crate) use private::NixError; #[deno_core::op2] #[string] @@ -220,7 +221,7 @@ impl Runtime { let global_value = self .js_runtime .execute_script("", script) - .map_err(|e| Error::eval_error(format!("{}", e.get_message())))?; + .map_err(|e| Error::eval_error(format!("{}", e.get_message()), e.stack))?; // Retrieve scope from JsRuntime deno_core::scope!(scope, self.js_runtime);