Repository: casey/just
Pull request #1860 (2024-01-21 00:51:41)

Add `blake3` and `blake3_file` hash functions
Blake3 is a hash algorithm that is pretty popular when working with larger files, and is also (in theory) closer to SHA-3 in security than SHA-2. I believe it is used by sccache and some other larger build systems.

This adds `blake3` and `blake3_file` functions. `blake3_file` is especially of note because it uses mmap and parallelization rather than reading the file into memory, so it should outperform `sha256_file` quite a bit (this is the same function used by the `b3sum` utility).

I need to rebase since I did this a while ago, but I may as well get your thoughts here.

casey: Love it, this is super cool.
tgross35: Awesome! I rebased so this should be all set for a look.
casey: Nice, LGTM! I used `blake3::hash` instead of a hasher in in `blake3()`, since it's one and done and doesn't need incremental updates. I also changed uses of `path.to_str` to `path.display()`. Since paths are unicode, they can't necessarily be converted to a string, and path.display returns something that represents a path but has a display implementation, so it can be printed. It looks like we made the same mistake in the `sha256` functions, so I fixed those too.
casey: I also added `<sup>master</sup>` annotations, so people know this hasn't made it into a release yet.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 0a6ebcf9..5f97feb0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -68,6 +68,18 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -91,6 +103,21 @@ version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
 
+[[package]]
+name = "blake3"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "memmap2",
+ "rayon",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
@@ -117,6 +144,15 @@ version = "1.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
 
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -145,6 +181,12 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "constant_time_eq"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.12"
@@ -163,6 +205,31 @@ dependencies = [
  "rustversion",
 ]
 
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -389,6 +456,7 @@ version = "1.24.0"
 dependencies = [
  "ansi_term",
  "atty",
+ "blake3",
  "camino",
  "clap",
  "cradle",
@@ -476,6 +544,15 @@ version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
 
+[[package]]
+name = "memmap2"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "nix"
 version = "0.27.1"
@@ -582,6 +659,26 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
diff --git a/Cargo.toml b/Cargo.toml
index 02cbbe0f..7d867993 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ members = [".", "bin/ref-type", "bin/generate-book", "bin/update-contributors"]
 [dependencies]
 ansi_term = "0.12.0"
 atty = "0.2.0"
+blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
 camino = "1.0.4"
 clap = { version = "2.33.0", features = ["wrap_help"] }
 ctrlc = { version = "3.1.1", features = ["termination"] }
diff --git a/README.md b/README.md
index 8a1ddbcd..46a93e65 100644
--- a/README.md
+++ b/README.md
@@ -1456,12 +1456,16 @@ which will halt execution.
 
 #### UUID and Hash Generation
 
-- `sha256(string)` - Return the SHA-256 hash of `string` as a hexadecimal
+- `blake3(string)`<sup>master</sup> - Return [BLAKE3] hash of `string` as hexadecimal string.
+- `blake3_file(path)`<sup>master</sup> - Return [BLAKE3] hash of file at `path` as hexadecimal
+  string.
+- `sha256(string)` - Return the SHA-256 hash of `string` as hexadecimal string.
+- `sha256_file(path)` - Return SHA-256 hash of file at `path` as hexadecimal
   string.
-- `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a
-  hexadecimal string.
 - `uuid()` - Generate a random version 4 UUID.
 
+[BLAKE3]: https://github.com/BLAKE3-team/BLAKE3/
+
 #### Semantic Versions
 
 - `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a
diff --git a/src/function.rs b/src/function.rs
index 1262076b..fee4bfc5 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -21,6 +21,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
   let function = match name {
     "absolute_path" => Unary(absolute_path),
     "arch" => Nullary(arch),
+    "blake3" => Unary(blake3),
+    "blake3_file" => Unary(blake3_file),
     "canonicalize" => Unary(canonicalize),
     "cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir)),
     "capitalize" => Unary(capitalize),
@@ -107,6 +109,19 @@ fn arch(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }
 
+fn blake3(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(blake3::hash(s.as_bytes()).to_string())
+}
+
+fn blake3_file(context: &FunctionContext, path: &str) -> Result<String, String> {
+  let path = context.search.working_directory.join(path);
+  let mut hasher = blake3::Hasher::new();
+  hasher
+    .update_mmap_rayon(&path)
+    .map_err(|err| format!("Failed to hash `{}`: {err}", path.display()))?;
+  Ok(hasher.finalize().to_string())
+}
+
 fn canonicalize(_context: &FunctionContext, path: &str) -> Result<String, String> {
   let canonical =
     std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
@@ -377,12 +392,12 @@ fn sha256(_context: &FunctionContext, s: &str) -> Result<String, String> {
 
 fn sha256_file(context: &FunctionContext, path: &str) -> Result<String, String> {
   use sha2::{Digest, Sha256};
-  let justpath = context.search.working_directory.join(path);
+  let path = context.search.working_directory.join(path);
   let mut hasher = Sha256::new();
-  let mut file = fs::File::open(&justpath)
-    .map_err(|err| format!("Failed to open file at `{:?}`: {err}", justpath.to_str()))?;
+  let mut file =
+    fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;
   std::io::copy(&mut file, &mut hasher)
-    .map_err(|err| format!("Failed to read file at `{:?}`: {err}", justpath.to_str()))?;
+    .map_err(|err| format!("Failed to read `{}`: {err}", path.display()))?;
   let hash = hasher.finalize();
   Ok(format!("{hash:x}"))
 }


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index c3565242..2c0e8143 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -663,6 +663,30 @@ fn just_pid() {
   assert_eq!(stdout.parse::<u32>().unwrap(), pid);
 }
 
+#[test]
+fn blake3() {
+  Test::new()
+    .justfile("x := blake3('5943ee37-0000-1000-8000-010203040506')")
+    .args(["--evaluate", "x"])
+    .stdout("026c9f740a793ff536ddf05f8915ea4179421f47f0fa9545476076e9ba8f3f2b")
+    .run();
+}
+
+#[test]
+fn blake3_file() {
+  Test::new()
+    .justfile("x := blake3_file('sub/blakefile')")
+    .tree(tree! {
+      sub: {
+        blakefile: "just is great\n",
+      }
+    })
+    .current_dir("sub")
+    .args(["--evaluate", "x"])
+    .stdout("8379241877190ca4b94076a8c8f89fe5747f95c62f3e4bf41f7408a0088ae16d")
+    .run();
+}
+
 #[cfg(unix)]
 #[test]
 fn canonicalize() {