diff --git a/Cargo.lock b/Cargo.lock index f7e0e4c..47701df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "0.2.17" @@ -76,6 +85,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + [[package]] name = "cradle" version = "0.2.2" @@ -85,6 +103,16 @@ dependencies = [ "rustversion", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctor" version = "0.1.22" @@ -122,6 +150,16 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -182,6 +220,16 @@ dependencies = [ "pulldown-cmark-to-cmark", ] +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getopts" version = "0.2.21" @@ -191,6 +239,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.3.3" @@ -259,6 +318,7 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "similar", "snafu", "strum", @@ -267,6 +327,7 @@ dependencies = [ "temptree", "typed-arena", "unicode-width", + "uuid", "which", "yaml-rust", ] @@ -498,6 +559,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "similar" version = "2.1.0" @@ -657,6 +729,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "unicase" version = "2.6.0" @@ -684,6 +762,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +[[package]] +name = "uuid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +dependencies = [ + "getrandom", +] + [[package]] name = "vec_map" version = "0.8.2" @@ -696,6 +783,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "which" version = "4.2.5" diff --git a/Cargo.toml b/Cargo.toml index 0e343ff..50e1642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ ansi_term = "0.12.0" atty = "0.2.0" camino = "1.0.4" clap = { version = "2.33.0", features = ["wrap_help"] } -ctrlc = { version = "3.1.1", features = ["termination"] } +ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" dotenv = "0.15.0" edit-distance = "2.0.0" @@ -32,13 +32,15 @@ log = "0.4.4" regex = "1.5.4" serde = { version = "1.0.130", features = ["derive", "rc"] } serde_json = "1.0.68" +sha2 = "0.10" similar = { version = "2.1.0", features = ["unicode"] } snafu = "0.7.0" -strum = { version = "0.24.0", features = ["derive"] } +strum = { version = "0.24.0", features = ["derive"] } target = "2.0.0" tempfile = "3.0.0" typed-arena = "2.0.1" unicode-width = "0.1.0" +uuid = { version = "1.0.0", features = ["v4"] } [dev-dependencies] cradle = "0.2.0" diff --git a/README.md b/README.md index f5d9272..5ad63df 100644 --- a/README.md +++ b/README.md @@ -1030,6 +1030,12 @@ These functions can fail, for example if a path does not have an extension, whic - `error(message)` - Abort execution and report error `message` to user. +#### UUID and Hash Generation + +- `sha256(string)` - Return the SHA-256 hash of `string` as a hexadecimal string. +- `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string. +- `uuid()` - Return a randomly generated UUID. + ### Command Evaluation Using Backticks Backticks can be used to store the result of commands: diff --git a/src/function.rs b/src/function.rs index ccdb8a5..7155098 100644 --- a/src/function.rs +++ b/src/function.rs @@ -35,6 +35,8 @@ lazy_static! { ("path_exists", Unary(path_exists)), ("quote", Unary(quote)), ("replace", Ternary(replace)), + ("sha256", Unary(sha256)), + ("sha256_file", Unary(sha256_file)), ("trim", Unary(trim)), ("trim_end", Unary(trim_end)), ("trim_end_match", Binary(trim_end_match)), @@ -43,6 +45,7 @@ lazy_static! { ("trim_start_match", Binary(trim_start_match)), ("trim_start_matches", Binary(trim_start_matches)), ("uppercase", Unary(uppercase)), + ("uuid", Nullary(uuid)), ("without_extension", Unary(without_extension)), ] .into_iter() @@ -247,6 +250,26 @@ fn replace(_context: &FunctionContext, s: &str, from: &str, to: &str) -> Result< Ok(s.replace(from, to)) } +fn sha256(_context: &FunctionContext, s: &str) -> Result { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(s); + let hash = hasher.finalize(); + Ok(format!("{:x}", hash)) +} + +fn sha256_file(context: &FunctionContext, path: &str) -> Result { + use sha2::{Digest, Sha256}; + let justpath = context.search.working_directory.join(path); + let mut hasher = Sha256::new(); + let mut file = std::fs::File::open(&justpath) + .map_err(|err| format!("Failed to open file at `{:?}`: {}", &justpath.to_str(), err))?; + std::io::copy(&mut file, &mut hasher) + .map_err(|err| format!("Failed to read file at `{:?}`: {}", &justpath.to_str(), err))?; + let hash = hasher.finalize(); + Ok(format!("{:x}", hash)) +} + fn trim(_context: &FunctionContext, s: &str) -> Result { Ok(s.trim().to_owned()) } @@ -279,6 +302,10 @@ fn uppercase(_context: &FunctionContext, s: &str) -> Result { Ok(s.to_uppercase()) } +fn uuid(_context: &FunctionContext) -> Result { + Ok(uuid::Uuid::new_v4().to_string()) +} + fn without_extension(_context: &FunctionContext, path: &str) -> Result { let parent = Utf8Path::new(path) .parent() diff --git a/tests/functions.rs b/tests/functions.rs index 713f1dd..e5f8d35 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -476,3 +476,36 @@ fn path_exists_subdir() { .stdout("true") .run(); } + +#[test] +fn uuid() { + Test::new() + .justfile("x := uuid()") + .args(&["--evaluate", "x"]) + .stdout_regex("........-....-....-....-............") + .run(); +} + +#[test] +fn sha256() { + Test::new() + .justfile("x := sha256('5943ee37-0000-1000-8000-010203040506')") + .args(&["--evaluate", "x"]) + .stdout("2330d7f5eb94a820b54fed59a8eced236f80b633a504289c030b6a65aef58871") + .run(); +} + +#[test] +fn sha256_file() { + Test::new() + .justfile("x := sha256_file('sub/shafile')") + .tree(tree! { + sub: { + shafile: "just is great\n", + } + }) + .current_dir("sub") + .args(&["--evaluate", "x"]) + .stdout("177b3d79aaafb53a7a4d7aaba99a82f27c73370e8cb0295571aade1e4fea1cd2") + .run(); +} diff --git a/tests/test.rs b/tests/test.rs index 4673ad2..a126fb5 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -10,6 +10,7 @@ macro_rules! test { $(env: { $($env_key:literal : $env_value:literal,)* },)? $(stdin: $stdin:expr,)? $(stdout: $stdout:expr,)? + $(stdout_regex: $stdout_regex:expr,)? $(stderr: $stderr:expr,)? $(stderr_regex: $stderr_regex:expr,)? $(status: $status:expr,)? @@ -28,6 +29,7 @@ macro_rules! test { $(let test = test.stderr_regex($stderr_regex);)? $(let test = test.stdin($stdin);)? $(let test = test.stdout($stdout);)? + $(let test = test.stdout_regex($stdout_regex);)? test.run(); } @@ -45,6 +47,7 @@ pub(crate) struct Test { pub(crate) stderr_regex: Option, pub(crate) stdin: String, pub(crate) stdout: String, + pub(crate) stdout_regex: Option, pub(crate) tempdir: TempDir, pub(crate) unindent_stdout: bool, } @@ -66,6 +69,7 @@ impl Test { stderr_regex: None, stdin: String::new(), stdout: String::new(), + stdout_regex: None, tempdir, unindent_stdout: true, } @@ -137,6 +141,11 @@ impl Test { self } + pub(crate) fn stdout_regex(mut self, stdout_regex: impl AsRef) -> Self { + self.stdout_regex = Some(Regex::new(&format!("(?m)^{}$", stdout_regex.as_ref())).unwrap()); + self + } + pub(crate) fn tree(self, mut tree: Tree) -> Self { tree.map(|_name, content| unindent(content)); tree.instantiate(self.tempdir.path()).unwrap(); @@ -203,8 +212,18 @@ impl Test { equal } + let output_stdout = str::from_utf8(&output.stdout).unwrap(); let output_stderr = str::from_utf8(&output.stderr).unwrap(); + if let Some(ref stdout_regex) = self.stdout_regex { + if !stdout_regex.is_match(output_stdout) { + panic!( + "Stdout regex mismatch:\n{:?}\n!~=\n/{:?}/", + output_stderr, stdout_regex + ); + } + } + if let Some(ref stderr_regex) = self.stderr_regex { if !stderr_regex.is_match(output_stderr) { panic!( @@ -215,7 +234,7 @@ impl Test { } if !compare("status", output.status.code().unwrap(), self.status) - | !compare("stdout", str::from_utf8(&output.stdout).unwrap(), &stdout) + | (self.stdout_regex.is_none() && !compare("stdout", output_stdout, &stdout)) | (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr)) { panic!("Output mismatch.");