From d0aa69bfcb2d14d10c7849302ff330b18d6672a1 Mon Sep 17 00:00:00 2001 From: uaeio <119953648+uaeio@users.noreply.github.com> Date: Fri, 24 Mar 2023 13:25:26 +0200 Subject: [PATCH] Decode and Encode hex (#8392) # Description I need a command that will transform hex string into bytes and into other direction. I've implemented `decode hex` command and `encode hex` command. (Based on `encode base64` and `decode base64` commands # User-Facing Changes ``` > '010203' | decode hex 0x[01 02 03] ``` and ``` > 0x[01 02 0a] | encode hex '01020A' ``` --------- Co-authored-by: whiteand --- crates/nu-command/src/default_context.rs | 2 + .../src/strings/encode_decode/decode_hex.rs | 72 +++++++ .../src/strings/encode_decode/encode_hex.rs | 60 ++++++ .../src/strings/encode_decode/hex.rs | 204 ++++++++++++++++++ .../src/strings/encode_decode/mod.rs | 5 + 5 files changed, 343 insertions(+) create mode 100644 crates/nu-command/src/strings/encode_decode/decode_hex.rs create mode 100644 crates/nu-command/src/strings/encode_decode/encode_hex.rs create mode 100644 crates/nu-command/src/strings/encode_decode/hex.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4fe22cbfe8..1e1a6de8fd 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -168,6 +168,8 @@ pub fn create_default_context() -> EngineState { Encode, DecodeBase64, EncodeBase64, + DecodeHex, + EncodeHex, DetectColumns, Format, FileSize, diff --git a/crates/nu-command/src/strings/encode_decode/decode_hex.rs b/crates/nu-command/src/strings/encode_decode/decode_hex.rs new file mode 100644 index 0000000000..a0529ad18c --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/decode_hex.rs @@ -0,0 +1,72 @@ +use super::hex::{operate, ActionType}; +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 DecodeHex; + +impl Command for DecodeHex { + fn name(&self) -> &str { + "decode hex" + } + + fn signature(&self) -> Signature { + Signature::build("decode hex") + .input_output_types(vec![(Type::String, Type::Binary)]) + .vectorizes_over_list(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, decode data at the given cell paths", + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Hex decode a value." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Hex decode a value and output as binary", + example: "'0102030A0a0B' | decode hex", + result: Some(Value::binary( + [0x01, 0x02, 0x03, 0x0A, 0x0A, 0x0B], + Span::test_data(), + )), + }, + Example { + description: "Whitespaces are allowed to be between hex digits", + example: "'01 02 03 0A 0a 0B' | decode hex", + result: Some(Value::binary( + [0x01, 0x02, 0x03, 0x0A, 0x0A, 0x0B], + 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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(DecodeHex) + } +} diff --git a/crates/nu-command/src/strings/encode_decode/encode_hex.rs b/crates/nu-command/src/strings/encode_decode/encode_hex.rs new file mode 100644 index 0000000000..cb9f350bc2 --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/encode_hex.rs @@ -0,0 +1,60 @@ +use super::hex::{operate, ActionType}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct EncodeHex; + +impl Command for EncodeHex { + fn name(&self) -> &str { + "encode hex" + } + + fn signature(&self) -> Signature { + Signature::build("encode hex") + .input_output_types(vec![(Type::Binary, Type::String)]) + .vectorizes_over_list(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, encode data at the given cell paths", + ) + .output_type(Type::String) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Encode a binary value using hex." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Encode binary data", + example: "0x[09 F9 11 02 9D 74 E3 5B D8 41 56 C5 63 56 88 C0] | encode hex", + result: Some(Value::test_string("09F911029D74E35BD84156C5635688C0")), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(ActionType::Encode, engine_state, stack, call, input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(EncodeHex) + } +} diff --git a/crates/nu-command/src/strings/encode_decode/hex.rs b/crates/nu-command/src/strings/encode_decode/hex.rs new file mode 100644 index 0000000000..56d8f997c7 --- /dev/null +++ b/crates/nu-command/src/strings/encode_decode/hex.rs @@ -0,0 +1,204 @@ +use crate::input_handler::{operate as general_operate, CmdArgument}; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{PipelineData, ShellError, Span, Value}; + +enum HexDecodingError { + InvalidLength(usize), + InvalidDigit(usize, char), +} + +fn hex_decode(value: &str) -> Result, HexDecodingError> { + let mut digits = value + .chars() + .enumerate() + .filter(|(_, c)| !c.is_whitespace()); + + let mut res = Vec::with_capacity(value.len() / 2); + loop { + let c1 = match digits.next() { + Some((ind, c)) => match c.to_digit(16) { + Some(d) => d, + None => return Err(HexDecodingError::InvalidDigit(ind, c)), + }, + None => return Ok(res), + }; + let c2 = match digits.next() { + Some((ind, c)) => match c.to_digit(16) { + Some(d) => d, + None => return Err(HexDecodingError::InvalidDigit(ind, c)), + }, + None => { + return Err(HexDecodingError::InvalidLength(value.len())); + } + }; + res.push((c1 << 4 | c2) as u8); + } +} + +fn hex_digit(num: u8) -> char { + match num { + 0..=9 => (num + b'0') as char, + 10..=15 => (num - 10 + b'A') as char, + _ => unreachable!(), + } +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut res = String::with_capacity(bytes.len() * 2); + for byte in bytes { + res.push(hex_digit(byte >> 4)); + res.push(hex_digit(byte & 0b1111)); + } + res +} + +#[derive(Clone)] +pub struct HexConfig { + pub action_type: ActionType, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ActionType { + Encode, + Decode, +} + +struct Arguments { + cell_paths: Option>, + encoding_config: HexConfig, +} + +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 cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + + let args = Arguments { + encoding_config: HexConfig { action_type }, + 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 hex_config = &args.encoding_config; + + match input { + // Propagate existing errors. + Value::Error { .. } => input.clone(), + Value::Binary { val, .. } => match hex_config.action_type { + ActionType::Encode => Value::string(hex_encode(val.as_ref()), command_span), + ActionType::Decode => Value::Error { + error: Box::new(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, .. } => { + match hex_config.action_type { + ActionType::Encode => Value::Error { + error: Box::new(ShellError::UnsupportedInput( + "String value can only be decoded".to_string(), + "value originates from here".into(), + command_span, + // This line requires the Value::Error {} match above. + input.expect_span(), + )), + }, + + ActionType::Decode => match hex_decode(val.as_ref()) { + Ok(decoded_value) => Value::binary(decoded_value, command_span), + Err(HexDecodingError::InvalidLength(len)) => Value::Error { + error: Box::new(ShellError::GenericError( + "value could not be hex decoded".to_string(), + format!("invalid hex input length: {len}. The length should be even"), + Some(command_span), + None, + Vec::new(), + )), + }, + Err(HexDecodingError::InvalidDigit(index, digit)) => Value::Error { + error: Box::new(ShellError::GenericError( + "value could not be hex decoded".to_string(), + format!("invalid hex digit: '{digit}' at index {index}. Only 0-9, A-F, a-f are allowed in hex encoding"), + Some(command_span), + None, + Vec::new(), + )), + }, + }, + } + } + other => Value::Error { + error: Box::new(ShellError::TypeMismatch { + err_message: format!("string or binary, not {}", other.get_type()), + span: other.span().unwrap_or(command_span), + }), + }, + } +} + +#[cfg(test)] +mod tests { + use super::{action, ActionType, Arguments, HexConfig}; + use nu_protocol::{Span, Value}; + + #[test] + fn hex_encode() { + let word = Value::binary([77, 97, 110], Span::test_data()); + let expected = Value::test_string("4D616E"); + + let actual = action( + &word, + &Arguments { + encoding_config: HexConfig { + action_type: ActionType::Encode, + }, + cell_paths: None, + }, + Span::test_data(), + ); + assert_eq!(actual, expected); + } + + #[test] + fn hex_decode() { + let word = Value::test_string("4D 61\r\n\n6E"); + let expected = Value::binary([77, 97, 110], Span::test_data()); + + let actual = action( + &word, + &Arguments { + encoding_config: HexConfig { + action_type: ActionType::Decode, + }, + cell_paths: None, + }, + Span::test_data(), + ); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/strings/encode_decode/mod.rs b/crates/nu-command/src/strings/encode_decode/mod.rs index e5d0ea06a6..3873c7b546 100644 --- a/crates/nu-command/src/strings/encode_decode/mod.rs +++ b/crates/nu-command/src/strings/encode_decode/mod.rs @@ -1,11 +1,16 @@ mod base64; mod decode; mod decode_base64; +mod decode_hex; mod encode; mod encode_base64; +mod encode_hex; mod encoding; +mod hex; pub use self::decode::Decode; pub use self::decode_base64::DecodeBase64; +pub use self::decode_hex::DecodeHex; pub use self::encode::Encode; pub use self::encode_base64::EncodeBase64; +pub use self::encode_hex::EncodeHex;