From d3895d71db6f909b0a8adfbb9dc7b8eaa078cbd8 Mon Sep 17 00:00:00 2001 From: moonlander <111705932+astral-l@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:43:50 +0000 Subject: [PATCH] add binary data handling to `bits` commands (#11854) # Description - enables `bits` commands to operate on binary data, where both inputs are binary and can vary in length - adds an `--endian` flag to `bits and`, `or`, `xor` for specifying endianness (for binary values of different lengths) # User-Facing Changes - `bits` commands will no longer error for non-int inputs - the default for `--number-bytes` is now `auto` (infer int size; changed from 8) # Tests + Formatting > addendum: first PR, please inform if any changes are needed --- Cargo.lock | 1 + crates/nu-cmd-extra/Cargo.toml | 1 + crates/nu-cmd-extra/src/extra/bits/and.rs | 99 ++++++--- crates/nu-cmd-extra/src/extra/bits/mod.rs | 96 +++++++-- crates/nu-cmd-extra/src/extra/bits/not.rs | 99 +++++---- crates/nu-cmd-extra/src/extra/bits/or.rs | 90 ++++++--- .../src/extra/bits/rotate_left.rs | 158 +++++++++------ .../src/extra/bits/rotate_right.rs | 172 +++++++++------- .../nu-cmd-extra/src/extra/bits/shift_left.rs | 191 ++++++++++-------- .../src/extra/bits/shift_right.rs | 166 ++++++++------- crates/nu-cmd-extra/src/extra/bits/xor.rs | 91 ++++++--- src/tests/test_bits.rs | 6 +- 12 files changed, 742 insertions(+), 428 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7b87e7888..2eef3868fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2910,6 +2910,7 @@ version = "0.90.2" dependencies = [ "fancy-regex", "heck", + "itertools 0.12.0", "nu-ansi-term", "nu-cmd-base", "nu-cmd-lang", diff --git a/crates/nu-cmd-extra/Cargo.toml b/crates/nu-cmd-extra/Cargo.toml index 720a645b64..cf7d5fe736 100644 --- a/crates/nu-cmd-extra/Cargo.toml +++ b/crates/nu-cmd-extra/Cargo.toml @@ -30,6 +30,7 @@ nu-pretty-hex = { version = "0.90.2", path = "../nu-pretty-hex" } nu-json = { version = "0.90.2", path = "../nu-json" } serde_urlencoded = "0.7.1" v_htmlescape = "0.15.0" +itertools = "0.12" [features] extra = ["default"] diff --git a/crates/nu-cmd-extra/src/extra/bits/and.rs b/crates/nu-cmd-extra/src/extra/bits/and.rs index fdfb70d98d..9b5b1e09bd 100644 --- a/crates/nu-cmd-extra/src/extra/bits/and.rs +++ b/crates/nu-cmd-extra/src/extra/bits/and.rs @@ -1,8 +1,9 @@ +use super::binary_op; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, }; #[derive(Clone)] @@ -17,17 +18,32 @@ impl Command for BitsAnd { Signature::build("bits and") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) - .required("target", SyntaxShape::Int, "target int to perform bit and") + .required( + "target", + SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::Int]), + "right-hand side of the operation", + ) + .named( + "endian", + SyntaxShape::String, + "byte encode endian, available options: native(default), little, big", + Some('e'), + ) .category(Category::Bits) } fn usage(&self) -> &str { - "Performs bitwise and for ints." + "Performs bitwise and for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -42,14 +58,32 @@ impl Command for BitsAnd { input: PipelineData, ) -> Result { let head = call.head; - let target: i64 = call.req(engine_state, stack, 0)?; + let target: Value = call.req(engine_state, stack, 0)?; + let endian = call.get_flag::>(engine_state, stack, "endian")?; + + let little_endian = if let Some(endian) = endian { + match endian.item.as_str() { + "native" => cfg!(target_endian = "little"), + "little" => true, + "big" => false, + _ => { + return Err(ShellError::TypeMismatch { + err_message: "Endian must be one of native, little, big".to_string(), + span: endian.span, + }) + } + } + } else { + cfg!(target_endian = "little") + }; // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } + input.map( - move |value| operate(value, target, head), + move |value| binary_op(&value, &target, little_endian, |(l, r)| l & r, head), engine_state.ctrlc.clone(), ) } @@ -57,40 +91,47 @@ impl Command for BitsAnd { fn examples(&self) -> Vec { vec![ Example { - description: "Apply bits and to two numbers", + description: "Apply bitwise and to two numbers", example: "2 | bits and 2", result: Some(Value::test_int(2)), }, Example { - description: "Apply logical and to a list of numbers", + description: "Apply bitwise and to two binary values", + example: "0x[ab cd] | bits and 0x[99 99]", + result: Some(Value::test_binary([0x89, 0x89])), + }, + Example { + description: "Apply bitwise and to a list of numbers", example: "[4 3 2] | bits and 2", - result: Some(Value::list( - vec![Value::test_int(0), Value::test_int(2), Value::test_int(2)], - Span::test_data(), - )), + result: Some(Value::test_list(vec![ + Value::test_int(0), + Value::test_int(2), + Value::test_int(2), + ])), + }, + Example { + description: "Apply bitwise and to a list of binary data", + example: "[0x[7f ff] 0x[ff f0]] | bits and 0x[99 99]", + result: Some(Value::test_list(vec![ + Value::test_binary([0x19, 0x99]), + Value::test_binary([0x99, 0x90]), + ])), + }, + Example { + description: + "Apply bitwise and to binary data of varying lengths with specified endianness", + example: "0x[c0 ff ee] | bits and 0x[ff] --endian big", + result: Some(Value::test_binary(vec![0x00, 0x00, 0xee])), + }, + Example { + description: "Apply bitwise and to input binary data smaller than the operand", + example: "0x[ff] | bits and 0x[12 34 56] --endian little", + result: Some(Value::test_binary(vec![0x12, 0x00, 0x00])), }, ] } } -fn operate(value: Value, target: i64, head: Span) -> Value { - let span = value.span(); - match value { - Value::Int { val, .. } => Value::int(val & target, span), - // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, - other => Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), - wrong_type: other.get_type().to_string(), - dst_span: head, - src_span: other.span(), - }, - head, - ), - } -} - #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-cmd-extra/src/extra/bits/mod.rs b/crates/nu-cmd-extra/src/extra/bits/mod.rs index bac80da701..5f1998b7ca 100644 --- a/crates/nu-cmd-extra/src/extra/bits/mod.rs +++ b/crates/nu-cmd-extra/src/extra/bits/mod.rs @@ -13,6 +13,7 @@ pub use and::BitsAnd; pub use bits_::Bits; pub use into::BitsInto; pub use not::BitsNot; +use nu_protocol::{ShellError, Value}; pub use or::BitsOr; pub use rotate_left::BitsRol; pub use rotate_right::BitsRor; @@ -20,7 +21,8 @@ pub use shift_left::BitsShl; pub use shift_right::BitsShr; pub use xor::BitsXor; -use nu_protocol::Spanned; +use nu_protocol::{Span, Spanned}; +use std::iter; #[derive(Clone, Copy)] enum NumberBytes { @@ -29,7 +31,6 @@ enum NumberBytes { Four, Eight, Auto, - Invalid, } #[derive(Clone, Copy)] @@ -44,17 +45,22 @@ enum InputNumType { SignedEight, } -fn get_number_bytes(number_bytes: Option<&Spanned>) -> NumberBytes { - match number_bytes.as_ref() { - None => NumberBytes::Eight, - Some(size) => match size.item.as_str() { - "1" => NumberBytes::One, - "2" => NumberBytes::Two, - "4" => NumberBytes::Four, - "8" => NumberBytes::Eight, - "auto" => NumberBytes::Auto, - _ => NumberBytes::Invalid, - }, +fn get_number_bytes( + number_bytes: Option>, + head: Span, +) -> Result { + match number_bytes { + None => Ok(NumberBytes::Auto), + Some(Spanned { item: 1, .. }) => Ok(NumberBytes::One), + Some(Spanned { item: 2, .. }) => Ok(NumberBytes::Two), + Some(Spanned { item: 4, .. }) => Ok(NumberBytes::Four), + Some(Spanned { item: 8, .. }) => Ok(NumberBytes::Eight), + Some(Spanned { span, .. }) => Err(ShellError::UnsupportedInput { + msg: "Only 1, 2, 4, or 8 bytes are supported as word sizes".to_string(), + input: "value originates from here".to_string(), + msg_span: head, + input_span: span, + }), } } @@ -76,7 +82,6 @@ fn get_input_num_type(val: i64, signed: bool, number_size: NumberBytes) -> Input InputNumType::SignedEight } } - NumberBytes::Invalid => InputNumType::SignedFour, } } else { match number_size { @@ -95,7 +100,68 @@ fn get_input_num_type(val: i64, signed: bool, number_size: NumberBytes) -> Input InputNumType::Eight } } - NumberBytes::Invalid => InputNumType::Four, } } } + +fn binary_op(lhs: &Value, rhs: &Value, little_endian: bool, f: F, head: Span) -> Value +where + F: Fn((i64, i64)) -> i64, +{ + let span = lhs.span(); + match (lhs, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + Value::int(f((*lhs, *rhs)), span) + } + (Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => { + let (lhs, rhs, max_len, min_len) = match (lhs.len(), rhs.len()) { + (max, min) if max > min => (lhs, rhs, max, min), + (min, max) => (rhs, lhs, max, min), + }; + + let pad = iter::repeat(0).take(max_len - min_len); + + let mut a; + let mut b; + + let padded: &mut dyn Iterator = if little_endian { + a = rhs.iter().copied().chain(pad); + &mut a + } else { + b = pad.chain(rhs.iter().copied()); + &mut b + }; + + let bytes: Vec = lhs + .iter() + .copied() + .zip(padded) + .map(|(lhs, rhs)| f((lhs as i64, rhs as i64)) as u8) + .collect(); + + Value::binary(bytes, span) + } + (Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => { + Value::error( + ShellError::PipelineMismatch { + exp_input_type: "input, and argument, to be both int or both binary" + .to_string(), + dst_span: rhs.span(), + src_span: span, + }, + span, + ) + } + // Propagate errors by explicitly matching them before the final case. + (e @ Value::Error { .. }, _) | (_, e @ Value::Error { .. }) => e.clone(), + (other, Value::Int { .. } | Value::Binary { .. }) | (_, other) => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "int or binary".into(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: other.span(), + }, + span, + ), + } +} diff --git a/crates/nu-cmd-extra/src/extra/bits/not.rs b/crates/nu-cmd-extra/src/extra/bits/not.rs index 24d1116790..9be083be7d 100644 --- a/crates/nu-cmd-extra/src/extra/bits/not.rs +++ b/crates/nu-cmd-extra/src/extra/bits/not.rs @@ -1,6 +1,7 @@ use super::{get_number_bytes, NumberBytes}; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; -use nu_protocol::ast::Call; +use nu_protocol::ast::{Call, CellPath}; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, @@ -9,6 +10,18 @@ use nu_protocol::{ #[derive(Clone)] pub struct BitsNot; +#[derive(Clone, Copy)] +struct Arguments { + signed: bool, + number_size: NumberBytes, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + None + } +} + impl Command for BitsNot { fn name(&self) -> &str { "bits not" @@ -18,10 +31,15 @@ impl Command for BitsNot { Signature::build("bits not") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) .allow_variants_without_examples(true) .switch( @@ -31,7 +49,7 @@ impl Command for BitsNot { ) .named( "number-bytes", - SyntaxShape::String, + SyntaxShape::Int, "the size of unsigned number in bytes, it can be 1, 2, 4, 8, auto", Some('n'), ) @@ -55,28 +73,21 @@ impl Command for BitsNot { ) -> Result { let head = call.head; let signed = call.has_flag(engine_state, stack, "signed")?; - let number_bytes: Option> = + let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let bytes_len = get_number_bytes(number_bytes.as_ref()); - if let NumberBytes::Invalid = bytes_len { - if let Some(val) = number_bytes { - return Err(ShellError::UnsupportedInput { - msg: "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), - input: "value originates from here".to_string(), - msg_span: head, - input_span: val.span, - }); - } - } + let number_size = get_number_bytes(number_bytes, head)?; // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head, signed, bytes_len), - engine_state.ctrlc.clone(), - ) + + let args = Arguments { + signed, + number_size, + }; + + operate(action, args, input, head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { @@ -86,9 +97,9 @@ impl Command for BitsNot { example: "[4 3 2] | bits not", result: Some(Value::list( vec![ - Value::test_int(140737488355323), - Value::test_int(140737488355324), - Value::test_int(140737488355325), + Value::test_int(251), + Value::test_int(252), + Value::test_int(253), ], Span::test_data(), )), @@ -96,7 +107,7 @@ impl Command for BitsNot { Example { description: "Apply logical negation to a list of numbers, treat input as 2 bytes number", - example: "[4 3 2] | bits not --number-bytes '2'", + example: "[4 3 2] | bits not --number-bytes 2", result: Some(Value::list( vec![ Value::test_int(65531), @@ -119,14 +130,23 @@ impl Command for BitsNot { Span::test_data(), )), }, + Example { + description: "Apply logical negation to binary data", + example: "0x[ff 00 7f] | bits not", + result: Some(Value::binary(vec![0x00, 0xff, 0x80], Span::test_data())), + }, ] } } -fn operate(value: Value, head: Span, signed: bool, number_size: NumberBytes) -> Value { - let span = value.span(); - match value { +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + let Arguments { + signed, + number_size, + } = *args; + match input { Value::Int { val, .. } => { + let val = *val; if signed || val < 0 { Value::int(!val, span) } else { @@ -147,25 +167,24 @@ fn operate(value: Value, head: Span, signed: bool, number_size: NumberBytes) -> !val & 0x7F_FF_FF_FF_FF_FF } } - // This case shouldn't happen here, as it's handled before - Invalid => 0, }; Value::int(out_val, span) } } - other => match other { - // Propagate errors inside the value - Value::Error { .. } => other, - _ => Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), - wrong_type: other.get_type().to_string(), - dst_span: head, - src_span: other.span(), - }, - head, - ), - }, + Value::Binary { val, .. } => { + Value::binary(val.iter().copied().map(|b| !b).collect::>(), span) + } + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "int or binary".into(), + wrong_type: other.get_type().to_string(), + dst_span: other.span(), + src_span: span, + }, + span, + ), } } diff --git a/crates/nu-cmd-extra/src/extra/bits/or.rs b/crates/nu-cmd-extra/src/extra/bits/or.rs index 87dca7fb96..8b153767c5 100644 --- a/crates/nu-cmd-extra/src/extra/bits/or.rs +++ b/crates/nu-cmd-extra/src/extra/bits/or.rs @@ -1,8 +1,9 @@ +use super::binary_op; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, }; #[derive(Clone)] @@ -17,17 +18,33 @@ impl Command for BitsOr { Signature::build("bits or") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) - .required("target", SyntaxShape::Int, "target int to perform bit or") + .allow_variants_without_examples(true) + .required( + "target", + SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::Int]), + "right-hand side of the operation", + ) + .named( + "endian", + SyntaxShape::String, + "byte encode endian, available options: native(default), little, big", + Some('e'), + ) .category(Category::Bits) } fn usage(&self) -> &str { - "Performs bitwise or for ints." + "Performs bitwise or for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -42,14 +59,32 @@ impl Command for BitsOr { input: PipelineData, ) -> Result { let head = call.head; - let target: i64 = call.req(engine_state, stack, 0)?; + let target: Value = call.req(engine_state, stack, 0)?; + let endian = call.get_flag::>(engine_state, stack, "endian")?; + + let little_endian = if let Some(endian) = endian { + match endian.item.as_str() { + "native" => cfg!(target_endian = "little"), + "little" => true, + "big" => false, + _ => { + return Err(ShellError::TypeMismatch { + err_message: "Endian must be one of native, little, big".to_string(), + span: endian.span, + }) + } + } + } else { + cfg!(target_endian = "little") + }; // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } + input.map( - move |value| operate(value, target, head), + move |value| binary_op(&value, &target, little_endian, |(l, r)| l | r, head), engine_state.ctrlc.clone(), ) } @@ -62,35 +97,34 @@ impl Command for BitsOr { result: Some(Value::test_int(6)), }, Example { - description: "Apply logical or to a list of numbers", + description: "Apply bitwise or to a list of numbers", example: "[8 3 2] | bits or 2", - result: Some(Value::list( - vec![Value::test_int(10), Value::test_int(3), Value::test_int(2)], - Span::test_data(), - )), + result: Some(Value::test_list(vec![ + Value::test_int(10), + Value::test_int(3), + Value::test_int(2), + ])), + }, + Example { + description: "Apply bitwise or to binary data", + example: "0x[88 cc] | bits or 0x[42 32]", + result: Some(Value::test_binary(vec![0xca, 0xfe])), + }, + Example { + description: + "Apply bitwise or to binary data of varying lengths with specified endianness", + example: "0x[c0 ff ee] | bits or 0x[ff] --endian big", + result: Some(Value::test_binary(vec![0xc0, 0xff, 0xff])), + }, + Example { + description: "Apply bitwise or to input binary data smaller than the operor", + example: "0x[ff] | bits or 0x[12 34 56] --endian little", + result: Some(Value::test_binary(vec![0xff, 0x34, 0x56])), }, ] } } -fn operate(value: Value, target: i64, head: Span) -> Value { - let span = value.span(); - match value { - Value::Int { val, .. } => Value::int(val | target, span), - // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, - other => Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), - wrong_type: other.get_type().to_string(), - dst_span: head, - src_span: other.span(), - }, - head, - ), - } -} - #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs index 10d8696572..d832ac8f54 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs @@ -1,12 +1,26 @@ use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +use itertools::Itertools; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Spanned; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use num_traits::int::PrimInt; -use std::fmt::Display; + +struct Arguments { + signed: bool, + bits: usize, + number_size: NumberBytes, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + None + } +} #[derive(Clone)] pub struct BitsRol; @@ -20,11 +34,17 @@ impl Command for BitsRol { Signature::build("bits rol") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) + .allow_variants_without_examples(true) .required("bits", SyntaxShape::Int, "number of bits to rotate left") .switch( "signed", @@ -33,7 +53,7 @@ impl Command for BitsRol { ) .named( "number-bytes", - SyntaxShape::String, + SyntaxShape::Int, "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", Some('n'), ) @@ -41,7 +61,7 @@ impl Command for BitsRol { } fn usage(&self) -> &str { - "Bitwise rotate left for ints." + "Bitwise rotate left for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -58,27 +78,22 @@ impl Command for BitsRol { let head = call.head; let bits: usize = call.req(engine_state, stack, 0)?; let signed = call.has_flag(engine_state, stack, "signed")?; - let number_bytes: Option> = + let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let bytes_len = get_number_bytes(number_bytes.as_ref()); - if let NumberBytes::Invalid = bytes_len { - if let Some(val) = number_bytes { - return Err(ShellError::UnsupportedInput { - msg: "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), - input: "value originates from here".to_string(), - msg_span: head, - input_span: val.span, - }); - } - } + let number_size = get_number_bytes(number_bytes, head)?; + // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, bits, head, signed, bytes_len), - engine_state.ctrlc.clone(), - ) + + let args = Arguments { + signed, + number_size, + bits, + }; + + operate(action, args, input, head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { @@ -96,61 +111,82 @@ impl Command for BitsRol { Span::test_data(), )), }, + Example { + description: "rotate left binary data", + example: "0x[c0 ff ee] | bits rol 10", + result: Some(Value::binary(vec![0xff, 0xbb, 0x03], Span::test_data())), + }, ] } } -fn get_rotate_left(val: T, bits: u32, span: Span) -> Value -where - i64: std::convert::TryFrom, -{ - let rotate_result = i64::try_from(val.rotate_left(bits)); - match rotate_result { - Ok(val) => Value::int(val, span), - Err(_) => Value::error( - ShellError::GenericError { - error: "Rotate left result beyond the range of 64 bit signed number".into(), - msg: format!( - "{val} of the specified number of bytes rotate left {bits} bits exceed limit" - ), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } -} +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + let Arguments { + signed, + number_size, + bits, + } = *args; -fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { - let span = value.span(); - match value { + match input { Value::Int { val, .. } => { use InputNumType::*; - // let bits = (((bits % 64) + 64) % 64) as u32; + let val = *val; let bits = bits as u32; - let input_type = get_input_num_type(val, signed, number_size); - match input_type { - One => get_rotate_left(val as u8, bits, span), - Two => get_rotate_left(val as u16, bits, span), - Four => get_rotate_left(val as u32, bits, span), - Eight => get_rotate_left(val as u64, bits, span), - SignedOne => get_rotate_left(val as i8, bits, span), - SignedTwo => get_rotate_left(val as i16, bits, span), - SignedFour => get_rotate_left(val as i32, bits, span), - SignedEight => get_rotate_left(val, bits, span), - } + let input_num_type = get_input_num_type(val, signed, number_size); + + let int = match input_num_type { + One => (val as u8).rotate_left(bits) as i64, + Two => (val as u16).rotate_left(bits) as i64, + Four => (val as u32).rotate_left(bits) as i64, + Eight => { + let Ok(i) = i64::try_from((val as u64).rotate_left(bits)) else { + return Value::error( + ShellError::GenericError { + error: "result out of range for specified number".into(), + msg: format!( + "rotating left by {bits} is out of range for the value {val}" + ), + span: Some(span), + help: None, + inner: vec![], + }, + span, + ); + }; + i + } + SignedOne => (val as i8).rotate_left(bits) as i64, + SignedTwo => (val as i16).rotate_left(bits) as i64, + SignedFour => (val as i32).rotate_left(bits) as i64, + SignedEight => val.rotate_left(bits), + }; + + Value::int(int, span) + } + Value::Binary { val, .. } => { + let byte_shift = bits / 8; + let bit_rotate = bits % 8; + + let mut bytes = val + .iter() + .copied() + .circular_tuple_windows::<(u8, u8)>() + .map(|(lhs, rhs)| (lhs << bit_rotate) | (rhs >> (8 - bit_rotate))) + .collect::>(); + bytes.rotate_left(byte_shift); + + Value::binary(bytes, span) } // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, + Value::Error { .. } => input.clone(), other => Value::error( ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), + exp_input_type: "int or binary".into(), wrong_type: other.get_type().to_string(), - dst_span: head, + dst_span: span, src_span: other.span(), }, - head, + span, ), } } diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs index 13a134d919..6f90a8a5b7 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs @@ -1,12 +1,26 @@ use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +use itertools::Itertools; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Spanned; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use num_traits::int::PrimInt; -use std::fmt::Display; + +struct Arguments { + signed: bool, + bits: usize, + number_size: NumberBytes, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + None + } +} #[derive(Clone)] pub struct BitsRor; @@ -20,11 +34,17 @@ impl Command for BitsRor { Signature::build("bits ror") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) + .allow_variants_without_examples(true) .required("bits", SyntaxShape::Int, "number of bits to rotate right") .switch( "signed", @@ -33,7 +53,7 @@ impl Command for BitsRor { ) .named( "number-bytes", - SyntaxShape::String, + SyntaxShape::Int, "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", Some('n'), ) @@ -41,7 +61,7 @@ impl Command for BitsRor { } fn usage(&self) -> &str { - "Bitwise rotate right for ints." + "Bitwise rotate right for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -58,103 +78,119 @@ impl Command for BitsRor { let head = call.head; let bits: usize = call.req(engine_state, stack, 0)?; let signed = call.has_flag(engine_state, stack, "signed")?; - let number_bytes: Option> = + let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let bytes_len = get_number_bytes(number_bytes.as_ref()); - if let NumberBytes::Invalid = bytes_len { - if let Some(val) = number_bytes { - return Err(ShellError::UnsupportedInput { - msg: "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), - input: "value originates from here".to_string(), - msg_span: head, - input_span: val.span, - }); - } - } + let number_size = get_number_bytes(number_bytes, head)?; + // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, bits, head, signed, bytes_len), - engine_state.ctrlc.clone(), - ) + + let args = Arguments { + signed, + number_size, + bits, + }; + + operate(action, args, input, head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { vec![ Example { - description: "Rotate right a number with 60 bits", - example: "17 | bits ror 60", - result: Some(Value::test_int(272)), + description: "rotate right a number with 2 bits", + example: "17 | bits ror 2", + result: Some(Value::test_int(68)), }, Example { - description: "Rotate right a list of numbers of one byte", - example: "[15 33 92] | bits ror 2 --number-bytes '1'", + description: "rotate right a list of numbers of two bytes", + example: "[15 33 92] | bits ror 2 --number-bytes 2", result: Some(Value::list( vec![ - Value::test_int(195), - Value::test_int(72), + Value::test_int(49155), + Value::test_int(16392), Value::test_int(23), ], Span::test_data(), )), }, + Example { + description: "rotate right binary data", + example: "0x[ff bb 03] | bits ror 10", + result: Some(Value::binary(vec![0xc0, 0xff, 0xee], Span::test_data())), + }, ] } } -fn get_rotate_right(val: T, bits: u32, span: Span) -> Value -where - i64: std::convert::TryFrom, -{ - let rotate_result = i64::try_from(val.rotate_right(bits)); - match rotate_result { - Ok(val) => Value::int(val, span), - Err(_) => Value::error( - ShellError::GenericError { - error: "Rotate right result beyond the range of 64 bit signed number".into(), - msg: format!( - "{val} of the specified number of bytes rotate right {bits} bits exceed limit" - ), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } -} +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + let Arguments { + signed, + number_size, + bits, + } = *args; -fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { - let span = value.span(); - match value { + match input { Value::Int { val, .. } => { use InputNumType::*; - // let bits = (((bits % 64) + 64) % 64) as u32; + let val = *val; let bits = bits as u32; - let input_type = get_input_num_type(val, signed, number_size); - match input_type { - One => get_rotate_right(val as u8, bits, span), - Two => get_rotate_right(val as u16, bits, span), - Four => get_rotate_right(val as u32, bits, span), - Eight => get_rotate_right(val as u64, bits, span), - SignedOne => get_rotate_right(val as i8, bits, span), - SignedTwo => get_rotate_right(val as i16, bits, span), - SignedFour => get_rotate_right(val as i32, bits, span), - SignedEight => get_rotate_right(val, bits, span), - } + let input_num_type = get_input_num_type(val, signed, number_size); + + let int = match input_num_type { + One => (val as u8).rotate_right(bits) as i64, + Two => (val as u16).rotate_right(bits) as i64, + Four => (val as u32).rotate_right(bits) as i64, + Eight => { + let Ok(i) = i64::try_from((val as u64).rotate_right(bits)) else { + return Value::error( + ShellError::GenericError { + error: "result out of range for specified number".into(), + msg: format!( + "rotating right by {bits} is out of range for the value {val}" + ), + span: Some(span), + help: None, + inner: vec![], + }, + span, + ); + }; + i + } + SignedOne => (val as i8).rotate_right(bits) as i64, + SignedTwo => (val as i16).rotate_right(bits) as i64, + SignedFour => (val as i32).rotate_right(bits) as i64, + SignedEight => val.rotate_right(bits), + }; + + Value::int(int, span) + } + Value::Binary { val, .. } => { + let byte_shift = bits / 8; + let bit_rotate = bits % 8; + + let mut bytes = val + .iter() + .copied() + .circular_tuple_windows::<(u8, u8)>() + .map(|(lhs, rhs)| (lhs >> bit_rotate) | (rhs << (8 - bit_rotate))) + .collect::>(); + bytes.rotate_right(byte_shift); + + Value::binary(bytes, span) } // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, + Value::Error { .. } => input.clone(), other => Value::error( ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), + exp_input_type: "int or binary".into(), wrong_type: other.get_type().to_string(), - dst_span: head, + dst_span: span, src_span: other.span(), }, - head, + span, ), } } diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs index baa2bb3b3f..f6df810c44 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs @@ -1,12 +1,27 @@ use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +use itertools::Itertools; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Spanned; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use num_traits::CheckedShl; -use std::fmt::Display; +use std::iter; + +struct Arguments { + signed: bool, + bits: usize, + number_size: NumberBytes, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + None + } +} #[derive(Clone)] pub struct BitsShl; @@ -20,11 +35,17 @@ impl Command for BitsShl { Signature::build("bits shl") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) + .allow_variants_without_examples(true) .required("bits", SyntaxShape::Int, "number of bits to shift left") .switch( "signed", @@ -33,7 +54,7 @@ impl Command for BitsShl { ) .named( "number-bytes", - SyntaxShape::String, + SyntaxShape::Int, "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", Some('n'), ) @@ -41,7 +62,7 @@ impl Command for BitsShl { } fn usage(&self) -> &str { - "Bitwise shift left for ints." + "Bitwise shift left for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -58,27 +79,22 @@ impl Command for BitsShl { let head = call.head; let bits: usize = call.req(engine_state, stack, 0)?; let signed = call.has_flag(engine_state, stack, "signed")?; - let number_bytes: Option> = + let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let bytes_len = get_number_bytes(number_bytes.as_ref()); - if let NumberBytes::Invalid = bytes_len { - if let Some(val) = number_bytes { - return Err(ShellError::UnsupportedInput { - msg: "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), - input: "value originates from here".to_string(), - msg_span: head, - input_span: val.span, - }); - } - } + let number_size = get_number_bytes(number_bytes, head)?; + // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, bits, head, signed, bytes_len), - engine_state.ctrlc.clone(), - ) + + let args = Arguments { + signed, + number_size, + bits, + }; + + operate(action, args, input, head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { @@ -86,17 +102,17 @@ impl Command for BitsShl { Example { description: "Shift left a number by 7 bits", example: "2 | bits shl 7", - result: Some(Value::test_int(256)), + result: Some(Value::test_int(0)), }, Example { - description: "Shift left a number with 1 byte by 7 bits", - example: "2 | bits shl 7 --number-bytes '1'", - result: Some(Value::test_int(0)), + description: "Shift left a number with 2 byte by 7 bits", + example: "2 | bits shl 7 --number-bytes 2", + result: Some(Value::test_int(256)), }, Example { description: "Shift left a signed number by 1 bit", example: "0x7F | bits shl 1 --signed", - result: Some(Value::test_int(254)), + result: Some(Value::test_int(-2)), }, Example { description: "Shift left a list of numbers", @@ -106,75 +122,88 @@ impl Command for BitsShl { Span::test_data(), )), }, + Example { + description: "Shift left a binary value", + example: "0x[4f f4] | bits shl 4", + result: Some(Value::binary(vec![0xff, 0x40], Span::test_data())), + }, ] } } -fn get_shift_left(val: T, bits: u32, span: Span) -> Value -where - i64: std::convert::TryFrom, -{ - match val.checked_shl(bits) { - Some(val) => { - let shift_result = i64::try_from(val); - match shift_result { - Ok(val) => Value::int( val, span ), - Err(_) => Value::error( - ShellError::GenericError { - error:"Shift left result beyond the range of 64 bit signed number".into(), - msg: format!( - "{val} of the specified number of bytes shift left {bits} bits exceed limit" - ), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } - } - None => Value::error( - ShellError::GenericError { - error: "Shift left failed".into(), - msg: format!("{val} shift left {bits} bits failed, you may shift too many bits"), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } -} +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + let Arguments { + signed, + number_size, + bits, + } = *args; -fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { - let span = value.span(); - match value { + match input { Value::Int { val, .. } => { use InputNumType::*; - // let bits = (((bits % 64) + 64) % 64) as u32; - let bits = bits as u32; - let input_type = get_input_num_type(val, signed, number_size); - match input_type { - One => get_shift_left(val as u8, bits, span), - Two => get_shift_left(val as u16, bits, span), - Four => get_shift_left(val as u32, bits, span), - Eight => get_shift_left(val as u64, bits, span), - SignedOne => get_shift_left(val as i8, bits, span), - SignedTwo => get_shift_left(val as i16, bits, span), - SignedFour => get_shift_left(val as i32, bits, span), - SignedEight => get_shift_left(val, bits, span), - } + let val = *val; + let bits = bits as u64; + + let input_num_type = get_input_num_type(val, signed, number_size); + let int = match input_num_type { + One => ((val as u8) << bits) as i64, + Two => ((val as u16) << bits) as i64, + Four => ((val as u32) << bits) as i64, + Eight => { + let Ok(i) = i64::try_from((val as u64) << bits) else { + return Value::error( + ShellError::GenericError { + error: "result out of range for specified number".into(), + msg: format!( + "shifting left by {bits} is out of range for the value {val}" + ), + span: Some(span), + help: None, + inner: vec![], + }, + span, + ); + }; + i + } + SignedOne => ((val as i8) << bits) as i64, + SignedTwo => ((val as i16) << bits) as i64, + SignedFour => ((val as i32) << bits) as i64, + SignedEight => val << bits, + }; + + Value::int(int, span) + } + Value::Binary { val, .. } => { + let byte_shift = bits / 8; + let bit_shift = bits % 8; + + use itertools::Position::*; + let bytes = val + .iter() + .copied() + .skip(byte_shift) + .circular_tuple_windows::<(u8, u8)>() + .with_position() + .map(|(pos, (lhs, rhs))| match pos { + Last | Only => lhs << bit_shift, + _ => (lhs << bit_shift) | (rhs >> bit_shift), + }) + .chain(iter::repeat(0).take(byte_shift)) + .collect::>(); + + Value::binary(bytes, span) } // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, + Value::Error { .. } => input.clone(), other => Value::error( ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), + exp_input_type: "int or binary".into(), wrong_type: other.get_type().to_string(), - dst_span: head, + dst_span: span, src_span: other.span(), }, - head, + span, ), } } diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs index b4625e56b2..94fb4cee93 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs @@ -1,12 +1,27 @@ use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +use itertools::Itertools; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Spanned; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use num_traits::CheckedShr; -use std::fmt::Display; +use std::iter; + +struct Arguments { + signed: bool, + bits: usize, + number_size: NumberBytes, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + None + } +} #[derive(Clone)] pub struct BitsShr; @@ -20,11 +35,17 @@ impl Command for BitsShr { Signature::build("bits shr") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) + .allow_variants_without_examples(true) .required("bits", SyntaxShape::Int, "number of bits to shift right") .switch( "signed", @@ -33,7 +54,7 @@ impl Command for BitsShr { ) .named( "number-bytes", - SyntaxShape::String, + SyntaxShape::Int, "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", Some('n'), ) @@ -41,7 +62,7 @@ impl Command for BitsShr { } fn usage(&self) -> &str { - "Bitwise shift right for ints." + "Bitwise shift right for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -58,27 +79,22 @@ impl Command for BitsShr { let head = call.head; let bits: usize = call.req(engine_state, stack, 0)?; let signed = call.has_flag(engine_state, stack, "signed")?; - let number_bytes: Option> = + let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let bytes_len = get_number_bytes(number_bytes.as_ref()); - if let NumberBytes::Invalid = bytes_len { - if let Some(val) = number_bytes { - return Err(ShellError::UnsupportedInput { - msg: "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), - input: "value originates from here".to_string(), - msg_span: head, - input_span: val.span, - }); - } - } + let number_size = get_number_bytes(number_bytes, head)?; + // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, bits, head, signed, bytes_len), - engine_state.ctrlc.clone(), - ) + + let args = Arguments { + signed, + number_size, + bits, + }; + + operate(action, args, input, head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { @@ -96,75 +112,75 @@ impl Command for BitsShr { Span::test_data(), )), }, + Example { + description: "Shift right a binary value", + example: "0x[4f f4] | bits shr 4", + result: Some(Value::binary(vec![0x04, 0xff], Span::test_data())), + }, ] } } -fn get_shift_right(val: T, bits: u32, span: Span) -> Value -where - i64: std::convert::TryFrom, -{ - match val.checked_shr(bits) { - Some(val) => { - let shift_result = i64::try_from(val); - match shift_result { - Ok(val) => Value::int( val, span ), - Err(_) => Value::error( - ShellError::GenericError { - error: "Shift right result beyond the range of 64 bit signed number".into(), - msg: format!( - "{val} of the specified number of bytes shift right {bits} bits exceed limit" - ), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } - } - None => Value::error( - ShellError::GenericError { - error: "Shift right failed".into(), - msg: format!("{val} shift right {bits} bits failed, you may shift too many bits"), - span: Some(span), - help: None, - inner: vec![], - }, - span, - ), - } -} +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + let Arguments { + signed, + number_size, + bits, + } = *args; -fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { - let span = value.span(); - match value { + match input { Value::Int { val, .. } => { use InputNumType::*; - // let bits = (((bits % 64) + 64) % 64) as u32; + let val = *val; let bits = bits as u32; - let input_type = get_input_num_type(val, signed, number_size); - match input_type { - One => get_shift_right(val as u8, bits, span), - Two => get_shift_right(val as u16, bits, span), - Four => get_shift_right(val as u32, bits, span), - Eight => get_shift_right(val as u64, bits, span), - SignedOne => get_shift_right(val as i8, bits, span), - SignedTwo => get_shift_right(val as i16, bits, span), - SignedFour => get_shift_right(val as i32, bits, span), - SignedEight => get_shift_right(val, bits, span), - } + let input_num_type = get_input_num_type(val, signed, number_size); + + let int = match input_num_type { + One => ((val as u8) >> bits) as i64, + Two => ((val as u16) >> bits) as i64, + Four => ((val as u32) >> bits) as i64, + Eight => ((val as u64) >> bits) as i64, + SignedOne => ((val as i8) >> bits) as i64, + SignedTwo => ((val as i16) >> bits) as i64, + SignedFour => ((val as i32) >> bits) as i64, + SignedEight => val >> bits, + }; + + Value::int(int, span) + } + Value::Binary { val, .. } => { + let byte_shift = bits / 8; + let bit_shift = bits % 8; + + let len = val.len(); + use itertools::Position::*; + let bytes = iter::repeat(0) + .take(byte_shift) + .chain( + val.iter() + .copied() + .circular_tuple_windows::<(u8, u8)>() + .with_position() + .map(|(pos, (lhs, rhs))| match pos { + First | Only => lhs >> bit_shift, + _ => (lhs >> bit_shift) | (rhs << bit_shift), + }) + .take(len - byte_shift), + ) + .collect::>(); + + Value::binary(bytes, span) } // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, + Value::Error { .. } => input.clone(), other => Value::error( ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), + exp_input_type: "int or binary".into(), wrong_type: other.get_type().to_string(), - dst_span: head, + dst_span: span, src_span: other.span(), }, - head, + span, ), } } diff --git a/crates/nu-cmd-extra/src/extra/bits/xor.rs b/crates/nu-cmd-extra/src/extra/bits/xor.rs index a082b295e3..87b55cd3dd 100644 --- a/crates/nu-cmd-extra/src/extra/bits/xor.rs +++ b/crates/nu-cmd-extra/src/extra/bits/xor.rs @@ -1,8 +1,9 @@ +use super::binary_op; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, }; #[derive(Clone)] @@ -17,17 +18,33 @@ impl Command for BitsXor { Signature::build("bits xor") .input_output_types(vec![ (Type::Int, Type::Int), + (Type::Binary, Type::Binary), ( Type::List(Box::new(Type::Int)), Type::List(Box::new(Type::Int)), ), + ( + Type::List(Box::new(Type::Binary)), + Type::List(Box::new(Type::Binary)), + ), ]) - .required("target", SyntaxShape::Int, "target int to perform bit xor") + .allow_variants_without_examples(true) + .required( + "target", + SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::Int]), + "right-hand side of the operation", + ) + .named( + "endian", + SyntaxShape::String, + "byte encode endian, available options: native(default), little, big", + Some('e'), + ) .category(Category::Bits) } fn usage(&self) -> &str { - "Performs bitwise xor for ints." + "Performs bitwise xor for ints or binary values." } fn search_terms(&self) -> Vec<&str> { @@ -42,13 +59,32 @@ impl Command for BitsXor { input: PipelineData, ) -> Result { let head = call.head; - let target: i64 = call.req(engine_state, stack, 0)?; + let target: Value = call.req(engine_state, stack, 0)?; + let endian = call.get_flag::>(engine_state, stack, "endian")?; + + let little_endian = if let Some(endian) = endian { + match endian.item.as_str() { + "native" => cfg!(target_endian = "little"), + "little" => true, + "big" => false, + _ => { + return Err(ShellError::TypeMismatch { + err_message: "Endian must be one of native, little, big".to_string(), + span: endian.span, + }) + } + } + } else { + cfg!(target_endian = "little") + }; + // This doesn't match explicit nulls if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } + input.map( - move |value| operate(value, target, head), + move |value| binary_op(&value, &target, little_endian, |(l, r)| l ^ r, head), engine_state.ctrlc.clone(), ) } @@ -61,35 +97,34 @@ impl Command for BitsXor { result: Some(Value::test_int(0)), }, Example { - description: "Apply logical xor to a list of numbers", + description: "Apply bitwise xor to a list of numbers", example: "[8 3 2] | bits xor 2", - result: Some(Value::list( - vec![Value::test_int(10), Value::test_int(1), Value::test_int(0)], - Span::test_data(), - )), + result: Some(Value::test_list(vec![ + Value::test_int(10), + Value::test_int(1), + Value::test_int(0), + ])), + }, + Example { + description: "Apply bitwise xor to binary data", + example: "0x[ca fe] | bits xor 0x[ba be]", + result: Some(Value::test_binary(vec![0x70, 0x40])), + }, + Example { + description: + "Apply bitwise xor to binary data of varying lengths with specified endianness", + example: "0x[ca fe] | bits xor 0x[aa] --endian big", + result: Some(Value::test_binary(vec![0xca, 0x54])), + }, + Example { + description: "Apply bitwise xor to input binary data smaller than the operand", + example: "0x[ff] | bits xor 0x[12 34 56] --endian little", + result: Some(Value::test_binary(vec![0xed, 0x34, 0x56])), }, ] } } -fn operate(value: Value, target: i64, head: Span) -> Value { - let span = value.span(); - match value { - Value::Int { val, .. } => Value::int(val ^ target, span), - // Propagate errors by explicitly matching them before the final case. - Value::Error { .. } => value, - other => Value::error( - ShellError::OnlySupportsThisInputType { - exp_input_type: "int".into(), - wrong_type: other.get_type().to_string(), - dst_span: head, - src_span: other.span(), - }, - head, - ), - } -} - #[cfg(test)] mod test { use super::*; diff --git a/src/tests/test_bits.rs b/src/tests/test_bits.rs index ea4231786c..fdb672d023 100644 --- a/src/tests/test_bits.rs +++ b/src/tests/test_bits.rs @@ -65,7 +65,7 @@ fn bits_shift_left_negative() -> TestResult { fn bits_shift_left_list() -> TestResult { run_test( "[1 2 7 32 9 10] | bits shl 3 | str join '.'", - "8.16.56.256.72.80", + "8.16.56.0.72.80", ) } @@ -101,7 +101,7 @@ fn bits_rotate_left_negative() -> TestResult { fn bits_rotate_left_list() -> TestResult { run_test( "[1 2 7 32 9 10] | bits rol 3 | str join '.'", - "8.16.56.256.72.80", + "8.16.56.1.72.80", ) } @@ -119,6 +119,6 @@ fn bits_rotate_right_negative() -> TestResult { fn bits_rotate_right_list() -> TestResult { run_test( "[1 2 7 32 23 10] | bits ror 60 | str join '.'", - "16.32.112.512.368.160", + "16.32.112.2.113.160", ) }