use crate::input_handler::{operate as general_operate, CmdArgument}; use base64::{ alphabet, engine::general_purpose::NO_PAD, engine::general_purpose::PAD, engine::GeneralPurpose, Engine, }; use nu_engine::CallExt; use nu_protocol::ast::{Call, CellPath}; 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', 'mutf7'"; #[derive(Clone)] pub struct Base64Config { pub character_set: Spanned, pub action_type: ActionType, } #[derive(Clone, Copy, PartialEq, Eq)] pub enum ActionType { Encode, Decode, } struct Arguments { cell_paths: Option>, binary: bool, encoding_config: Base64Config, } impl CmdArgument for Arguments { fn take_cell_paths(&mut self) -> Option> { self.cell_paths.take() } } pub fn operate( action_type: ActionType, engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let head = call.head; let character_set: Option> = call.get_flag(engine_state, stack, "character-set")?; let binary = call.has_flag("binary"); let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); // Default the character set to standard if the argument is not specified. let character_set = match character_set { Some(inner_tag) => inner_tag, None => Spanned { item: "standard".to_string(), span: head, // actually this span is always useless, because default character_set is always valid. }, }; let args = Arguments { encoding_config: Base64Config { character_set, action_type, }, binary, cell_paths, }; general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) } fn action( input: &Value, // only used for `decode` action args: &Arguments, command_span: Span, ) -> Value { let base64_config = &args.encoding_config; let output_binary = args.binary; let config_character_set = &base64_config.character_set; let base64_engine: GeneralPurpose = match config_character_set.item.as_str() { "standard" => GeneralPurpose::new(&alphabet::STANDARD, PAD), "standard-no-padding" => GeneralPurpose::new(&alphabet::STANDARD, NO_PAD), "url-safe" => GeneralPurpose::new(&alphabet::URL_SAFE, PAD), "url-safe-no-padding" => GeneralPurpose::new(&alphabet::URL_SAFE, NO_PAD), "bcrypt" => GeneralPurpose::new(&alphabet::BCRYPT, NO_PAD), "binhex" => GeneralPurpose::new(&alphabet::BIN_HEX, NO_PAD), "crypt" => GeneralPurpose::new(&alphabet::CRYPT, NO_PAD), "mutf7" => GeneralPurpose::new(&alphabet::IMAP_MUTF7, NO_PAD), not_valid => return Value::Error { error:ShellError::GenericError( "value is not an accepted character set".to_string(), format!( "{not_valid} is not a valid character-set.\nPlease use `help hash base64` to see a list of valid character sets." ), Some(config_character_set.span), None, Vec::new(), )} }; match input { // Propagate existing errors. Value::Error { .. } => input.clone(), Value::Binary { val, .. } => match base64_config.action_type { ActionType::Encode => { let mut enc_vec = Vec::new(); enc_vec.resize(val.len() * 4 / 3 + 4, 0); let bytes_written = match base64_engine.encode_slice(val, &mut enc_vec) { Ok(bytes_written) => bytes_written, Err(err) => { return Value::Error { error: ShellError::GenericError( "Error encoding data".into(), err.to_string(), Some(Span::unknown()), None, Vec::new(), ), } } }; enc_vec.truncate(bytes_written); Value::string(std::str::from_utf8(&enc_vec).unwrap_or(""), command_span) } ActionType::Decode => Value::Error { error: ShellError::UnsupportedInput( "Binary data can only be encoded".to_string(), "value originates from here".into(), command_span, // This line requires the Value::Error {} match above. input.expect_span(), ), }, }, Value::String { val, span: value_span, } => { match base64_config.action_type { ActionType::Encode => { let mut enc_str = String::new(); base64_engine.encode_string(val, &mut enc_str); Value::string(enc_str, command_span) } ActionType::Decode => { // 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', ""); match base64_engine.decode(val) { Ok(decoded_value) => { if output_binary { Value::binary(decoded_value, command_span) } else { match String::from_utf8(decoded_value) { Ok(string_value) => Value::string(string_value, command_span), Err(e) => Value::Error { error: 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(_) => Value::Error { error: ShellError::GenericError( "value could not be base64 decoded".to_string(), format!( "invalid base64 input for character set {}", &config_character_set.item ), Some(command_span), None, Vec::new(), ), }, } } } } other => Value::Error { error: ShellError::TypeMismatch( format!("string or binary, not {}", other.get_type()), other.span().unwrap_or(command_span), ), }, } } #[cfg(test)] mod tests { use super::{action, ActionType, Arguments, Base64Config}; use nu_protocol::{Span, Spanned, Value}; #[test] fn base64_encode_standard() { let word = Value::test_string("Some Data Padding"); let expected = Value::test_string("U29tZSBEYXRhIFBhZGRpbmc="); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "standard".to_string(), span: Span::test_data(), }, action_type: ActionType::Encode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_encode_standard_no_padding() { let word = Value::test_string("Some Data Padding"); let expected = Value::test_string("U29tZSBEYXRhIFBhZGRpbmc"); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "standard-no-padding".to_string(), span: Span::test_data(), }, action_type: ActionType::Encode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_encode_url_safe() { let word = Value::test_string("this is for url"); let expected = Value::test_string("dGhpcyBpcyBmb3IgdXJs"); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "url-safe".to_string(), span: Span::test_data(), }, action_type: ActionType::Encode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_decode_binhex() { let word = Value::test_string("A5\"KC9jRB@IIF'8bF!"); let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data()); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "binhex".to_string(), span: Span::test_data(), }, action_type: ActionType::Decode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_decode_binhex_with_new_line_input() { let word = Value::test_string("A5\"KC9jRB\n@IIF'8bF!"); let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data()); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "binhex".to_string(), span: Span::test_data(), }, action_type: ActionType::Decode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_encode_binary() { let word = Value::Binary { val: vec![77, 97, 110], span: Span::test_data(), }; let expected = Value::test_string("TWFu"); let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "standard".to_string(), span: Span::test_data(), }, action_type: ActionType::Encode, }, binary: true, cell_paths: None, }, Span::test_data(), ); assert_eq!(actual, expected); } #[test] fn base64_decode_binary_expect_error() { let word = Value::Binary { val: vec![77, 97, 110], span: Span::test_data(), }; let actual = action( &word, &Arguments { encoding_config: Base64Config { character_set: Spanned { item: "standard".to_string(), span: Span::test_data(), }, action_type: ActionType::Decode, }, binary: true, cell_paths: None, }, Span::test_data(), ); match actual { Value::Error { .. } => {} _ => panic!("the result should be Value::Error"), } } }