about summary refs log tree commit diff
path: root/tvix/eval
diff options
context:
space:
mode:
authorPadraic-O-Mhuiris <patrick.morris.310@gmail.com>2024-02-21T16·49+0000
committerPádraic Ó Mhuiris <patrick.morris.310@gmail.com>2024-02-23T16·04+0000
commit5c3065b43a61a5fa019cbbb157984fc5eb81d439 (patch)
tree897a44fdb7da446413276861c13d2a2365ea5f4b /tvix/eval
parentffb134398dedcae6cd13cdf49b2cd57d43793bda (diff)
feat(tvix/eval): implement `builtins.hashString` r/7597
Implements md5, sha1, sha256 and sha512 using the related crates from
the RustCrypto hashes project (https://github.com/RustCrypto/hashes)

Change-Id: I00730dea44ec9ef85309edc27addab0ae88814b8
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11005
Tested-by: BuildkiteCI
Reviewed-by: aspen <root@gws.fyi>
Diffstat (limited to 'tvix/eval')
-rw-r--r--tvix/eval/Cargo.toml4
-rw-r--r--tvix/eval/docs/builtins.md2
-rw-r--r--tvix/eval/src/builtins/mod.rs32
-rw-r--r--tvix/eval/src/errors.rs10
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-groupBy.exp (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp)0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-groupBy.nix (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix)2
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-hashstring.exp (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp)0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-hashstring.nix (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix)0
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.nix6
-rw-r--r--tvix/eval/src/value/string.rs6
11 files changed, 52 insertions, 11 deletions
diff --git a/tvix/eval/Cargo.toml b/tvix/eval/Cargo.toml
index 4f4551a349d2..e2bd07f8279f 100644
--- a/tvix/eval/Cargo.toml
+++ b/tvix/eval/Cargo.toml
@@ -33,6 +33,10 @@ tabwriter = "1.2"
 test-strategy = { version = "0.2.1", optional = true }
 toml = "0.6.0"
 xml-rs = "0.8.4"
+sha2 = "0.10.8"
+sha1 = "0.10.6"
+md-5 = "0.10.6"
+data-encoding = "2.5.0"
 
 [dev-dependencies]
 criterion = "0.5"
diff --git a/tvix/eval/docs/builtins.md b/tvix/eval/docs/builtins.md
index 39b59437e26b..eff761c7057d 100644
--- a/tvix/eval/docs/builtins.md
+++ b/tvix/eval/docs/builtins.md
@@ -66,7 +66,7 @@ The `impl` column indicates implementation status in tvix:
 | hasAttr                       | false  |       |       |         |
 | hasContext                    | false  |       |       |         |
 | hashFile                      | false  |       | false | todo    |
-| hashString                    | false  |       |       | todo    |
+| hashString                    | false  |       |       |         |
 | head                          | false  |       |       |         |
 | import                        | true   |       |       |         |
 | intersectAttrs                | false  |       |       |         |
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
index 131f2b7bb201..119c0bda2dc3 100644
--- a/tvix/eval/src/builtins/mod.rs
+++ b/tvix/eval/src/builtins/mod.rs
@@ -5,9 +5,14 @@
 
 use bstr::{ByteSlice, ByteVec};
 use builtin_macros::builtins;
+use data_encoding::HEXLOWER;
 use genawaiter::rc::Gen;
 use imbl::OrdMap;
+use md5::Md5;
 use regex::Regex;
+use sha1::Sha1;
+use sha2::digest::Output;
+use sha2::{Digest, Sha256, Sha512};
 use std::cmp::{self, Ordering};
 use std::collections::VecDeque;
 use std::collections::{BTreeMap, HashSet};
@@ -686,15 +691,24 @@ mod pure_builtins {
 
     #[builtin("hashString")]
     #[allow(non_snake_case)]
-    async fn builtin_hashString(
-        co: GenCo,
-        _algo: Value,
-        _string: Value,
-    ) -> Result<Value, ErrorKind> {
-        // FIXME: propagate contexts here.
-        Ok(Value::from(CatchableErrorKind::UnimplementedFeature(
-            "hashString".into(),
-        )))
+    async fn builtin_hashString(co: GenCo, algo: Value, s: Value) -> Result<Value, ErrorKind> {
+        fn hash<D: Digest>(b: &[u8]) -> Output<D> {
+            let mut hasher = D::new();
+            hasher.update(b);
+            hasher.finalize()
+        }
+
+        let s = s.to_str()?;
+
+        let encoded_hash = match algo.to_str()?.as_bytes() {
+            b"md5" => HEXLOWER.encode(hash::<Md5>(&s).as_bstr()),
+            b"sha1" => HEXLOWER.encode(hash::<Sha1>(&s).as_bstr()),
+            b"sha256" => HEXLOWER.encode(hash::<Sha256>(&s).as_bstr()),
+            b"sha512" => HEXLOWER.encode(hash::<Sha512>(&s).as_bstr()),
+            _ => return Err(ErrorKind::UnknownHashType(s.into())),
+        };
+
+        Ok(Value::from(encoded_hash))
     }
 
     #[builtin("head")]
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
index db02093b8d65..652252dadfa0 100644
--- a/tvix/eval/src/errors.rs
+++ b/tvix/eval/src/errors.rs
@@ -229,6 +229,10 @@ pub enum ErrorKind {
     /// tvix-eval when returning a result to the user, never inside of
     /// eval code.
     CatchableError(CatchableErrorKind),
+
+    /// Invalid hash type specified, must be one of "md5", "sha1", "sha256"
+    /// or "sha512"
+    UnknownHashType(String),
 }
 
 impl error::Error for Error {
@@ -533,6 +537,10 @@ to a missing value in the attribute set(s) included via `with`."#,
             ErrorKind::CatchableError(inner) => {
                 write!(f, "{}", inner)
             }
+
+            ErrorKind::UnknownHashType(hash_type) => {
+                write!(f, "unknown hash type '{}'", hash_type)
+            }
         }
     }
 }
@@ -821,6 +829,7 @@ impl Error {
             | ErrorKind::TvixBug { .. }
             | ErrorKind::NotImplemented(_)
             | ErrorKind::WithContext { .. }
+            | ErrorKind::UnknownHashType(_)
             | ErrorKind::CatchableError(_) => return None,
         };
 
@@ -866,6 +875,7 @@ impl Error {
             ErrorKind::NotSerialisableToJson(_) => "E036",
             ErrorKind::UnexpectedContext => "E037",
             ErrorKind::Utf8 => "E038",
+            ErrorKind::UnknownHashType(_) => "E039",
 
             // Special error code for errors from other Tvix
             // components. We may want to introduce a code namespacing
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp b/tvix/eval/src/tests/nix_tests/eval-okay-groupBy.exp
index bfca5652a59b..bfca5652a59b 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-groupBy.exp
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix b/tvix/eval/src/tests/nix_tests/eval-okay-groupBy.nix
index 7e0eab28b036..862d89dbd670 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-groupBy.nix
@@ -1,4 +1,4 @@
-with import ./../lib.nix;
+with import ./lib.nix;
 
 builtins.groupBy (n:
   builtins.substring 0 1 (builtins.hashString "sha256" (toString n))
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp b/tvix/eval/src/tests/nix_tests/eval-okay-hashstring.exp
index d720a082ddb3..d720a082ddb3 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-hashstring.exp
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix b/tvix/eval/src/tests/nix_tests/eval-okay-hashstring.nix
index b0f62b245ca8..b0f62b245ca8 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-hashstring.nix
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.exp
new file mode 100644
index 000000000000..e00b80e561fb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.exp
@@ -0,0 +1 @@
+[ "8a0614b4eaa4cffb7515ec101847e198" "8bd218cf61321d8aa05b3602b99f90d2d8cef3d6" "80ac06d74cb6c5d14af718ce8c3c1255969a1a595b76a3cf92354a95331a879a" "0edac513b6b0454705b553deda4c9b055da0939d26d2f73548862817ebeac5378cf64ff7a752ce1a0590a736735d3bbd9e8a7f04d93617cdf514313f5ab5baa4" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.nix
new file mode 100644
index 000000000000..aed723d3670a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hashString.nix
@@ -0,0 +1,6 @@
+[
+  (builtins.hashString "md5" "tvix")
+  (builtins.hashString "sha1" "tvix")
+  (builtins.hashString "sha256" "tvix")
+  (builtins.hashString "sha512" "tvix")
+]
diff --git a/tvix/eval/src/value/string.rs b/tvix/eval/src/value/string.rs
index 2f1592546b85..6ce0d190c0a0 100644
--- a/tvix/eval/src/value/string.rs
+++ b/tvix/eval/src/value/string.rs
@@ -524,6 +524,12 @@ impl<'a> From<&'a NixString> for &'a BStr {
     }
 }
 
+impl From<NixString> for String {
+    fn from(s: NixString) -> Self {
+        s.to_string()
+    }
+}
+
 impl From<NixString> for BString {
     fn from(s: NixString) -> Self {
         s.as_bstr().to_owned()