From a5cdd22bfe2391e2297ff8080855a1209461c130 Mon Sep 17 00:00:00 2001 From: Ryan Blecher Date: Sat, 20 Mar 2021 14:48:53 -0400 Subject: [PATCH] Add basic support for md5 hashing strings and binary data (#3197) --- Cargo.lock | 7 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/commands.rs | 2 +- .../src/commands/default_context.rs | 1 + crates/nu-command/src/commands/hash_/md5_.rs | 133 ++++++++++++++++++ crates/nu-command/src/commands/hash_/mod.rs | 2 + crates/nu-command/tests/commands/hash_/mod.rs | 13 ++ 7 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 crates/nu-command/src/commands/hash_/md5_.rs diff --git a/Cargo.lock b/Cargo.lock index 2a05adcf06..001e671319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2796,6 +2796,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e6bcd6433cff03a4bfc3d9834d504467db1f1cf6d0ea765d37d330249ed629d" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.3.4" @@ -3265,6 +3271,7 @@ dependencies = [ "itertools", "lazy_static 1.4.0", "log 0.4.14", + "md5 0.7.0", "meval", "minus", "nu-ansi-term", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 3f3599a2a1..06051193a7 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -62,6 +62,7 @@ indexmap = { version = "1.6.1", features = ["serde-1"] } itertools = "0.10.0" lazy_static = "1.*" log = "0.4.14" +md5 = "0.7.0" meval = "0.2.0" minus = { version = "3.3.0", optional = true, features = ["async_std_lib", "search"] } num-bigint = { version = "0.3.1", features = ["serde"] } diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index e8f263b0cc..c3773afedb 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -202,7 +202,7 @@ pub(crate) use from_yaml::FromYML; pub(crate) use get::Command as Get; pub(crate) use group_by::Command as GroupBy; pub(crate) use group_by_date::GroupByDate; -pub(crate) use hash_::{Hash, HashBase64}; +pub(crate) use hash_::{Hash, HashBase64, HashMd5}; pub(crate) use headers::Headers; pub(crate) use help::Help; pub(crate) use histogram::Histogram; diff --git a/crates/nu-command/src/commands/default_context.rs b/crates/nu-command/src/commands/default_context.rs index 6ed7bb1025..5a945fabc2 100644 --- a/crates/nu-command/src/commands/default_context.rs +++ b/crates/nu-command/src/commands/default_context.rs @@ -74,6 +74,7 @@ pub fn create_default_context(interactive: bool) -> Result, +} + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "hash md5" + } + + fn signature(&self) -> Signature { + Signature::build("hash md5").rest( + SyntaxShape::ColumnPath, + "optionally md5 encode data by column paths", + ) + } + + fn usage(&self) -> &str { + "md5 encode a value" + } + + async fn run(&self, args: CommandArgs) -> Result { + operate(args).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "md5 encode a string", + example: "echo 'abcdefghijklmnopqrstuvwxyz' | hash md5", + result: Some(vec![UntaggedValue::string( + "c3fcd3d76192e4007dfb496cca67e13b", + ) + .into_untagged_value()]), + }, + Example { + description: "md5 encode a file", + example: "open ./nu_0_24_1_windows.zip | hash md5", + result: Some(vec![UntaggedValue::string( + "dcf30f2836a1a99fc55cf72e28272606", + ) + .into_untagged_value()]), + }, + ] + } +} + +async fn operate(args: CommandArgs) -> Result { + let (Arguments { rest }, input) = args.process().await?; + + let column_paths: Vec<_> = rest; + + Ok(input + .map(move |v| { + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, v.tag())?) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, old.tag())), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::String(s)) => { + let md5_digest = md5::compute(s.as_bytes()); + Ok(UntaggedValue::string(&format!("{:x}", md5_digest)).into_value(tag)) + } + UntaggedValue::Primitive(Primitive::Binary(bytes)) => { + let md5_digest = md5::compute(bytes); + Ok(UntaggedValue::string(&format!("{:x}", md5_digest)).into_value(tag)) + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not supported for hashing as md5", + got, + tag.into().span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::action; + use nu_protocol::{Primitive, UntaggedValue}; + use nu_source::Tag; + use nu_test_support::value::string; + + #[test] + fn md5_encode_string() { + let word = string("abcdefghijklmnopqrstuvwxyz"); + let expected = + UntaggedValue::string("c3fcd3d76192e4007dfb496cca67e13b").into_untagged_value(); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn md5_encode_bytes() { + let bytes = vec![0xC0, 0xFF, 0xEE]; + let binary = UntaggedValue::Primitive(Primitive::Binary(bytes)).into_untagged_value(); + let expected = + UntaggedValue::string("5f80e231382769b0102b1164cf722d83").into_untagged_value(); + + let actual = action(&binary, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/commands/hash_/mod.rs b/crates/nu-command/src/commands/hash_/mod.rs index 48c6f03d15..30dcacf3f2 100644 --- a/crates/nu-command/src/commands/hash_/mod.rs +++ b/crates/nu-command/src/commands/hash_/mod.rs @@ -1,5 +1,7 @@ mod base64_; mod command; +mod md5_; pub use base64_::SubCommand as HashBase64; pub use command::Command as Hash; +pub use md5_::SubCommand as HashMd5; diff --git a/crates/nu-command/tests/commands/hash_/mod.rs b/crates/nu-command/tests/commands/hash_/mod.rs index 48dcdc2e02..51a3463f64 100644 --- a/crates/nu-command/tests/commands/hash_/mod.rs +++ b/crates/nu-command/tests/commands/hash_/mod.rs @@ -83,3 +83,16 @@ fn error_use_both_flags() { .err .contains("only one of --decode and --encode flags can be used")); } + +#[test] +fn md5_works_with_file() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.db | hash md5 + "# + ) + ); + + assert_eq!(actual.out, "4de97601d232c427977ef11db396c951"); +}