diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4e9716ee28..b82a77b545 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -314,6 +314,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Hash, HashMd5::default(), HashSha256::default(), + Base64, }; #[cfg(feature = "plugin")] diff --git a/crates/nu-command/src/hash/base64.rs b/crates/nu-command/src/hash/base64.rs new file mode 100644 index 0000000000..0a5ccc424e --- /dev/null +++ b/crates/nu-command/src/hash/base64.rs @@ -0,0 +1,299 @@ +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, +}; + +#[derive(Clone)] +pub struct Base64Config { + pub character_set: String, + pub action_type: ActionType, +} + +#[derive(Clone, Copy, PartialEq)] +pub enum ActionType { + Encode, + 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( + 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 column_paths: Vec = call.rest(engine_state, stack, 0)?; + + if encode && decode { + return Err(ShellError::SpannedLabeledError( + "only one of --decode and --encode flags can be used".to_string(), + "conflicting flags".to_string(), + head, + )); + } + + // 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.item, + None => "standard".to_string(), + }; + + let encoding_config = Base64Config { + character_set, + action_type, + }; + + input.map( + move |v| { + if column_paths.is_empty() { + match action(&v, &encoding_config, &head) { + Ok(v) => v, + Err(e) => Value::Error { error: e }, + } + } else { + let mut ret = v; + + for path in &column_paths { + let config = encoding_config.clone(); + + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| match action(old, &config, &head) { + Ok(v) => v, + Err(e) => Value::Error { error: e }, + }), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + base64_config: &Base64Config, + command_span: &Span, +) -> Result { + match input { + Value::String { val, span } => { + let base64_config_enum: base64::Config = if &base64_config.character_set == "standard" { + base64::STANDARD + } else if &base64_config.character_set == "standard-no-padding" { + base64::STANDARD_NO_PAD + } else if &base64_config.character_set == "url-safe" { + base64::URL_SAFE + } else if &base64_config.character_set == "url-safe-no-padding" { + base64::URL_SAFE_NO_PAD + } else if &base64_config.character_set == "binhex" { + base64::BINHEX + } else if &base64_config.character_set == "bcrypt" { + base64::BCRYPT + } else if &base64_config.character_set == "crypt" { + base64::CRYPT + } else { + return Err(ShellError::SpannedLabeledError( + "value is not an accepted character set".to_string(), + format!( + "{} is not a valid character-set.\nPlease use `help hash base64` to see a list of valid character sets.", + &base64_config.character_set + ), + *span, + )); + }; + + match base64_config.action_type { + ActionType::Encode => Ok(Value::string( + encode_config(&val, base64_config_enum), + *command_span, + )), + + ActionType::Decode => { + 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, + )), + Err(_) => Err(ShellError::SpannedLabeledError( + "value could not be base64 decoded".to_string(), + format!( + "invalid base64 input for character set {}", + &base64_config.character_set + ), + *command_span, + )), + } + } + } + } + other => Err(ShellError::TypeMismatch( + format!("value is {}, not string", other.get_type()), + other.span()?, + )), + } +} + +#[cfg(test)] +mod tests { + use super::{action, ActionType, Base64Config}; + use nu_protocol::{Span, Value}; + + #[test] + fn base64_encode_standard() { + let word = Value::string("username:password", Span::test_data()); + let expected = Value::string("dXNlcm5hbWU6cGFzc3dvcmQ=", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "standard".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[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 actual = action( + &word, + &Base64Config { + character_set: "standard-no-padding".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn base64_encode_url_safe() { + let word = Value::string("this is for url", Span::test_data()); + let expected = Value::string("dGhpcyBpcyBmb3IgdXJs", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "url-safe".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[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 actual = action( + &word, + &Base64Config { + character_set: "binhex".to_string(), + action_type: ActionType::Decode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/hash/mod.rs b/crates/nu-command/src/hash/mod.rs index e3aca085fa..01c97765b2 100644 --- a/crates/nu-command/src/hash/mod.rs +++ b/crates/nu-command/src/hash/mod.rs @@ -1,8 +1,10 @@ +mod base64; mod command; mod generic_digest; mod md5; mod sha256; +pub use self::base64::Base64; pub use self::command::Hash; pub use self::md5::HashMd5; pub use self::sha256::HashSha256;