From ce6df93d05fef476bbdb287bd9b9c4c1f073e255 Mon Sep 17 00:00:00 2001 From: Justin Ma Date: Wed, 3 Aug 2022 04:52:04 +0800 Subject: [PATCH] Add `bits shl` and `bits shr` command (#6202) * Add `bits shift-left` and `bits shift-right` command * update bits shift error tips * some code refactor * update shift right * some code refactor for bits shift commands * rename bits shift commands align with bits operators * update search term * Update crates/nu-command/src/bits/not.rs Co-authored-by: Stefan Holderbach * Update crates/nu-command/src/bits/shift_left.rs Co-authored-by: Stefan Holderbach * Update crates/nu-command/src/bits/shift_right.rs Co-authored-by: Stefan Holderbach * ci skip * change default number-bytes for bits shift * fix bits not tests * fix bits tests Co-authored-by: Stefan Holderbach --- Cargo.lock | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/bits/mod.rs | 84 ++++++++ crates/nu-command/src/bits/not.rs | 51 ++--- crates/nu-command/src/bits/shift_left.rs | 188 ++++++++++++++++++ crates/nu-command/src/bits/shift_right.rs | 172 ++++++++++++++++ crates/nu-command/src/bytes/add.rs | 2 +- crates/nu-command/src/bytes/remove.rs | 2 +- crates/nu-command/src/default_context.rs | 2 + .../strings/encode_decode/decode_base64.rs | 2 +- .../strings/encode_decode/encode_base64.rs | 2 +- src/tests/test_bits.rs | 30 +++ 12 files changed, 501 insertions(+), 36 deletions(-) create mode 100644 crates/nu-command/src/bits/shift_left.rs create mode 100644 crates/nu-command/src/bits/shift_right.rs diff --git a/Cargo.lock b/Cargo.lock index 6c14f93211..e19dd63beb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2620,6 +2620,7 @@ dependencies = [ "nu-test-support", "nu-utils", "num 0.4.0", + "num-traits", "pathdiff", "polars", "powierza-coefficient", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index a935724bd3..d74c0cd47e 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -58,6 +58,7 @@ meval = "0.2.0" mime = "0.3.16" notify = "4.0.17" num = { version = "0.4.0", optional = true } +num-traits = "0.2.14" pathdiff = "0.2.1" powierza-coefficient = "1.0.1" quick-xml = "0.23.0" diff --git a/crates/nu-command/src/bits/mod.rs b/crates/nu-command/src/bits/mod.rs index 016081db32..902f349618 100644 --- a/crates/nu-command/src/bits/mod.rs +++ b/crates/nu-command/src/bits/mod.rs @@ -2,10 +2,94 @@ mod and; mod bits_; mod not; mod or; +mod shift_left; +mod shift_right; mod xor; +use nu_protocol::Spanned; + pub use and::SubCommand as BitsAnd; pub use bits_::Bits; pub use not::SubCommand as BitsNot; pub use or::SubCommand as BitsOr; +pub use shift_left::SubCommand as BitsShiftLeft; +pub use shift_right::SubCommand as BitsShiftRight; pub use xor::SubCommand as BitsXor; + +#[derive(Clone, Copy)] +enum NumberBytes { + One, + Two, + Four, + Eight, + Auto, + Invalid, +} + +#[derive(Clone, Copy)] +enum InputNumType { + One, + Two, + Four, + Eight, + SignedOne, + SignedTwo, + SignedFour, + SignedEight, +} + +fn get_number_bytes(number_bytes: &Option>) -> 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_input_num_type(val: i64, signed: bool, number_size: NumberBytes) -> InputNumType { + if signed || val < 0 { + match number_size { + NumberBytes::One => InputNumType::SignedOne, + NumberBytes::Two => InputNumType::SignedTwo, + NumberBytes::Four => InputNumType::SignedFour, + NumberBytes::Eight => InputNumType::SignedEight, + NumberBytes::Auto => { + if val <= 0x7F && val >= -(2i64.pow(7)) { + InputNumType::SignedOne + } else if val <= 0x7FFF && val >= -(2i64.pow(15)) { + InputNumType::SignedTwo + } else if val <= 0x7FFFFFFF && val >= -(2i64.pow(31)) { + InputNumType::SignedFour + } else { + InputNumType::SignedEight + } + } + NumberBytes::Invalid => InputNumType::SignedFour, + } + } else { + match number_size { + NumberBytes::One => InputNumType::One, + NumberBytes::Two => InputNumType::Two, + NumberBytes::Four => InputNumType::Four, + NumberBytes::Eight => InputNumType::Eight, + NumberBytes::Auto => { + if val <= 0xFF { + InputNumType::One + } else if val <= 0xFFFF { + InputNumType::Two + } else if val <= 0xFFFFFFFF { + InputNumType::Four + } else { + InputNumType::Eight + } + } + NumberBytes::Invalid => InputNumType::Four, + } + } +} diff --git a/crates/nu-command/src/bits/not.rs b/crates/nu-command/src/bits/not.rs index 7bb3122885..a6baa27e68 100644 --- a/crates/nu-command/src/bits/not.rs +++ b/crates/nu-command/src/bits/not.rs @@ -1,3 +1,4 @@ +use super::{get_number_bytes, NumberBytes}; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; @@ -8,15 +9,6 @@ use nu_protocol::{ #[derive(Clone)] pub struct SubCommand; -#[derive(Clone, Copy)] -enum NumberBytes { - One, - Two, - Four, - Eight, - Auto, -} - impl Command for SubCommand { fn name(&self) -> &str { "bits not" @@ -57,25 +49,18 @@ impl Command for SubCommand { let signed = call.has_flag("signed"); let number_bytes: Option> = call.get_flag(engine_state, stack, "number-bytes")?; - let number_bytes = match number_bytes.as_ref() { - None => NumberBytes::Auto, - Some(size) => match size.item.as_str() { - "1" => NumberBytes::One, - "2" => NumberBytes::Two, - "4" => NumberBytes::Four, - "8" => NumberBytes::Eight, - "auto" => NumberBytes::Auto, - _ => { - return Err(ShellError::UnsupportedInput( - "the size of number is invalid".to_string(), - size.span, - )) - } - }, - }; + let bytes_len = get_number_bytes(&number_bytes); + if let NumberBytes::Invalid = bytes_len { + if let Some(val) = number_bytes { + return Err(ShellError::UnsupportedInput( + "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), + val.span, + )); + } + } input.map( - move |value| operate(value, head, signed, number_bytes), + move |value| operate(value, head, signed, bytes_len), engine_state.ctrlc.clone(), ) } @@ -83,20 +68,20 @@ impl Command for SubCommand { fn examples(&self) -> Vec { vec![ Example { - description: "Apply the logical negation to a list of numbers", + description: "Apply logical negation to a list of numbers", example: "[4 3 2] | bits not", result: Some(Value::List { vals: vec![ - Value::test_int(251), - Value::test_int(252), - Value::test_int(253), + Value::test_int(140737488355323), + Value::test_int(140737488355324), + Value::test_int(140737488355325), ], span: Span::test_data(), }), }, Example { description: - "Apply the logical negation to a list of numbers, treat input as 2 bytes number", + "Apply logical negation to a list of numbers, treat input as 2 bytes number", example: "[4 3 2] | bits not -n 2", result: Some(Value::List { vals: vec![ @@ -109,7 +94,7 @@ impl Command for SubCommand { }, Example { description: - "Apply the logical negation to a list of numbers, treat input as signed number", + "Apply logical negation to a list of numbers, treat input as signed number", example: "[4 3 2] | bits not -s", result: Some(Value::List { vals: vec![ @@ -147,6 +132,8 @@ 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 { val: out_val, span } } diff --git a/crates/nu-command/src/bits/shift_left.rs b/crates/nu-command/src/bits/shift_left.rs new file mode 100644 index 0000000000..62c982a872 --- /dev/null +++ b/crates/nu-command/src/bits/shift_left.rs @@ -0,0 +1,188 @@ +use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +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, Spanned, SyntaxShape, Value, +}; +use num_traits::CheckedShl; +use std::fmt::Display; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "bits shl" + } + + fn signature(&self) -> Signature { + Signature::build("bits shl") + .required("bits", SyntaxShape::Int, "number of bits to shift left") + .switch( + "signed", + "always treat input number as a signed number", + Some('s'), + ) + .named( + "number-bytes", + SyntaxShape::String, + "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", + Some('n'), + ) + .category(Category::Bits) + } + + fn usage(&self) -> &str { + "Bitwise shift left for integers" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["shift left"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let bits: usize = call.req(engine_state, stack, 0)?; + let signed = call.has_flag("signed"); + let number_bytes: Option> = + call.get_flag(engine_state, stack, "number-bytes")?; + let bytes_len = get_number_bytes(&number_bytes); + if let NumberBytes::Invalid = bytes_len { + if let Some(val) = number_bytes { + return Err(ShellError::UnsupportedInput( + "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), + val.span, + )); + } + } + + input.map( + move |value| operate(value, bits, head, signed, bytes_len), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Shift left a number by 7 bits", + example: "2 | bits shl 7", + result: Some(Value::Int { + val: 256, + span: Span::test_data(), + }), + }, + Example { + description: "Shift left a number with 1 byte by 7 bits", + example: "2 | bits shl 7 -n 1", + result: Some(Value::Int { + val: 0, + span: Span::test_data(), + }), + }, + Example { + description: "Shift left a signed number by 1 bit", + example: "0x7F | bits shl 1 -s", + result: Some(Value::Int { + val: 254, + span: Span::test_data(), + }), + }, + Example { + description: "Shift left a list of numbers", + example: "[5 3 2] | bits shl 2", + result: Some(Value::List { + vals: vec![Value::test_int(20), Value::test_int(12), Value::test_int(8)], + span: 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 { + error: ShellError::GenericError( + "Shift left result beyond the range of 64 bit signed number".to_string(), + format!( + "{} of the specified number of bytes shift left {} bits exceed limit", + val, bits + ), + Some(span), + None, + Vec::new(), + ), + }, + } + } + None => Value::Error { + error: ShellError::GenericError( + "Shift left failed".to_string(), + format!( + "{} shift left {} bits failed, you may shift too many bits", + val, bits + ), + Some(span), + None, + Vec::new(), + ), + }, + } +} + +fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { + match value { + Value::Int { val, span } => { + 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 as i64, bits, span), + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Only integer values are supported, input type: {:?}", + other.get_type() + ), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/bits/shift_right.rs b/crates/nu-command/src/bits/shift_right.rs new file mode 100644 index 0000000000..1ffde30a8d --- /dev/null +++ b/crates/nu-command/src/bits/shift_right.rs @@ -0,0 +1,172 @@ +use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; +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, Spanned, SyntaxShape, Value, +}; +use num_traits::CheckedShr; +use std::fmt::Display; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "bits shr" + } + + fn signature(&self) -> Signature { + Signature::build("bits shr") + .required("bits", SyntaxShape::Int, "number of bits to shift right") + .switch( + "signed", + "always treat input number as a signed number", + Some('s'), + ) + .named( + "number-bytes", + SyntaxShape::String, + "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", + Some('n'), + ) + .category(Category::Bits) + } + + fn usage(&self) -> &str { + "Bitwise shift right for integers" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["shift right"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let bits: usize = call.req(engine_state, stack, 0)?; + let signed = call.has_flag("signed"); + let number_bytes: Option> = + call.get_flag(engine_state, stack, "number-bytes")?; + let bytes_len = get_number_bytes(&number_bytes); + if let NumberBytes::Invalid = bytes_len { + if let Some(val) = number_bytes { + return Err(ShellError::UnsupportedInput( + "Only 1, 2, 4, 8, or 'auto' bytes are supported as word sizes".to_string(), + val.span, + )); + } + } + + input.map( + move |value| operate(value, bits, head, signed, bytes_len), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Shift right a number with 2 bits", + example: "8 | bits shr 2", + result: Some(Value::Int { + val: 2, + span: Span::test_data(), + }), + }, + Example { + description: "Shift right a list of numbers", + example: "[15 35 2] | bits shr 2", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(8), Value::test_int(0)], + span: 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 { + error: ShellError::GenericError( + "Shift right result beyond the range of 64 bit signed number".to_string(), + format!( + "{} of the specified number of bytes shift right {} bits exceed limit", + val, bits + ), + Some(span), + None, + Vec::new(), + ), + }, + } + } + None => Value::Error { + error: ShellError::GenericError( + "Shift right failed".to_string(), + format!( + "{} shift right {} bits failed, you may shift too many bits", + val, bits + ), + Some(span), + None, + Vec::new(), + ), + }, + } +} + +fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { + match value { + Value::Int { val, span } => { + 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_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 as i64, bits, span), + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Only integer values are supported, input type: {:?}", + other.get_type() + ), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/bytes/add.rs b/crates/nu-command/src/bytes/add.rs index ff1bda62bf..3a93bc35c7 100644 --- a/crates/nu-command/src/bytes/add.rs +++ b/crates/nu-command/src/bytes/add.rs @@ -47,7 +47,7 @@ impl Command for BytesAdd { } fn usage(&self) -> &str { - "add specified bytes to the input" + "Add specified bytes to the input" } fn search_terms(&self) -> Vec<&str> { diff --git a/crates/nu-command/src/bytes/remove.rs b/crates/nu-command/src/bytes/remove.rs index e7d66ca5e7..316d5795a6 100644 --- a/crates/nu-command/src/bytes/remove.rs +++ b/crates/nu-command/src/bytes/remove.rs @@ -41,7 +41,7 @@ impl Command for BytesRemove { } fn usage(&self) -> &str { - "remove bytes" + "Remove bytes" } fn search_terms(&self) -> Vec<&str> { diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 8f1929cb71..a270267d38 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -214,6 +214,8 @@ pub fn create_default_context() -> EngineState { BitsNot, BitsOr, BitsXor, + BitsShiftLeft, + BitsShiftRight, } // Bytes diff --git a/crates/nu-command/src/strings/encode_decode/decode_base64.rs b/crates/nu-command/src/strings/encode_decode/decode_base64.rs index 9892693a28..3fbf39d5dd 100644 --- a/crates/nu-command/src/strings/encode_decode/decode_base64.rs +++ b/crates/nu-command/src/strings/encode_decode/decode_base64.rs @@ -35,7 +35,7 @@ impl Command for DecodeBase64 { } fn usage(&self) -> &str { - "base64 decode a value" + "Base64 decode a value" } fn extra_usage(&self) -> &str { diff --git a/crates/nu-command/src/strings/encode_decode/encode_base64.rs b/crates/nu-command/src/strings/encode_decode/encode_base64.rs index b0e4afd737..9b94a72242 100644 --- a/crates/nu-command/src/strings/encode_decode/encode_base64.rs +++ b/crates/nu-command/src/strings/encode_decode/encode_base64.rs @@ -31,7 +31,7 @@ impl Command for EncodeBase64 { } fn usage(&self) -> &str { - "base64 encode a value" + "Base64 encode a value" } fn examples(&self) -> Vec { diff --git a/src/tests/test_bits.rs b/src/tests/test_bits.rs index ada74f4b16..23c6bdfb7b 100644 --- a/src/tests/test_bits.rs +++ b/src/tests/test_bits.rs @@ -44,3 +44,33 @@ fn bits_xor_negative() -> TestResult { fn bits_xor_list() -> TestResult { run_test("[1 2 3 8 9 10] | bits xor 2 | str collect '.'", "3.0.1.10.11.8") } + +#[test] +fn bits_shift_left() -> TestResult { + run_test("2 | bits shl 3", "16") +} + +#[test] +fn bits_shift_left_negative() -> TestResult { + run_test("-3 | bits shl 5", "-96") +} + +#[test] +fn bits_shift_left_list() -> TestResult { + run_test("[1 2 7 32 9 10] | bits shl 3 | str collect '.'", "8.16.56.256.72.80") +} + +#[test] +fn bits_shift_right() -> TestResult { + run_test("8 | bits shr 2", "2") +} + +#[test] +fn bits_shift_right_negative() -> TestResult { + run_test("-32 | bits shr 2", "-8") +} + +#[test] +fn bits_shift_right_list() -> TestResult { + run_test("[12 98 7 64 900 10] | bits shr 3 | str collect '.'", "1.12.0.8.112.1") +}