diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 094781e..6b794ed 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -37,7 +37,7 @@ deno_error = "0.7" nix-nar = "0.3" sha2 = "0.10" sha1 = "0.10" -md5 = "0.7" +md5 = "0.8" hex = "0.4" base64 = "0.22" diff --git a/nix-js/runtime-ts/src/builtins/hash.ts b/nix-js/runtime-ts/src/builtins/hash.ts index a8fcee1..6405f6d 100644 --- a/nix-js/runtime-ts/src/builtins/hash.ts +++ b/nix-js/runtime-ts/src/builtins/hash.ts @@ -1,21 +1,37 @@ -import { forceStringNoCtx } from "../type-assert"; +import { forceAttrs, forceStringNoCtx, forceStringValue } from "../type-assert"; import type { NixValue } from "../types"; import { realisePath } from "./io"; export const hashFile = (type: NixValue) => (p: NixValue): string => { - const _ty = forceStringNoCtx(type); - const _pathStr = realisePath(p); - throw new Error("Not implemented: hashFile"); + const algo = forceStringNoCtx(type); + const pathStr = realisePath(p); + return Deno.core.ops.op_hash_file(algo, pathStr); }; export const hashString = - (_type: NixValue) => - (_p: NixValue): never => { - throw new Error("Not implemented: hashString"); + (type: NixValue) => + (s: NixValue): string => { + const algo = forceStringNoCtx(type); + const data = forceStringValue(s); + return Deno.core.ops.op_hash_string(algo, data); }; -export const convertHash = (_args: NixValue): never => { - throw new Error("Not implemented: convertHash"); +export const convertHash = (args: NixValue): string => { + const attrs = forceAttrs(args); + const hash = forceStringNoCtx(attrs.hash); + + let hashAlgo: string | null = null; + if ("hashAlgo" in attrs) { + hashAlgo = forceStringNoCtx(attrs.hashAlgo); + } + + const toHashFormat = forceStringNoCtx(attrs.toHashFormat); + + return Deno.core.ops.op_convert_hash({ + hash, + hashAlgo, + toHashFormat, + }); }; diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 56580d8..2015aad 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -20,6 +20,13 @@ declare global { function op_make_placeholder(output: string): string; function op_store_path(path: string): string; + function op_convert_hash(input: { + hash: string; + hashAlgo: string | null; + toHashFormat: string; + }): string; + function op_hash_string(algo: string, data: string): string; + function op_hash_file(algo: string, path: string): string; function op_parse_hash(hashStr: string, algo: string | null): { hex: string; algo: string }; function op_add_path( diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 2fbd7f4..533231a 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -58,6 +58,9 @@ fn runtime_extension() -> Extension { op_make_placeholder(), op_store_path::(), + op_convert_hash(), + op_hash_string(), + op_hash_file(), op_parse_hash(), op_add_path::(), diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index abe3ef1..43f796f 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -1106,3 +1106,135 @@ fn op_make_fixed_output_path_impl( crate::nix_utils::make_store_path(store_dir, "output:out", &inner_hash, name) } } + +#[deno_core::op2] +#[string] +pub(super) fn op_hash_string( + #[string] algo: String, + #[string] data: String, +) -> std::result::Result { + use sha2::{Digest, Sha256, Sha512}; + + let hash_bytes: Vec = match algo.as_str() { + "sha256" => { + let mut hasher = Sha256::new(); + hasher.update(data.as_bytes()); + hasher.finalize().to_vec() + } + "sha512" => { + let mut hasher = Sha512::new(); + hasher.update(data.as_bytes()); + hasher.finalize().to_vec() + } + "sha1" => { + use sha1::Digest as _; + let mut hasher = sha1::Sha1::new(); + hasher.update(data.as_bytes()); + hasher.finalize().to_vec() + } + "md5" => { + let digest = md5::compute(data.as_bytes()); + digest.to_vec() + } + _ => { + return Err(NixRuntimeError::from(format!( + "unknown hash algorithm '{}'", + algo + ))); + } + }; + + Ok(hex::encode(hash_bytes)) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_hash_file( + #[string] algo: String, + #[string] path: String, +) -> std::result::Result { + let data = std::fs::read(&path) + .map_err(|e| NixRuntimeError::from(format!("cannot read '{}': {}", path, e)))?; + + use sha2::{Digest, Sha256, Sha512}; + + let hash_bytes: Vec = match algo.as_str() { + "sha256" => { + let mut hasher = Sha256::new(); + hasher.update(&data); + hasher.finalize().to_vec() + } + "sha512" => { + let mut hasher = Sha512::new(); + hasher.update(&data); + hasher.finalize().to_vec() + } + "sha1" => { + use sha1::Digest as _; + let mut hasher = sha1::Sha1::new(); + hasher.update(&data); + hasher.finalize().to_vec() + } + "md5" => { + let digest = md5::compute(&data); + digest.to_vec() + } + _ => { + return Err(NixRuntimeError::from(format!( + "unknown hash algorithm '{}'", + algo + ))); + } + }; + + Ok(hex::encode(hash_bytes)) +} + +#[deno_core::op2] +#[string] +pub(super) fn op_convert_hash( + #[serde] input: ConvertHashInput, +) -> std::result::Result { + use nix_compat::nixhash::{HashAlgo, NixHash}; + + let hash_algo = input + .hash_algo + .as_deref() + .and_then(|a| HashAlgo::from_str(a).ok()); + + let hash = NixHash::from_str(&input.hash, hash_algo).map_err(|e| { + NixRuntimeError::from(format!("cannot convert hash '{}': {}", input.hash, e)) + })?; + + let bytes = hash.digest_as_bytes(); + + match input.to_format.as_str() { + "base16" => Ok(hex::encode(bytes)), + "nix32" | "base32" => Ok(nix_compat::nixbase32::encode(bytes)), + "base64" => { + use base64::Engine as _; + Ok(base64::engine::general_purpose::STANDARD.encode(bytes)) + } + "sri" => Ok(format!( + "{}-{}", + hash.algo(), + { + use base64::Engine as _; + base64::engine::general_purpose::STANDARD.encode(bytes) + } + )), + _ => Err(NixRuntimeError::from(format!( + "unknown hash format '{}'", + input.to_format + ))), + } +} + +#[derive(serde::Deserialize)] +pub(super) struct ConvertHashInput { + hash: String, + #[serde(rename = "hashAlgo")] + hash_algo: Option, + #[serde(rename = "toHashFormat")] + to_format: String, +} diff --git a/nix-js/tests/lang.rs b/nix-js/tests/lang.rs index 4b70997..666784f 100644 --- a/nix-js/tests/lang.rs +++ b/nix-js/tests/lang.rs @@ -122,10 +122,7 @@ eval_okay_test!(concatmap); eval_okay_test!(concatstringssep); eval_okay_test!(context); eval_okay_test!(context_introspection); -eval_okay_test!( - #[ignore = "not implemented: convertHash"] - convertHash -); +eval_okay_test!(convertHash); eval_okay_test!(curpos); eval_okay_test!(deepseq); eval_okay_test!(delayed_with); @@ -158,24 +155,15 @@ eval_okay_test!( fromTOML_timestamps ); eval_okay_test!(functionargs); -eval_okay_test!( - #[ignore = "not implemented: hashFile"] - hashfile -); -eval_okay_test!( - #[ignore = "not implemented: hashString"] - hashstring -); +eval_okay_test!(hashfile); +eval_okay_test!(hashstring); eval_okay_test!(getattrpos); eval_okay_test!(getattrpos_functionargs); eval_okay_test!(getattrpos_undefined); eval_okay_test!(getenv, || { unsafe { std::env::set_var("TEST_VAR", "foo") }; }); -eval_okay_test!( - #[ignore = "not implemented: hashString"] - groupBy -); +eval_okay_test!(groupBy); eval_okay_test!(r#if); eval_okay_test!(ind_string); eval_okay_test!(import); @@ -265,10 +253,7 @@ eval_okay_test!(tryeval); eval_okay_test!(types); eval_okay_test!(versions); eval_okay_test!(with); -eval_okay_test!( - #[ignore = "not implemented: hashString"] - zipAttrsWith -); +eval_okay_test!(zipAttrsWith); eval_fail_test!(fail_abort); eval_fail_test!(fail_addDrvOutputDependencies_empty_context); diff --git a/nix-js/tests/lang/binary-data b/nix-js/tests/lang/binary-data new file mode 100644 index 0000000..06d7405 Binary files /dev/null and b/nix-js/tests/lang/binary-data differ