diff --git a/.lazy.lua b/.lazy.lua index e72947b..db04e7e 100644 --- a/.lazy.lua +++ b/.lazy.lua @@ -3,16 +3,5 @@ vim.lsp.config("biome", { on_dir(vim.fn.getcwd()) end }) -vim.lsp.config("rust_analyzer", { - settings = { - ["rust-analyzer"] = { - cargo = { - features = { - "daemon" - } - } - } - } -}) return {} diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 4f8bd61..10cac34 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -4,14 +4,10 @@ version = "0.1.0" edition = "2024" build = "build.rs" -[features] -default = ["daemon"] -daemon = ["dep:tokio"] - [dependencies] mimalloc = "0.1" -tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"], optional = true } +tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"] } nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] } # REPL diff --git a/nix-js/runtime-ts/src/builtins/derivation.ts b/nix-js/runtime-ts/src/builtins/derivation.ts index 7f5ad52..6be5659 100644 --- a/nix-js/runtime-ts/src/builtins/derivation.ts +++ b/nix-js/runtime-ts/src/builtins/derivation.ts @@ -1,5 +1,5 @@ import type { NixValue, NixAttrs } from "../types"; -import { forceStringValue, forceList } from "../type-assert"; +import { forceStringValue, forceList, forceStringNoCtx } from "../type-assert"; import { force, createThunk } from "../thunk"; import { type DerivationData, @@ -241,14 +241,13 @@ const extractFixedOutputInfo = (attrs: NixAttrs, ignoreNulls: boolean): FixedOut if (ignoreNulls && hashValue === null) { return null; } - const hashRaw = forceStringValue(attrs.outputHash); + const hashRaw = forceStringNoCtx(attrs.outputHash); - // FIXME: default value? - let hashAlgo = "sha256"; + let hashAlgo = null; if ("outputHashAlgo" in attrs) { const algoValue = force(attrs.outputHashAlgo); if (!(ignoreNulls && algoValue === null)) { - hashAlgo = forceStringValue(attrs.outputHashAlgo); + hashAlgo = forceStringNoCtx(attrs.outputHashAlgo); } } diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 075e023..176558b 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -18,7 +18,7 @@ import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion"; import { getPathValue } from "../path"; import type { NixStringContext, StringWithContext } from "../string-context"; import { mkStringWithContext } from "../string-context"; -import { isPath } from "./type-check"; +import { isAttrs, isPath } from "./type-check"; const importCache = new Map(); @@ -139,22 +139,19 @@ const normalizeUrlInput = ( const normalizeTarballInput = ( args: NixValue, -): { url: string; hash?: string; narHash?: string; name?: string } => { +): { url: string; sha256?: string; name?: string } => { const forced = force(args); - if (typeof forced === "string") { - return { url: forced }; - } - const attrs = forceAttrs(args); - const url = forceStringValue(attrs.url); - const hash = "hash" in attrs ? forceStringValue(attrs.hash) : undefined; - const narHash = - "narHash" in attrs - ? forceStringValue(attrs.narHash) - : "sha256" in attrs - ? forceStringValue(attrs.sha256) + if (isAttrs(forced)) { + const url = forceStringNoCtx(forced.url); + const sha256 = + "sha256" in forced + ? forceStringNoCtx(forced.sha256) : undefined; - const name = "name" in attrs ? forceStringValue(attrs.name) : undefined; - return { url, hash, narHash, name }; + const name = "name" in forced ? forceStringNoCtx(forced.name) : undefined; + return { url, sha256, name }; + } else { + return { url: forceStringNoCtx(forced) }; + } }; export const fetchurl = (args: NixValue): string => { @@ -169,12 +166,11 @@ export const fetchurl = (args: NixValue): string => { }; export const fetchTarball = (args: NixValue): string => { - const { url, hash, narHash, name } = normalizeTarballInput(args); + const { url, name, sha256 } = normalizeTarballInput(args); const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball( url, - hash ?? null, - narHash ?? null, name ?? null, + sha256 ?? null, ); return result.store_path; }; diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index c88cac4..2faa4af 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -50,9 +50,8 @@ declare global { column: number | null; }; function op_make_store_path(ty: string, hash_hex: string, name: string): string; - function op_make_text_store_path(hash_hex: string, name: string, references: string[]): string; function op_output_path_name(drv_name: string, output_name: string): string; - function op_parse_hash(hash_str: string, algo: string): { hex: string; algo: string }; + function op_parse_hash(hash_str: string, algo: string | null): { hex: string; algo: string }; function op_make_fixed_output_path( hash_algo: string, hash: string, @@ -67,9 +66,8 @@ declare global { ): FetchUrlResult; function op_fetch_tarball( url: string, - expected_hash: string | null, - expected_nar_hash: string | null, name: string | null, + sha256: string | null, ): FetchTarballResult; function op_fetch_git( url: string, diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 2112afc..c7a4b7f 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -14,7 +14,7 @@ use crate::ir::{ ToIr as _, }; use crate::runtime::{Runtime, RuntimeContext}; -use crate::store::{Store, StoreBackend, StoreConfig}; +use crate::store::{DaemonStore, Store, StoreConfig}; use crate::value::{Symbol, Value}; pub struct Context { @@ -93,7 +93,7 @@ pub(crate) struct Ctx { symbols: DefaultStringInterner, global: NonNull>, sources: Vec, - store: StoreBackend, + store: DaemonStore, } impl Ctx { @@ -182,7 +182,7 @@ impl Ctx { } let config = StoreConfig::from_env(); - let store = StoreBackend::new(config)?; + let store = DaemonStore::connect(&config.daemon_socket)?; Ok(Self { symbols, @@ -296,7 +296,7 @@ impl CodegenContext for Ctx { self.sources.last().expect("current_source not set").clone() } fn get_store_dir(&self) -> &str { - self.store.as_store().get_store_dir() + self.store.get_store_dir() } } @@ -316,8 +316,8 @@ impl RuntimeContext for Ctx { fn get_source(&self, id: usize) -> Source { self.get_source(id) } - fn get_store(&self) -> &dyn Store { - self.store.as_store() + fn get_store(&self) -> &DaemonStore { + &self.store } } diff --git a/nix-js/src/fetcher.rs b/nix-js/src/fetcher.rs index 345c324..afe0699 100644 --- a/nix-js/src/fetcher.rs +++ b/nix-js/src/fetcher.rs @@ -1,10 +1,13 @@ use deno_core::OpState; use deno_core::op2; +use nix_compat::nixhash::HashAlgo; +use nix_compat::nixhash::NixHash; use serde::Serialize; use tracing::{debug, info, warn}; use crate::runtime::OpStateExt; use crate::runtime::RuntimeContext; +use crate::store::Store as _; mod archive; pub(crate) mod cache; @@ -117,7 +120,7 @@ pub fn op_fetch_url( info!(bytes = data.len(), "Download complete"); - let hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(&data)); + let hash = crate::nix_utils::sha256_hex(&String::from_utf8_lossy(&data)); if let Some(ref expected) = expected_hash { let normalized_expected = normalize_hash(expected); @@ -164,9 +167,8 @@ pub fn op_fetch_url( pub fn op_fetch_tarball( state: &mut OpState, #[string] url: String, - #[string] expected_hash: Option, - #[string] expected_nar_hash: Option, #[string] name: Option, + #[string] sha256: Option, ) -> Result { let _span = tracing::info_span!("op_fetch_tarball", url = %url).entered(); info!("fetchTarball started"); @@ -181,6 +183,16 @@ pub fn op_fetch_tarball( "name": dir_name, }); + let expected_sha256 = sha256 + .map( + |ref sha256| match NixHash::from_str(sha256, Some(HashAlgo::Sha256)) { + Ok(NixHash::Sha256(digest)) => Ok(digest), + _ => Err(format!("fetchTarball: invalid sha256 '{sha256}'")), + }, + ) + .transpose()?; + let expected_hex = expected_sha256.map(hex::encode); + if let Some(cached_entry) = metadata_cache .lookup(&input) .map_err(|e| NixRuntimeError::from(e.to_string()))? @@ -196,9 +208,8 @@ pub fn op_fetch_tarball( .and_then(|v| v.as_str()) .unwrap_or(""); - if let Some(ref expected_nar) = expected_nar_hash { - let normalized_expected = normalize_hash(expected_nar); - if cached_nar_hash == normalized_expected { + if let Some(ref hex) = expected_hex { + if cached_nar_hash == hex { info!("Cache hit"); return Ok(FetchTarballResult { store_path: cached_entry.store_path.clone(), @@ -224,16 +235,15 @@ pub fn op_fetch_tarball( info!(bytes = data.len(), "Download complete"); - let tarball_hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(&data)); + let tarball_hash = crate::nix_utils::sha256_hex(&String::from_utf8_lossy(&data)); - if let Some(ref expected) = expected_hash { - let normalized_expected = normalize_hash(expected); - if tarball_hash != normalized_expected { - return Err(NixRuntimeError::from(format!( - "Tarball hash mismatch for '{}': expected {}, got {}", - url, normalized_expected, tarball_hash - ))); - } + if let Some(ref expected) = expected_hex + && tarball_hash != *expected + { + return Err(NixRuntimeError::from(format!( + "Tarball hash mismatch for '{}': expected {}, got {}", + url, expected, tarball_hash + ))); } info!("Extracting tarball"); @@ -245,22 +255,23 @@ pub fn op_fetch_tarball( info!("Computing NAR hash"); let nar_hash = nar::compute_nar_hash(&extracted_path).map_err(|e| NixRuntimeError::from(e.to_string()))?; + let nar_hash_hex = hex::encode(nar_hash); debug!( tarball_hash = %tarball_hash, - nar_hash = %nar_hash, + nar_hash = %nar_hash_hex, "Hash computation complete" ); - if let Some(ref expected) = expected_nar_hash { - let normalized_expected = normalize_hash(expected); - if nar_hash != normalized_expected { + if let Some(ref expected) = expected_sha256 + && nar_hash != *expected { return Err(NixRuntimeError::from(format!( "NAR hash mismatch for '{}': expected {}, got {}", - url, normalized_expected, nar_hash + url, + expected_hex.expect("must be Some"), + nar_hash_hex ))); } - } info!("Adding to store"); let ctx: &Ctx = state.get_ctx(); @@ -277,7 +288,7 @@ pub fn op_fetch_tarball( "url": url, }); - let immutable = expected_nar_hash.is_some(); + let immutable = expected_sha256.is_some(); metadata_cache .add(&input, &info, &store_path, immutable) .map_err(|e| NixRuntimeError::from(e.to_string()))?; @@ -285,7 +296,7 @@ pub fn op_fetch_tarball( Ok(FetchTarballResult { store_path, hash: tarball_hash, - nar_hash, + nar_hash: nar_hash_hex, }) } diff --git a/nix-js/src/fetcher/cache.rs b/nix-js/src/fetcher/cache.rs index 85c5e11..30567d6 100644 --- a/nix-js/src/fetcher/cache.rs +++ b/nix-js/src/fetcher/cache.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use super::archive::ArchiveError; @@ -65,7 +65,7 @@ impl FetcherCache { } fn hash_key(url: &str) -> String { - crate::nix_hash::sha256_hex(url) + crate::nix_utils::sha256_hex(url) } pub fn get_git_bare(&self, url: &str) -> PathBuf { @@ -87,21 +87,3 @@ impl FetcherCache { Ok((extracted_path, temp_dir)) } } - -pub fn copy_dir_recursive(src: &Path, dst: &Path) -> 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/git.rs b/nix-js/src/fetcher/git.rs index 5982947..c5be5a2 100644 --- a/nix-js/src/fetcher/git.rs +++ b/nix-js/src/fetcher/git.rs @@ -31,8 +31,8 @@ pub fn fetch_git( let temp_dir = tempfile::tempdir()?; let checkout_dir = checkout_rev_to_temp(&bare_repo, &target_rev, submodules, temp_dir.path())?; - let nar_hash = crate::nar::compute_nar_hash(&checkout_dir) - .map_err(|e| GitError::NarHashError(e.to_string()))?; + let nar_hash = hex::encode(crate::nar::compute_nar_hash(&checkout_dir) + .map_err(|e| GitError::NarHashError(e.to_string()))?); let store_path = store .add_to_store_from_path(name, &checkout_dir, vec![]) diff --git a/nix-js/src/fetcher/hg.rs b/nix-js/src/fetcher/hg.rs index aa7896c..711a401 100644 --- a/nix-js/src/fetcher/hg.rs +++ b/nix-js/src/fetcher/hg.rs @@ -124,7 +124,7 @@ fn checkout_rev( name: &str, cache: &FetcherCache, ) -> Result { - let hash = crate::nix_hash::sha256_hex(&format!("{}:{}", bare_repo.display(), rev)); + let hash = crate::nix_utils::sha256_hex(&format!("{}:{}", bare_repo.display(), rev)); let checkout_dir = cache.make_store_path(&hash, name); if checkout_dir.exists() { diff --git a/nix-js/src/lib.rs b/nix-js/src/lib.rs index c627c50..0f00160 100644 --- a/nix-js/src/lib.rs +++ b/nix-js/src/lib.rs @@ -10,9 +10,9 @@ mod downgrade; mod fetcher; mod ir; mod nar; -mod nix_hash; mod runtime; mod store; +mod nix_utils; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; diff --git a/nix-js/src/nar.rs b/nix-js/src/nar.rs index ab51867..0f93242 100644 --- a/nix-js/src/nar.rs +++ b/nix-js/src/nar.rs @@ -5,14 +5,14 @@ use std::path::Path; use crate::error::{Error, Result}; -pub fn compute_nar_hash(path: &Path) -> Result { +pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> { let mut hasher = Sha256::new(); std::io::copy( &mut Encoder::new(path).map_err(|err| Error::internal(err.to_string()))?, &mut hasher, ) .map_err(|err| Error::internal(err.to_string()))?; - Ok(hex::encode(hasher.finalize())) + Ok(hasher.finalize().into()) } pub fn pack_nar(path: &Path) -> Result> { @@ -37,7 +37,7 @@ mod tests { let file_path = temp.path().join("test.txt"); fs::write(&file_path, "hello").unwrap(); - let hash = compute_nar_hash(&file_path).unwrap(); + let hash = hex::encode(compute_nar_hash(&file_path).unwrap()); assert_eq!( hash, "0a430879c266f8b57f4092a0f935cf3facd48bbccde5760d4748ca405171e969" @@ -52,7 +52,7 @@ mod tests { 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(); + let hash = hex::encode(compute_nar_hash(temp.path()).unwrap()); assert_eq!( hash, "0036c14209749bc9b9631e2077b108b701c322ab53853cd26f2746268a86fc0f" diff --git a/nix-js/src/nix_hash.rs b/nix-js/src/nix_hash.rs deleted file mode 100644 index ec3cff8..0000000 --- a/nix-js/src/nix_hash.rs +++ /dev/null @@ -1,175 +0,0 @@ -use sha2::{Digest, Sha256}; - -const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz"; - -pub fn sha256_hex(data: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(data.as_bytes()); - hex::encode(hasher.finalize()) -} - -pub fn compress_hash(hash: &[u8; 32], new_size: usize) -> Vec { - let mut result = vec![0u8; new_size]; - for i in 0..32 { - result[i % new_size] ^= hash[i]; - } - result -} - -pub fn nix_base32_encode(bytes: &[u8]) -> String { - let len = (bytes.len() * 8 - 1) / 5 + 1; - let mut result = String::with_capacity(len); - - for n in (0..len).rev() { - let b = n * 5; - let i = b / 8; - let j = b % 8; - - let c = if i >= bytes.len() { - 0 - } else { - let mut c = (bytes[i] as u16) >> j; - if j > 3 && i + 1 < bytes.len() { - c |= (bytes[i + 1] as u16) << (8 - j); - } - c - }; - - result.push(NIX_BASE32_CHARS[(c & 0x1f) as usize] as char); - } - - result -} - -pub fn nix_base32_decode(input: &str) -> Option> { - let len = input.len() * 5 / 8; - let mut bytes = vec![0u8; len]; - - for (n, ch) in input.chars().rev().enumerate() { - let digit = NIX_BASE32_CHARS.iter().position(|&c| c == ch as u8)? as u16; - let b = n * 5; - let i = b / 8; - let j = b % 8; - if i < len { - bytes[i] |= (digit << j) as u8; - } - if j > 3 && i + 1 < len { - bytes[i + 1] |= (digit >> (8 - j)) as u8; - } - } - - Some(bytes) -} - -pub fn decode_hash_to_hex(hash_str: &str) -> Option { - if let Some(rest) = hash_str.strip_prefix("sha256:") { - return decode_hash_to_hex(rest); - } - if let Some(base64_str) = hash_str.strip_prefix("sha256-") { - use base64::{Engine, engine::general_purpose::STANDARD}; - let bytes = STANDARD.decode(base64_str).ok()?; - return Some(hex::encode(bytes)); - } - if hash_str.len() == 64 && hash_str.chars().all(|c| c.is_ascii_hexdigit()) { - return Some(hash_str.to_string()); - } - if hash_str.len() == 52 { - let bytes = nix_base32_decode(hash_str)?; - return Some(hex::encode(bytes)); - } - None -} - -pub fn make_store_path(store_dir: &str, ty: &str, hash_hex: &str, name: &str) -> String { - let s = format!("{}:sha256:{}:{}:{}", ty, hash_hex, store_dir, name); - - let mut hasher = Sha256::new(); - hasher.update(s.as_bytes()); - let hash: [u8; 32] = hasher.finalize().into(); - - let compressed = compress_hash(&hash, 20); - let encoded = nix_base32_encode(&compressed); - - format!("{}/{}-{}", store_dir, encoded, name) -} - -pub fn make_text_store_path( - store_dir: &str, - hash_hex: &str, - name: &str, - references: &[String], -) -> String { - let mut ty = String::from("text"); - for reference in references { - ty.push(':'); - ty.push_str(reference); - } - make_store_path(store_dir, &ty, hash_hex, name) -} - -pub fn output_path_name(drv_name: &str, output_name: &str) -> String { - if output_name == "out" { - drv_name.to_string() - } else { - format!("{}-{}", drv_name, output_name) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_nix_base32_encode() { - let bytes = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; - let encoded = nix_base32_encode(&bytes); - assert_eq!(encoded.len(), 8); - - let bytes_zero = [0u8; 20]; - let encoded_zero = nix_base32_encode(&bytes_zero); - assert_eq!(encoded_zero.len(), 32); - assert!(encoded_zero.chars().all(|c| c == '0')); - } - - #[test] - fn test_compress_hash() { - let hash = [0u8; 32]; - let compressed = compress_hash(&hash, 20); - assert_eq!(compressed.len(), 20); - assert!(compressed.iter().all(|&b| b == 0)); - - let hash_ones = [0xFF; 32]; - let compressed_ones = compress_hash(&hash_ones, 20); - assert_eq!(compressed_ones.len(), 20); - } - - #[test] - fn test_sha256_hex() { - let data = "hello world"; - let hash = sha256_hex(data); - assert_eq!(hash.len(), 64); - assert_eq!( - hash, - "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - ); - } - - #[test] - fn test_output_path_name() { - assert_eq!(output_path_name("hello", "out"), "hello"); - assert_eq!(output_path_name("hello", "dev"), "hello-dev"); - assert_eq!(output_path_name("hello", "doc"), "hello-doc"); - } - - #[test] - fn test_make_store_path() { - let path = make_store_path("/nix/store", "output:out", "abc123", "hello"); - assert!(path.starts_with("/nix/store/")); - assert!(path.ends_with("-hello")); - - let hash_parts: Vec<&str> = path.split('/').collect(); - assert_eq!(hash_parts.len(), 4); - let name_part = hash_parts[3]; - assert!(name_part.contains('-')); - } -} diff --git a/nix-js/src/nix_utils.rs b/nix-js/src/nix_utils.rs new file mode 100644 index 0000000..64e544d --- /dev/null +++ b/nix-js/src/nix_utils.rs @@ -0,0 +1,29 @@ +use nix_compat::store_path::compress_hash; +use sha2::{Digest as _, Sha256}; + +pub fn sha256_hex(data: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(data.as_bytes()); + hex::encode(hasher.finalize()) +} + +pub fn make_store_path(store_dir: &str, ty: &str, hash_hex: &str, name: &str) -> String { + let s = format!("{}:sha256:{}:{}:{}", ty, hash_hex, store_dir, name); + + let mut hasher = Sha256::new(); + hasher.update(s.as_bytes()); + let hash: [u8; 32] = hasher.finalize().into(); + + let compressed = compress_hash::<20>(&hash); + let encoded = nix_compat::nixbase32::encode(&compressed); + + format!("{}/{}-{}", store_dir, encoded, name) +} + +pub fn output_path_name(drv_name: &str, output_name: &str) -> String { + if output_name == "out" { + drv_name.to_string() + } else { + format!("{}-{}", drv_name, output_name) + } +} diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 2102256..c133227 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -5,7 +5,7 @@ use std::path::Path; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use crate::error::{Error, Result, Source}; -use crate::store::Store; +use crate::store::DaemonStore; use crate::value::{AttrSet, List, Symbol, Value}; mod ops; @@ -21,7 +21,7 @@ pub(crate) trait RuntimeContext: 'static { fn compile(&mut self, source: Source) -> Result; fn compile_scoped(&mut self, source: Source, scope: Vec) -> Result; fn get_source(&self, id: usize) -> Source; - fn get_store(&self) -> &dyn Store; + fn get_store(&self) -> &DaemonStore; } pub(crate) trait OpStateExt { @@ -55,7 +55,6 @@ fn runtime_extension() -> Extension { op_make_placeholder(), op_decode_span::(), op_make_store_path::(), - op_make_text_store_path::(), op_output_path_name(), op_parse_hash(), op_make_fixed_output_path::(), diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index b8f3abc..e4fa9cb 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -8,9 +8,9 @@ use deno_core::OpState; use regex::Regex; use rust_embed::Embed; -use crate::error::Source; - use super::{NixRuntimeError, OpStateExt, RuntimeContext}; +use crate::error::Source; +use crate::store::Store as _; #[derive(Debug, Default)] pub(super) struct RegexCache { @@ -247,7 +247,7 @@ pub(super) fn op_resolve_path( #[deno_core::op2] #[string] pub(super) fn op_sha256_hex(#[string] data: String) -> String { - crate::nix_hash::sha256_hex(&data) + crate::nix_utils::sha256_hex(&data) } #[deno_core::op2] @@ -323,21 +323,7 @@ pub(super) fn op_make_store_path( let ctx: &Ctx = state.get_ctx(); let store = ctx.get_store(); let store_dir = store.get_store_dir(); - crate::nix_hash::make_store_path(store_dir, &ty, &hash_hex, &name) -} - -#[deno_core::op2] -#[string] -pub(super) fn op_make_text_store_path( - state: &mut OpState, - #[string] hash_hex: String, - #[string] name: String, - #[serde] references: Vec, -) -> String { - let ctx: &Ctx = state.get_ctx(); - let store = ctx.get_store(); - let store_dir = store.get_store_dir(); - crate::nix_hash::make_text_store_path(store_dir, &hash_hex, &name, &references) + crate::nix_utils::make_store_path(store_dir, &ty, &hash_hex, &name) } #[deno_core::op2] @@ -346,7 +332,7 @@ pub(super) fn op_output_path_name( #[string] drv_name: String, #[string] output_name: String, ) -> String { - crate::nix_hash::output_path_name(&drv_name, &output_name) + crate::nix_utils::output_path_name(&drv_name, &output_name) } #[derive(serde::Serialize)] @@ -359,21 +345,26 @@ pub(super) struct ParsedHash { #[serde] pub(super) fn op_parse_hash( #[string] hash_str: String, - #[string] algo: String, + #[string] algo: Option, ) -> std::result::Result { use nix_compat::nixhash::{HashAlgo, NixHash}; - let hash_algo = HashAlgo::from_str(&algo).ok(); - let nix_hash = NixHash::from_str(&hash_str, hash_algo).map_err(|e| { + let hash_algo = algo + .as_deref() + .and_then(|algo| HashAlgo::from_str(algo).ok()); + + let hash = NixHash::from_str(&hash_str, hash_algo).map_err(|e| { NixRuntimeError::from(format!( - "invalid hash '{}' for algorithm '{}': {}", - hash_str, algo, e + "invalid hash '{}'{}: {}", + hash_str, + algo.map_or("".to_string(), |algo| format!(" for algorithm '{algo}'")), + e )) })?; Ok(ParsedHash { - hex: hex::encode(nix_hash.digest_as_bytes()), - algo, + hex: hex::encode(hash.digest_as_bytes()), + algo: hash.algo().to_string(), }) } @@ -393,7 +384,7 @@ pub(super) fn op_make_fixed_output_path( let store_dir = store.get_store_dir(); if hash_algo == "sha256" && hash_mode == "recursive" { - crate::nix_hash::make_store_path(store_dir, "source", &hash, &name) + crate::nix_utils::make_store_path(store_dir, "source", &hash, &name) } else { let prefix = if hash_mode == "recursive" { "r:" } else { "" }; let inner_input = format!("fixed:out:{}{}:{}:", prefix, hash_algo, hash); @@ -401,7 +392,7 @@ pub(super) fn op_make_fixed_output_path( hasher.update(inner_input.as_bytes()); let inner_hash = hex::encode(hasher.finalize()); - crate::nix_hash::make_store_path(store_dir, "output:out", &inner_hash, &name) + crate::nix_utils::make_store_path(store_dir, "output:out", &inner_hash, &name) } } @@ -414,6 +405,7 @@ pub(super) fn op_add_path( recursive: bool, #[string] sha256: Option, ) -> std::result::Result { + use nix_compat::nixhash::{HashAlgo, NixHash}; use sha2::{Digest, Sha256}; use std::fs; use std::path::Path; @@ -449,18 +441,19 @@ pub(super) fn op_add_path( let mut hasher = Sha256::new(); hasher.update(&contents); - hex::encode(hasher.finalize()) + hasher.finalize().into() }; - if let Some(expected_hash) = sha256 { + if let Some(ref expected_hash) = sha256 { let expected_hex = - crate::nix_hash::decode_hash_to_hex(&expected_hash).ok_or_else(|| { - NixRuntimeError::from(format!("invalid hash format: {}", expected_hash)) - })?; - if computed_hash != expected_hex { + NixHash::from_str(expected_hash, Some(HashAlgo::Sha256)) + .map_err(|err| err.to_string())?; + if computed_hash != expected_hex.digest_as_bytes() { return Err(NixRuntimeError::from(format!( "hash mismatch for path '{}': expected {}, got {}", - path, expected_hex, computed_hash + path, + hex::encode(expected_hex.digest_as_bytes()), + hex::encode(computed_hash) ))); } } @@ -548,8 +541,9 @@ pub(super) fn op_write_derivation( pub(super) fn op_read_derivation_outputs( #[string] drv_path: String, ) -> std::result::Result, NixRuntimeError> { - let content = std::fs::read_to_string(&drv_path) - .map_err(|e| NixRuntimeError::from(format!("failed to read derivation {}: {}", drv_path, e)))?; + let content = std::fs::read_to_string(&drv_path).map_err(|e| { + NixRuntimeError::from(format!("failed to read derivation {}: {}", drv_path, e)) + })?; let outputs = parse_derivation_outputs(&content) .ok_or_else(|| NixRuntimeError::from(format!("failed to parse derivation {}", drv_path)))?; @@ -892,6 +886,7 @@ pub(super) fn op_add_filtered_path( #[string] sha256: Option, #[serde] include_paths: Vec, ) -> std::result::Result { + use nix_compat::nixhash::{HashAlgo, NixHash}; use sha2::{Digest, Sha256}; use std::fs; @@ -961,18 +956,19 @@ pub(super) fn op_add_filtered_path( .map_err(|e| NixRuntimeError::from(format!("failed to read file: {}", e)))?; let mut hasher = Sha256::new(); hasher.update(&contents); - hex::encode(hasher.finalize()) + hasher.finalize().into() }; - if let Some(expected_hash) = sha256 { + if let Some(ref expected_hash) = sha256 { let expected_hex = - crate::nix_hash::decode_hash_to_hex(&expected_hash).ok_or_else(|| { - NixRuntimeError::from(format!("invalid hash format: {}", expected_hash)) - })?; - if computed_hash != expected_hex { + NixHash::from_str(expected_hash, Some(HashAlgo::Sha256)) + .map_err(|err| err.to_string())?; + if computed_hash != expected_hex.digest_as_bytes() { return Err(NixRuntimeError::from(format!( "hash mismatch for path '{}': expected {}, got {}", - src_path, expected_hex, computed_hash + src_path, + hex::encode(expected_hex.digest_as_bytes()), + hex::encode(computed_hash) ))); } } diff --git a/nix-js/src/store.rs b/nix-js/src/store.rs index 3b90899..f2ed51c 100644 --- a/nix-js/src/store.rs +++ b/nix-js/src/store.rs @@ -1,14 +1,16 @@ #![allow(dead_code)] +use crate::error::Result; + mod config; +mod daemon; mod error; mod validation; -pub use config::{StoreConfig, StoreMode}; +pub use config::StoreConfig; +pub use daemon::DaemonStore; pub use validation::validate_store_path; -use crate::error::Result; - pub trait Store: Send + Sync { fn get_store_dir(&self) -> &str; @@ -46,66 +48,3 @@ pub trait Store: Send + Sync { name: &str, ) -> Result; } - -pub enum StoreBackend { - Simulated(SimulatedStore), - #[cfg(feature = "daemon")] - Daemon(Box), -} - -impl StoreBackend { - pub fn new(config: StoreConfig) -> Result { - match config.mode { - #[cfg(feature = "daemon")] - StoreMode::Daemon => { - let daemon = Box::new(DaemonStore::connect(&config.daemon_socket)?); - Ok(StoreBackend::Daemon(daemon)) - } - #[cfg(not(feature = "daemon"))] - StoreMode::Daemon => { - tracing::warn!( - "Daemon mode not available (nix-js not compiled with 'daemon' feature), falling back to simulated store" - ); - let simulated = SimulatedStore::new()?; - Ok(StoreBackend::Simulated(simulated)) - } - StoreMode::Simulated => { - let simulated = SimulatedStore::new()?; - Ok(StoreBackend::Simulated(simulated)) - } - #[cfg(feature = "daemon")] - StoreMode::Auto => match DaemonStore::connect(&config.daemon_socket) { - Ok(daemon) => { - tracing::debug!("Using nix-daemon at {}", config.daemon_socket.display()); - Ok(StoreBackend::Daemon(Box::new(daemon))) - } - Err(e) => { - tracing::warn!("Daemon unavailable ({}), using simulated store", e); - let simulated = SimulatedStore::new()?; - Ok(StoreBackend::Simulated(simulated)) - } - }, - #[cfg(not(feature = "daemon"))] - StoreMode::Auto => { - let simulated = SimulatedStore::new()?; - Ok(StoreBackend::Simulated(simulated)) - } - } - } - - pub fn as_store(&self) -> &dyn Store { - match self { - StoreBackend::Simulated(s) => s, - #[cfg(feature = "daemon")] - StoreBackend::Daemon(d) => d.as_ref(), - } - } -} - -mod simulated; -pub use simulated::SimulatedStore; - -#[cfg(feature = "daemon")] -mod daemon; -#[cfg(feature = "daemon")] -pub use daemon::DaemonStore; diff --git a/nix-js/src/store/config.rs b/nix-js/src/store/config.rs index e4f674f..059d694 100644 --- a/nix-js/src/store/config.rs +++ b/nix-js/src/store/config.rs @@ -1,40 +1,17 @@ use std::path::PathBuf; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StoreMode { - Daemon, - Simulated, - Auto, -} - #[derive(Debug, Clone)] pub struct StoreConfig { - pub mode: StoreMode, pub daemon_socket: PathBuf, } impl StoreConfig { pub fn from_env() -> Self { - let mode = match std::env::var("NIX_JS_STORE_MODE") - .as_deref() - .map(|s| s.to_lowercase()) - .as_deref() - { - Ok("daemon") => StoreMode::Daemon, - Ok("simulated") => StoreMode::Simulated, - Ok("auto") | Err(_) => StoreMode::Auto, - Ok(other) => { - tracing::warn!("Invalid NIX_JS_STORE_MODE '{}', using 'auto'", other); - StoreMode::Auto - } - }; - let daemon_socket = std::env::var("NIX_DAEMON_SOCKET") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/nix/var/nix/daemon-socket/socket")); Self { - mode, daemon_socket, } } diff --git a/nix-js/src/store/daemon.rs b/nix-js/src/store/daemon.rs index 5aecbbf..0cde362 100644 --- a/nix-js/src/store/daemon.rs +++ b/nix-js/src/store/daemon.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult}; use std::path::Path; @@ -16,7 +14,6 @@ use tokio::sync::Mutex; use thiserror::Error; use crate::error::{Error, Result}; - use super::Store; pub struct DaemonStore { @@ -510,19 +507,6 @@ impl NixDaemonClient { }) } - async fn execute(&mut self, operation: Operation, write: F1, read: F2) -> IoResult - where - T: nix_compat::wire::de::NixDeserialize, - F1: FnOnce() -> IoResult<()>, - F2: FnOnce() -> IoResult - { - // Send operation - self.writer.write_value(&operation).await?; - self.writer.flush().await?; - - self.read_response().await - } - /// Execute an operation with a single parameter async fn execute_with(&mut self, operation: Operation, param: &P) -> IoResult where diff --git a/nix-js/src/store/simulated.rs b/nix-js/src/store/simulated.rs deleted file mode 100644 index 8c2cfb5..0000000 --- a/nix-js/src/store/simulated.rs +++ /dev/null @@ -1,121 +0,0 @@ -use super::Store; -use crate::error::{Error, Result}; -use crate::fetcher::cache::FetcherCache; -use std::fs; -use std::path::Path; - -pub struct SimulatedStore { - cache: FetcherCache, - store_dir: String, -} - -impl SimulatedStore { - pub fn new() -> Result { - let cache = FetcherCache::new() - .map_err(|e| Error::internal(format!("Failed to create simulated store: {}", e)))?; - - let store_dir = dirs::cache_dir() - .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) - .join("nix-js") - .join("fetchers") - .join("store") - .to_string_lossy() - .to_string(); - - Ok(Self { cache, store_dir }) - } -} - -impl Store for SimulatedStore { - fn get_store_dir(&self) -> &str { - &self.store_dir - } - - fn is_valid_path(&self, path: &str) -> Result { - Ok(Path::new(path).exists()) - } - - fn ensure_path(&self, path: &str) -> Result<()> { - if !Path::new(path).exists() { - return Err(Error::eval_error( - format!( - "builtins.storePath: path '{}' does not exist in the simulated store", - path - ), - None, - )); - } - Ok(()) - } - - fn add_to_store( - &self, - name: &str, - content: &[u8], - _recursive: bool, - _references: Vec, - ) -> Result { - let hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(content)); - - let store_path = self.cache.make_store_path(&hash, name); - - if !store_path.exists() { - fs::create_dir_all(store_path.parent().unwrap_or(&store_path)) - .map_err(|e| Error::internal(format!("Failed to create store directory: {}", e)))?; - - fs::write(&store_path, content) - .map_err(|e| Error::internal(format!("Failed to write to store: {}", e)))?; - } - - Ok(store_path.to_string_lossy().to_string()) - } - - fn add_to_store_from_path( - &self, - name: &str, - source_path: &Path, - _references: Vec, - ) -> Result { - use crate::fetcher::cache::copy_dir_recursive; - - let nar_hash = crate::nar::compute_nar_hash(source_path) - .map_err(|e| Error::internal(format!("Failed to compute NAR hash: {}", e)))?; - - let store_path = self.cache.make_store_path(&nar_hash, name); - - if !store_path.exists() { - fs::create_dir_all(&store_path) - .map_err(|e| Error::internal(format!("Failed to create store directory: {}", e)))?; - - if source_path.is_dir() { - copy_dir_recursive(source_path, &store_path) - .map_err(|e| Error::internal(format!("Failed to copy to store: {}", e)))?; - } else { - fs::copy(source_path, &store_path) - .map_err(|e| Error::internal(format!("Failed to copy to store: {}", e)))?; - } - } - - Ok(store_path.to_string_lossy().to_string()) - } - - fn add_text_to_store( - &self, - name: &str, - content: &str, - references: Vec, - ) -> Result { - self.add_to_store(name, content.as_bytes(), false, references) - } - - fn make_fixed_output_path( - &self, - _hash_algo: &str, - hash: &str, - _hash_mode: &str, - name: &str, - ) -> Result { - let store_path = self.cache.make_store_path(hash, name); - Ok(store_path.to_string_lossy().to_string()) - } -} diff --git a/nix-js/tests/builtins_store.rs b/nix-js/tests/builtins_store.rs index 0857774..d382f57 100644 --- a/nix-js/tests/builtins_store.rs +++ b/nix-js/tests/builtins_store.rs @@ -1,28 +1,10 @@ mod utils; -use std::sync::Once; - use nix_js::value::Value; use utils::eval_result; -fn init() { - static INIT: Once = Once::new(); - INIT.call_once(|| { - #[cfg(not(feature = "daemon"))] - unsafe { - std::env::set_var("NIX_JS_STORE_MODE", "simulated") - }; - #[cfg(feature = "daemon")] - unsafe { - std::env::set_var("NIX_JS_STORE_MODE", "daemon") - }; - }); -} - #[test] fn to_file_simple() { - init(); - let result = eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate"); @@ -40,8 +22,6 @@ fn to_file_simple() { #[test] fn to_file_with_references() { - init(); - let result = eval_result( r#" let @@ -65,8 +45,6 @@ fn to_file_with_references() { #[test] fn to_file_invalid_name_with_slash() { - init(); - let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#); assert!(result.is_err()); @@ -80,8 +58,6 @@ fn to_file_invalid_name_with_slash() { #[test] fn to_file_invalid_name_dot() { - init(); - let result = eval_result(r#"builtins.toFile "." "content""#); assert!(result.is_err()); @@ -90,8 +66,6 @@ fn to_file_invalid_name_dot() { #[test] fn to_file_invalid_name_dotdot() { - init(); - let result = eval_result(r#"builtins.toFile ".." "content""#); assert!(result.is_err()); @@ -100,8 +74,6 @@ fn to_file_invalid_name_dotdot() { #[test] fn store_path_validation_not_in_store() { - init(); - let result = eval_result(r#"builtins.storePath "/tmp/foo""#); assert!(result.is_err()); @@ -115,8 +87,6 @@ fn store_path_validation_not_in_store() { #[test] fn store_path_validation_malformed_hash() { - init(); - let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#) .expect("Failed to create dummy file"); @@ -145,8 +115,6 @@ fn store_path_validation_malformed_hash() { #[test] fn store_path_validation_missing_name() { - init(); - let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#) .expect("Failed to create dummy file"); @@ -175,8 +143,6 @@ fn store_path_validation_missing_name() { #[test] fn to_file_curried_application() { - init(); - let result = eval_result( r#" let @@ -199,8 +165,6 @@ fn to_file_curried_application() { #[test] fn to_file_number_conversion() { - init(); - let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#) .expect("Failed to evaluate"); @@ -215,8 +179,6 @@ fn to_file_number_conversion() { #[test] fn to_file_list_conversion() { - init(); - let result = eval_result( r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#, ) diff --git a/nix-js/tests/derivation.rs b/nix-js/tests/derivation.rs index b44a340..e77cf15 100644 --- a/nix-js/tests/derivation.rs +++ b/nix-js/tests/derivation.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "daemon")] - mod utils; use nix_js::value::Value; @@ -368,26 +366,18 @@ fn fixed_output_sha256_flat() { } #[test] -fn fixed_output_default_algo() { - let result = eval_deep( - r#"derivation { +fn fixed_output_missing_hashalgo() { + assert!( + eval_deep_result( + r#"derivation { name = "default"; builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; }"#, + ) + .is_err() ); - - match result { - Value::AttrSet(attrs) => { - assert!(attrs.contains_key("outPath")); - // Verify it defaults to sha256 (same as explicitly specifying it) - if let Some(Value::String(out_path)) = attrs.get("outPath") { - assert!(out_path.contains("/nix/store/")); - } - } - _ => panic!("Expected AttrSet"), - } } #[test] @@ -428,6 +418,7 @@ fn fixed_output_rejects_multi_output() { builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; + outputHashAlgo = "sha256"; outputs = ["out" "dev"]; }"#, ); @@ -644,6 +635,7 @@ fn fixed_output_with_structured_attrs() { builder = "/bin/sh"; system = "x86_64-linux"; outputHash = "0000000000000000000000000000000000000000000000000000000000000000"; + outputHashAlgo = "sha256"; __structuredAttrs = true; data = { key = "value"; }; }"#,