From 173d60d59de9865d0623ed71abc14be09a2800a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Cortier?= Date: Sat, 25 Jun 2022 17:35:23 -0400 Subject: [PATCH] Deprecate `hash base64`, extend `decode` and add `encode` commands (#5863) * feat: deprecate `hash base64` command * feat: extend `decode` and `encode` command families This commit - Adds `encode` command family - Backports `hash base64` features to `encode base64` and `decode base64` subcommands. - Refactors code a bit and extends tests for encodings - `decode base64` returns a binary `Value` (that may be decoded into a string using `decode` command) * feat: add `--binary(-b)` flag to `decode base64` Default output type is now string, but binary can be requested using this new flag. --- Cargo.lock | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/default_context.rs | 5 +- .../nu-command/src/deprecated/hash_base64.rs | 34 ++++ crates/nu-command/src/deprecated/mod.rs | 2 + crates/nu-command/src/hash/mod.rs | 2 - .../{hash => strings/encode_decode}/base64.rs | 166 ++++++------------ .../src/strings/{ => encode_decode}/decode.rs | 70 ++------ .../strings/encode_decode/decode_base64.rs | 90 ++++++++++ .../src/strings/encode_decode/encode.rs | 95 ++++++++++ .../strings/encode_decode/encode_base64.rs | 78 ++++++++ .../src/strings/encode_decode/encoding.rs | 67 +++++++ .../src/strings/encode_decode/mod.rs | 11 ++ crates/nu-command/src/strings/mod.rs | 4 +- crates/nu-command/tests/commands/hash_/mod.rs | 25 +-- crates/nu-protocol/src/value/mod.rs | 7 + 16 files changed, 466 insertions(+), 192 deletions(-) create mode 100644 crates/nu-command/src/deprecated/hash_base64.rs rename crates/nu-command/src/{hash => strings/encode_decode}/base64.rs (68%) rename crates/nu-command/src/strings/{ => encode_decode}/decode.rs (50%) create mode 100644 crates/nu-command/src/strings/encode_decode/decode_base64.rs create mode 100644 crates/nu-command/src/strings/encode_decode/encode.rs create mode 100644 crates/nu-command/src/strings/encode_decode/encode_base64.rs create mode 100644 crates/nu-command/src/strings/encode_decode/encoding.rs create mode 100644 crates/nu-command/src/strings/encode_decode/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 30d3dee7ac..a2e7f66999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2621,6 +2621,7 @@ dependencies = [ "regex", "reqwest", "roxmltree", + "rstest", "rusqlite", "rust-embed", "serde", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index a2102d851f..5ee1b698a1 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -132,3 +132,4 @@ hamcrest2 = "0.3.0" dirs-next = "2.0.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" +rstest = "0.12.0" diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 3c69688c9d..01d0e151fd 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -170,6 +170,9 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { BuildString, Char, Decode, + Encode, + DecodeBase64, + EncodeBase64, DetectColumns, Format, FileSize, @@ -382,7 +385,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Hash, HashMd5::default(), HashSha256::default(), - Base64, + HashBase64, }; // Experimental diff --git a/crates/nu-command/src/deprecated/hash_base64.rs b/crates/nu-command/src/deprecated/hash_base64.rs new file mode 100644 index 0000000000..3896e8ea18 --- /dev/null +++ b/crates/nu-command/src/deprecated/hash_base64.rs @@ -0,0 +1,34 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct HashBase64; + +impl Command for HashBase64 { + fn name(&self) -> &str { + "hash base64" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Deprecated) + } + + fn usage(&self) -> &str { + "Deprecated command" + } + + fn run( + &self, + _: &EngineState, + _: &mut Stack, + call: &Call, + _: PipelineData, + ) -> Result { + Err(nu_protocol::ShellError::DeprecatedCommand( + self.name().to_string(), + "encode base64".to_owned(), + call.head, + )) + } +} diff --git a/crates/nu-command/src/deprecated/mod.rs b/crates/nu-command/src/deprecated/mod.rs index f4af1ca287..3caa9c4ffc 100644 --- a/crates/nu-command/src/deprecated/mod.rs +++ b/crates/nu-command/src/deprecated/mod.rs @@ -1,3 +1,4 @@ +mod hash_base64; mod keep_; mod keep_until; mod keep_while; @@ -10,6 +11,7 @@ mod str_find_replace; mod str_int; mod unalias; +pub use hash_base64::HashBase64; pub use keep_::KeepDeprecated; pub use keep_until::KeepUntilDeprecated; pub use keep_while::KeepWhileDeprecated; diff --git a/crates/nu-command/src/hash/mod.rs b/crates/nu-command/src/hash/mod.rs index 013ff57f05..417da1a1eb 100644 --- a/crates/nu-command/src/hash/mod.rs +++ b/crates/nu-command/src/hash/mod.rs @@ -1,10 +1,8 @@ -mod base64; mod generic_digest; mod hash_; mod md5; mod sha256; -pub use self::base64::Base64; pub use self::hash_::Hash; pub use self::md5::HashMd5; pub use self::sha256::HashSha256; diff --git a/crates/nu-command/src/hash/base64.rs b/crates/nu-command/src/strings/encode_decode/base64.rs similarity index 68% rename from crates/nu-command/src/hash/base64.rs rename to crates/nu-command/src/strings/encode_decode/base64.rs index c6f964e68f..85c6724b38 100644 --- a/crates/nu-command/src/hash/base64.rs +++ b/crates/nu-command/src/strings/encode_decode/base64.rs @@ -1,10 +1,12 @@ use base64::{decode_config, encode_config}; use nu_engine::CallExt; use nu_protocol::ast::{Call, CellPath}; -use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, -}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{PipelineData, ShellError, Span, Spanned, Value}; + +pub const CHARACTER_SET_DESC: &str = "specify the character rules for encoding the input.\n\ + \tValid values are 'standard', 'standard-no-padding', 'url-safe', 'url-safe-no-padding',\ + 'binhex', 'bcrypt', 'crypt'"; #[derive(Clone)] pub struct Base64Config { @@ -18,106 +20,19 @@ pub enum ActionType { Decode, } -#[derive(Clone)] -pub struct Base64; - -impl Command for Base64 { - fn name(&self) -> &str { - "hash base64" - } - - fn signature(&self) -> Signature { - Signature::build("hash base64") - .named( - "character-set", - SyntaxShape::String, - "specify the character rules for encoding the input.\n\ - \tValid values are 'standard', 'standard-no-padding', 'url-safe', 'url-safe-no-padding',\ - 'binhex', 'bcrypt', 'crypt'", - Some('c'), - ) - .switch( - "encode", - "encode the input as base64. This is the default behavior if not specified.", - Some('e') - ) - .switch( - "decode", - "decode the input from base64", - Some('d')) - .rest( - "rest", - SyntaxShape::CellPath, - "optionally base64 encode / decode data by column paths", - ) - .category(Category::Hash) - } - - fn usage(&self) -> &str { - "base64 encode or decode a value" - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Base64 encode a string with default settings", - example: "echo 'username:password' | hash base64", - result: Some(Value::string("dXNlcm5hbWU6cGFzc3dvcmQ=", Span::test_data())), - }, - Example { - description: "Base64 encode a string with the binhex character set", - example: "echo 'username:password' | hash base64 --character-set binhex --encode", - result: Some(Value::string("F@0NEPjJD97kE'&bEhFZEP3", Span::test_data())), - }, - Example { - description: "Base64 decode a value", - example: "echo 'dXNlcm5hbWU6cGFzc3dvcmQ=' | hash base64 --decode", - result: Some(Value::string("username:password", Span::test_data())), - }, - ] - } - - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, - ) -> Result { - operate(engine_state, stack, call, input) - } -} - -fn operate( +pub fn operate( + action_type: ActionType, engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let head = call.head; - let encode = call.has_flag("encode"); - let decode = call.has_flag("decode"); let character_set: Option> = call.get_flag(engine_state, stack, "character-set")?; + let binary = call.has_flag("binary"); let column_paths: Vec = call.rest(engine_state, stack, 0)?; - if encode && decode { - return Err(ShellError::GenericError( - "only one of --decode and --encode flags can be used".to_string(), - "conflicting flags".to_string(), - Some(head), - None, - Vec::new(), - )); - } - - // Default the action to be encoding if no flags are specified. - let action_type = if decode { - ActionType::Decode - } else { - ActionType::Encode - }; - // Default the character set to standard if the argument is not specified. let character_set = match character_set { Some(inner_tag) => inner_tag, @@ -135,7 +50,7 @@ fn operate( input.map( move |v| { if column_paths.is_empty() { - match action(&v, &encoding_config, &head) { + match action(&v, binary, &encoding_config, &head) { Ok(v) => v, Err(e) => Value::Error { error: e }, } @@ -147,7 +62,7 @@ fn operate( let r = ret.update_cell_path( &path.members, - Box::new(move |old| match action(old, &config, &head) { + Box::new(move |old| match action(old, binary, &config, &head) { Ok(v) => v, Err(e) => Value::Error { error: e }, }), @@ -166,6 +81,8 @@ fn operate( fn action( input: &Value, + // only used for `decode` action + output_binary: bool, base64_config: &Base64Config, command_span: &Span, ) -> Result { @@ -200,7 +117,10 @@ fn action( *command_span, )), }, - Value::String { val, .. } => { + Value::String { + val, + span: value_span, + } => { match base64_config.action_type { ActionType::Encode => Ok(Value::string( encode_config(&val, base64_config_enum), @@ -211,13 +131,26 @@ fn action( // for decode, input val may contains invalid new line character, which is ok to omitted them by default. let val = val.clone(); let val = val.replace("\r\n", "").replace('\n', ""); - let decode_result = decode_config(&val, base64_config_enum); - match decode_result { - Ok(decoded_value) => Ok(Value::string( - std::string::String::from_utf8_lossy(&decoded_value), - *command_span, - )), + match decode_config(&val, base64_config_enum) { + Ok(decoded_value) => { + if output_binary { + Ok(Value::binary(decoded_value, *command_span)) + } else { + match String::from_utf8(decoded_value) { + Ok(string_value) => { + Ok(Value::string(string_value, *command_span)) + } + Err(e) => Err(ShellError::GenericError( + "base64 payload isn't a valid utf-8 sequence".to_owned(), + e.to_string(), + Some(*value_span), + Some("consider using the `--binary` flag".to_owned()), + Vec::new(), + )), + } + } + } Err(_) => Err(ShellError::GenericError( "value could not be base64 decoded".to_string(), format!( @@ -241,22 +174,17 @@ fn action( #[cfg(test)] mod tests { - use super::{action, ActionType, Base64, Base64Config}; + use super::{action, ActionType, Base64Config}; use nu_protocol::{Span, Spanned, Value}; - #[test] - fn test_examples() { - use crate::test_examples; - test_examples(Base64 {}) - } - #[test] fn base64_encode_standard() { - let word = Value::string("username:password", Span::test_data()); - let expected = Value::string("dXNlcm5hbWU6cGFzc3dvcmQ=", Span::test_data()); + let word = Value::string("Some Data Padding", Span::test_data()); + let expected = Value::string("U29tZSBEYXRhIFBhZGRpbmc=", Span::test_data()); let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "standard".to_string(), @@ -272,11 +200,12 @@ mod tests { #[test] fn base64_encode_standard_no_padding() { - let word = Value::string("username:password", Span::test_data()); - let expected = Value::string("dXNlcm5hbWU6cGFzc3dvcmQ", Span::test_data()); + let word = Value::string("Some Data Padding", Span::test_data()); + let expected = Value::string("U29tZSBEYXRhIFBhZGRpbmc", Span::test_data()); let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "standard-no-padding".to_string(), @@ -297,6 +226,7 @@ mod tests { let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "url-safe".to_string(), @@ -313,10 +243,11 @@ mod tests { #[test] fn base64_decode_binhex() { let word = Value::string("A5\"KC9jRB@IIF'8bF!", Span::test_data()); - let expected = Value::string("a binhex test", Span::test_data()); + let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data()); let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "binhex".to_string(), @@ -333,10 +264,11 @@ mod tests { #[test] fn base64_decode_binhex_with_new_line_input() { let word = Value::string("A5\"KC9jRB\n@IIF'8bF!", Span::test_data()); - let expected = Value::string("a binhex test", Span::test_data()); + let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data()); let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "binhex".to_string(), @@ -360,6 +292,7 @@ mod tests { let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "standard".to_string(), @@ -382,6 +315,7 @@ mod tests { let actual = action( &word, + true, &Base64Config { character_set: Spanned { item: "standard".to_string(), diff --git a/crates/nu-command/src/strings/decode.rs b/crates/nu-command/src/strings/encode_decode/decode.rs similarity index 50% rename from crates/nu-command/src/strings/decode.rs rename to crates/nu-command/src/strings/encode_decode/decode.rs index 6bb1a15524..410108a91e 100644 --- a/crates/nu-command/src/strings/decode.rs +++ b/crates/nu-command/src/strings/encode_decode/decode.rs @@ -1,10 +1,9 @@ -use encoding_rs::Encoding; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, - Value, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, }; #[derive(Clone)] @@ -38,11 +37,21 @@ documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# } fn examples(&self) -> Vec { - vec![Example { - description: "Decode the output of an external command", - example: "cat myfile.q | decode utf-8", - result: None, - }] + vec![ + Example { + description: "Decode the output of an external command", + example: "^cat myfile.q | decode utf-8", + result: None, + }, + Example { + description: "Decode an UTF-16 string into nushell UTF-8 string", + example: r#"0x[00 53 00 6F 00 6D 00 65 00 20 00 44 00 61 00 74 00 61] | decode utf-16be"#, + result: Some(Value::String { + val: "Some Data".to_owned(), + span: Span::test_data(), + }), + }, + ] } fn run( @@ -62,51 +71,10 @@ documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# .. } => { let bytes: Vec = stream.into_bytes()?.item; - - let encoding = match Encoding::for_label(encoding.item.as_bytes()) { - None => Err(ShellError::GenericError( - format!( - r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, - encoding.item - ), - "invalid encoding".into(), - Some(encoding.span), - None, - Vec::new(), - )), - Some(encoding) => Ok(encoding), - }?; - - let result = encoding.decode(&bytes); - - Ok(Value::String { - val: result.0.to_string(), - span: head, - } - .into_pipeline_data()) + super::encoding::decode(head, encoding, &bytes).map(|val| val.into_pipeline_data()) } PipelineData::Value(Value::Binary { val: bytes, .. }, ..) => { - let encoding = match Encoding::for_label(encoding.item.as_bytes()) { - None => Err(ShellError::GenericError( - format!( - r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, - encoding.item - ), - "invalid encoding".into(), - Some(encoding.span), - None, - Vec::new(), - )), - Some(encoding) => Ok(encoding), - }?; - - let result = encoding.decode(&bytes); - - Ok(Value::String { - val: result.0.to_string(), - span: head, - } - .into_pipeline_data()) + super::encoding::decode(head, encoding, &bytes).map(|val| val.into_pipeline_data()) } _ => Err(ShellError::UnsupportedInput( "non-binary input".into(), diff --git a/crates/nu-command/src/strings/encode_decode/decode_base64.rs b/crates/nu-command/src/strings/encode_decode/decode_base64.rs new file mode 100644 index 0000000000..9896d89b93 --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/decode_base64.rs @@ -0,0 +1,90 @@ +use super::base64::{operate, ActionType, CHARACTER_SET_DESC}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct DecodeBase64; + +impl Command for DecodeBase64 { + fn name(&self) -> &str { + "decode base64" + } + + fn signature(&self) -> Signature { + Signature::build("decode base64") + .named( + "character-set", + SyntaxShape::String, + CHARACTER_SET_DESC, + Some('c'), + ) + .switch( + "binary", + "do not decode payload as UTF-8 and output binary", + Some('b'), + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally base64 decode data by column paths", + ) + .category(Category::Hash) + } + + fn usage(&self) -> &str { + "base64 decode a value" + } + + fn extra_usage(&self) -> &str { + r#"Will attempt to decode binary payload as an UTF-8 string by default. Use the `--binary(-b)` argument to force binary output."# + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Base64 decode a value and output as UTF-8 string", + example: "echo 'U29tZSBEYXRh' | decode base64", + result: Some(Value::string("Some Data", Span::test_data())), + }, + Example { + description: "Base64 decode a value and output as binary", + example: "echo 'U29tZSBEYXRh' | decode base64 --binary", + result: Some(Value::binary( + [0x53, 0x6f, 0x6d, 0x65, 0x20, 0x44, 0x61, 0x74, 0x61], + Span::test_data(), + )), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(ActionType::Decode, engine_state, stack, call, input) + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::Any + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(DecodeBase64) + } +} diff --git a/crates/nu-command/src/strings/encode_decode/encode.rs b/crates/nu-command/src/strings/encode_decode/encode.rs new file mode 100644 index 0000000000..989d83d73b --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/encode.rs @@ -0,0 +1,95 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Encode; + +impl Command for Encode { + fn name(&self) -> &str { + "encode" + } + + fn usage(&self) -> &str { + "Encode an UTF-8 string into other kind of representations." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["text", "encoding", "decoding"] + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("encode") + .required("encoding", SyntaxShape::String, "the text encoding to use") + .category(Category::Strings) + } + + fn extra_usage(&self) -> &str { + r#"Multiple encodings are supported, here is an example of a few: +big5, euc-jp, euc-kr, gbk, iso-8859-1, cp1252, latin5 + +Note that since the Encoding Standard doesn't specify encoders for utf-16le and utf-16be, these are not yet supported. + +For a more complete list of encodings please refer to the encoding_rs +documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Encode an UTF-8 string into Shift-JIS", + example: r#"echo "負けると知って戦うのが、遥かに美しいのだ" | encode shift-jis"#, + result: Some(Value::Binary { + val: vec![ + 0x95, 0x89, 0x82, 0xaf, 0x82, 0xe9, 0x82, 0xc6, 0x92, 0x6d, 0x82, 0xc1, 0x82, + 0xc4, 0x90, 0xed, 0x82, 0xa4, 0x82, 0xcc, 0x82, 0xaa, 0x81, 0x41, 0x97, 0x79, + 0x82, 0xa9, 0x82, 0xc9, 0x94, 0xfc, 0x82, 0xb5, 0x82, 0xa2, 0x82, 0xcc, 0x82, + 0xbe, + ], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let encoding: Spanned = call.req(engine_state, stack, 0)?; + + match input { + PipelineData::ExternalStream { stdout: None, .. } => Ok(PipelineData::new(call.head)), + PipelineData::ExternalStream { + stdout: Some(stream), + .. + } => { + let s = stream.into_string()?.item; + super::encoding::encode(head, encoding, &s).map(|val| val.into_pipeline_data()) + } + PipelineData::Value(Value::String { val: s, .. }, ..) => { + super::encoding::encode(head, encoding, &s).map(|val| val.into_pipeline_data()) + } + _ => Err(ShellError::UnsupportedInput( + "non-string input".into(), + head, + )), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(Encode) + } +} diff --git a/crates/nu-command/src/strings/encode_decode/encode_base64.rs b/crates/nu-command/src/strings/encode_decode/encode_base64.rs new file mode 100644 index 0000000000..6ef29fb96a --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/encode_base64.rs @@ -0,0 +1,78 @@ +use super::base64::{operate, ActionType, CHARACTER_SET_DESC}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct EncodeBase64; + +impl Command for EncodeBase64 { + fn name(&self) -> &str { + "encode base64" + } + + fn signature(&self) -> Signature { + Signature::build("encode base64") + .named( + "character-set", + SyntaxShape::String, + CHARACTER_SET_DESC, + Some('c'), + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally base64 encode data by column paths", + ) + .category(Category::Hash) + } + + fn usage(&self) -> &str { + "base64 encode a value" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Base64 encode a string with default settings", + example: "echo 'Some Data' | encode base64", + result: Some(Value::string("U29tZSBEYXRh", Span::test_data())), + }, + Example { + description: "Base64 encode a string with the binhex character set", + example: "echo 'Some Data' | encode base64 --character-set binhex", + result: Some(Value::string(r#"7epXB5"%A@4J"#, Span::test_data())), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(ActionType::Encode, engine_state, stack, call, input) + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::String + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(EncodeBase64) + } +} diff --git a/crates/nu-command/src/strings/encode_decode/encoding.rs b/crates/nu-command/src/strings/encode_decode/encoding.rs new file mode 100644 index 0000000000..bc78833122 --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/encoding.rs @@ -0,0 +1,67 @@ +use encoding_rs::Encoding; +use nu_protocol::{ShellError, Span, Spanned, Value}; + +pub fn decode(head: Span, encoding: Spanned, bytes: &[u8]) -> Result { + let encoding = parse_encoding(encoding.span, &encoding.item)?; + let (result, ..) = encoding.decode(bytes); + Ok(Value::String { + val: result.into_owned(), + span: head, + }) +} + +pub fn encode(head: Span, encoding: Spanned, s: &str) -> Result { + let encoding = parse_encoding(encoding.span, &encoding.item)?; + let (result, ..) = encoding.encode(s); + Ok(Value::Binary { + val: result.into_owned(), + span: head, + }) +} + +fn parse_encoding(span: Span, label: &str) -> Result<&'static Encoding, ShellError> { + match Encoding::for_label_no_replacement(label.as_bytes()) { + None => Err(ShellError::GenericError( + format!( + r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, + label + ), + "invalid encoding".into(), + Some(span), + None, + Vec::new(), + )), + Some(encoding) => Ok(encoding), + } +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case::big5("big5", "簡体字")] + #[case::shift_jis("shift-jis", "何だと?……無駄な努力だ?……百も承知だ!")] + #[case::euc_jp("euc-jp", "だがな、勝つ望みがある時ばかり、戦うのとは訳が違うぞ!")] + #[case::euc_kr("euc-kr", "가셨어요?")] + #[case::gbk("gbk", "簡体字")] + #[case::iso_8859_1("iso-8859-1", "Some ¼½¿ Data µ¶·¸¹º")] + #[case::cp1252("cp1252", "Some ¼½¿ Data")] + #[case::latin5("latin5", "Some ¼½¿ Data µ¶·¸¹º")] + fn smoke(#[case] encoding: String, #[case] expected: &str) { + let test_span = Span::test_data(); + let encoding = Spanned { + item: encoding, + span: test_span, + }; + + let encoded = encode(test_span, encoding.clone(), expected).unwrap(); + let encoded = encoded.as_binary().unwrap(); + + let decoded = decode(test_span, encoding, encoded).unwrap(); + let decoded = decoded.as_string().unwrap(); + + assert_eq!(decoded, expected); + } +} diff --git a/crates/nu-command/src/strings/encode_decode/mod.rs b/crates/nu-command/src/strings/encode_decode/mod.rs new file mode 100644 index 0000000000..e5d0ea06a6 --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/mod.rs @@ -0,0 +1,11 @@ +mod base64; +mod decode; +mod decode_base64; +mod encode; +mod encode_base64; +mod encoding; + +pub use self::decode::Decode; +pub use self::decode_base64::DecodeBase64; +pub use self::encode::Encode; +pub use self::encode_base64::EncodeBase64; diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index 1cdcb4a18f..3556e54727 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -1,7 +1,7 @@ mod build_string; mod char_; -mod decode; mod detect_columns; +mod encode_decode; mod format; mod parse; mod size; @@ -10,8 +10,8 @@ mod str_; pub use build_string::BuildString; pub use char_::Char; -pub use decode::*; pub use detect_columns::*; +pub use encode_decode::*; pub use format::*; pub use parse::*; pub use size::Size; diff --git a/crates/nu-command/tests/commands/hash_/mod.rs b/crates/nu-command/tests/commands/hash_/mod.rs index 51840b3851..e60a6a6e63 100644 --- a/crates/nu-command/tests/commands/hash_/mod.rs +++ b/crates/nu-command/tests/commands/hash_/mod.rs @@ -5,7 +5,7 @@ fn base64_defaults_to_encoding_with_standard_character_type() { let actual = nu!( cwd: ".", pipeline( r#" - echo 'username:password' | hash base64 + echo 'username:password' | encode base64 "# ) ); @@ -18,7 +18,7 @@ fn base64_encode_characterset_binhex() { let actual = nu!( cwd: ".", pipeline( r#" - echo 'username:password' | hash base64 --character-set binhex --encode + echo 'username:password' | encode base64 --character-set binhex "# ) ); @@ -31,7 +31,7 @@ fn error_when_invalid_character_set_given() { let actual = nu!( cwd: ".", pipeline( r#" - echo 'username:password' | hash base64 --character-set 'this is invalid' --encode + echo 'username:password' | encode base64 --character-set 'this is invalid' "# ) ); @@ -46,7 +46,7 @@ fn base64_decode_characterset_binhex() { let actual = nu!( cwd: ".", pipeline( r#" - echo "F@0NEPjJD97kE'&bEhFZEP3" | hash base64 --character-set binhex --decode + echo "F@0NEPjJD97kE'&bEhFZEP3" | decode base64 --character-set binhex --binary | decode utf-8 "# ) ); @@ -59,7 +59,7 @@ fn error_invalid_decode_value() { let actual = nu!( cwd: ".", pipeline( r#" - echo "this should not be a valid encoded value" | hash base64 --character-set url-safe --decode + echo "this should not be a valid encoded value" | decode base64 --character-set url-safe "# ) ); @@ -69,21 +69,6 @@ fn error_invalid_decode_value() { .contains("invalid base64 input for character set url-safe")); } -#[test] -fn error_use_both_flags() { - let actual = nu!( - cwd: ".", pipeline( - r#" - echo 'username:password' | hash base64 --encode --decode - "# - ) - ); - - assert!(actual - .err - .contains("only one of --decode and --encode flags can be used")); -} - #[test] fn md5_works_with_file() { let actual = nu!( diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index c301cd6f87..6922759397 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1035,6 +1035,13 @@ impl Value { } } + pub fn binary(val: impl Into>, span: Span) -> Value { + Value::Binary { + val: val.into(), + span, + } + } + pub fn int(val: i64, span: Span) -> Value { Value::Int { val, span } }