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
This commit is contained in:
moonlander 2024-02-28 12:43:50 +00:00 committed by GitHub
parent 7b95e37bbe
commit d3895d71db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 742 additions and 428 deletions

1
Cargo.lock generated
View File

@ -2910,6 +2910,7 @@ version = "0.90.2"
dependencies = [ dependencies = [
"fancy-regex", "fancy-regex",
"heck", "heck",
"itertools 0.12.0",
"nu-ansi-term", "nu-ansi-term",
"nu-cmd-base", "nu-cmd-base",
"nu-cmd-lang", "nu-cmd-lang",

View File

@ -30,6 +30,7 @@ nu-pretty-hex = { version = "0.90.2", path = "../nu-pretty-hex" }
nu-json = { version = "0.90.2", path = "../nu-json" } nu-json = { version = "0.90.2", path = "../nu-json" }
serde_urlencoded = "0.7.1" serde_urlencoded = "0.7.1"
v_htmlescape = "0.15.0" v_htmlescape = "0.15.0"
itertools = "0.12"
[features] [features]
extra = ["default"] extra = ["default"]

View File

@ -1,8 +1,9 @@
use super::binary_op;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -17,17 +18,32 @@ impl Command for BitsAnd {
Signature::build("bits and") Signature::build("bits and")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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) .category(Category::Bits)
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Performs bitwise and for ints." "Performs bitwise and for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -42,14 +58,32 @@ impl Command for BitsAnd {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; 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::<Spanned<String>>(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 // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map( 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(), engine_state.ctrlc.clone(),
) )
} }
@ -57,40 +91,47 @@ impl Command for BitsAnd {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "Apply bits and to two numbers", description: "Apply bitwise and to two numbers",
example: "2 | bits and 2", example: "2 | bits and 2",
result: Some(Value::test_int(2)), result: Some(Value::test_int(2)),
}, },
Example { 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", example: "[4 3 2] | bits and 2",
result: Some(Value::list( result: Some(Value::test_list(vec![
vec![Value::test_int(0), Value::test_int(2), Value::test_int(2)], Value::test_int(0),
Span::test_data(), 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -13,6 +13,7 @@ pub use and::BitsAnd;
pub use bits_::Bits; pub use bits_::Bits;
pub use into::BitsInto; pub use into::BitsInto;
pub use not::BitsNot; pub use not::BitsNot;
use nu_protocol::{ShellError, Value};
pub use or::BitsOr; pub use or::BitsOr;
pub use rotate_left::BitsRol; pub use rotate_left::BitsRol;
pub use rotate_right::BitsRor; pub use rotate_right::BitsRor;
@ -20,7 +21,8 @@ pub use shift_left::BitsShl;
pub use shift_right::BitsShr; pub use shift_right::BitsShr;
pub use xor::BitsXor; pub use xor::BitsXor;
use nu_protocol::Spanned; use nu_protocol::{Span, Spanned};
use std::iter;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum NumberBytes { enum NumberBytes {
@ -29,7 +31,6 @@ enum NumberBytes {
Four, Four,
Eight, Eight,
Auto, Auto,
Invalid,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -44,17 +45,22 @@ enum InputNumType {
SignedEight, SignedEight,
} }
fn get_number_bytes(number_bytes: Option<&Spanned<String>>) -> NumberBytes { fn get_number_bytes(
match number_bytes.as_ref() { number_bytes: Option<Spanned<usize>>,
None => NumberBytes::Eight, head: Span,
Some(size) => match size.item.as_str() { ) -> Result<NumberBytes, ShellError> {
"1" => NumberBytes::One, match number_bytes {
"2" => NumberBytes::Two, None => Ok(NumberBytes::Auto),
"4" => NumberBytes::Four, Some(Spanned { item: 1, .. }) => Ok(NumberBytes::One),
"8" => NumberBytes::Eight, Some(Spanned { item: 2, .. }) => Ok(NumberBytes::Two),
"auto" => NumberBytes::Auto, Some(Spanned { item: 4, .. }) => Ok(NumberBytes::Four),
_ => NumberBytes::Invalid, 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 InputNumType::SignedEight
} }
} }
NumberBytes::Invalid => InputNumType::SignedFour,
} }
} else { } else {
match number_size { match number_size {
@ -95,7 +100,68 @@ fn get_input_num_type(val: i64, signed: bool, number_size: NumberBytes) -> Input
InputNumType::Eight InputNumType::Eight
} }
} }
NumberBytes::Invalid => InputNumType::Four,
} }
} }
} }
fn binary_op<F>(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<Item = u8> = if little_endian {
a = rhs.iter().copied().chain(pad);
&mut a
} else {
b = pad.chain(rhs.iter().copied());
&mut b
};
let bytes: Vec<u8> = 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,
),
}
}

View File

@ -1,6 +1,7 @@
use super::{get_number_bytes, NumberBytes}; use super::{get_number_bytes, NumberBytes};
use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::CallExt; 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::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value,
@ -9,6 +10,18 @@ use nu_protocol::{
#[derive(Clone)] #[derive(Clone)]
pub struct BitsNot; pub struct BitsNot;
#[derive(Clone, Copy)]
struct Arguments {
signed: bool,
number_size: NumberBytes,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
None
}
}
impl Command for BitsNot { impl Command for BitsNot {
fn name(&self) -> &str { fn name(&self) -> &str {
"bits not" "bits not"
@ -18,10 +31,15 @@ impl Command for BitsNot {
Signature::build("bits not") Signature::build("bits not")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::Int)), Type::List(Box::new(Type::Int)),
), ),
(
Type::List(Box::new(Type::Binary)),
Type::List(Box::new(Type::Binary)),
),
]) ])
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.switch( .switch(
@ -31,7 +49,7 @@ impl Command for BitsNot {
) )
.named( .named(
"number-bytes", "number-bytes",
SyntaxShape::String, SyntaxShape::Int,
"the size of unsigned number in bytes, it can be 1, 2, 4, 8, auto", "the size of unsigned number in bytes, it can be 1, 2, 4, 8, auto",
Some('n'), Some('n'),
) )
@ -55,28 +73,21 @@ impl Command for BitsNot {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let signed = call.has_flag(engine_state, stack, "signed")?; let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<String>> = let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?; call.get_flag(engine_state, stack, "number-bytes")?;
let bytes_len = get_number_bytes(number_bytes.as_ref()); let number_size = get_number_bytes(number_bytes, head)?;
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,
});
}
}
// This doesn't match explicit nulls // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map(
move |value| operate(value, head, signed, bytes_len), let args = Arguments {
engine_state.ctrlc.clone(), signed,
) number_size,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -86,9 +97,9 @@ impl Command for BitsNot {
example: "[4 3 2] | bits not", example: "[4 3 2] | bits not",
result: Some(Value::list( result: Some(Value::list(
vec![ vec![
Value::test_int(140737488355323), Value::test_int(251),
Value::test_int(140737488355324), Value::test_int(252),
Value::test_int(140737488355325), Value::test_int(253),
], ],
Span::test_data(), Span::test_data(),
)), )),
@ -96,7 +107,7 @@ impl Command for BitsNot {
Example { Example {
description: description:
"Apply 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 --number-bytes '2'", example: "[4 3 2] | bits not --number-bytes 2",
result: Some(Value::list( result: Some(Value::list(
vec![ vec![
Value::test_int(65531), Value::test_int(65531),
@ -119,14 +130,23 @@ impl Command for BitsNot {
Span::test_data(), 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 { fn action(input: &Value, args: &Arguments, span: Span) -> Value {
let span = value.span(); let Arguments {
match value { signed,
number_size,
} = *args;
match input {
Value::Int { val, .. } => { Value::Int { val, .. } => {
let val = *val;
if signed || val < 0 { if signed || val < 0 {
Value::int(!val, span) Value::int(!val, span)
} else { } else {
@ -147,25 +167,24 @@ fn operate(value: Value, head: Span, signed: bool, number_size: NumberBytes) ->
!val & 0x7F_FF_FF_FF_FF_FF !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) Value::int(out_val, span)
} }
} }
other => match other { Value::Binary { val, .. } => {
// Propagate errors inside the value Value::binary(val.iter().copied().map(|b| !b).collect::<Vec<_>>(), span)
Value::Error { .. } => other, }
_ => Value::error( // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => input.clone(),
other => Value::error(
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "int".into(), exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: other.span(),
src_span: other.span(), src_span: span,
}, },
head, span,
), ),
},
} }
} }

View File

@ -1,8 +1,9 @@
use super::binary_op;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -17,17 +18,33 @@ impl Command for BitsOr {
Signature::build("bits or") Signature::build("bits or")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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) .category(Category::Bits)
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Performs bitwise or for ints." "Performs bitwise or for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -42,14 +59,32 @@ impl Command for BitsOr {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; 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::<Spanned<String>>(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 // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map( 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(), engine_state.ctrlc.clone(),
) )
} }
@ -62,35 +97,34 @@ impl Command for BitsOr {
result: Some(Value::test_int(6)), result: Some(Value::test_int(6)),
}, },
Example { 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", example: "[8 3 2] | bits or 2",
result: Some(Value::list( result: Some(Value::test_list(vec![
vec![Value::test_int(10), Value::test_int(3), Value::test_int(2)], Value::test_int(10),
Span::test_data(), 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -1,12 +1,26 @@
use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; 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_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::Spanned;
use nu_protocol::{ 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<Vec<CellPath>> {
None
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct BitsRol; pub struct BitsRol;
@ -20,11 +34,17 @@ impl Command for BitsRol {
Signature::build("bits rol") Signature::build("bits rol")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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") .required("bits", SyntaxShape::Int, "number of bits to rotate left")
.switch( .switch(
"signed", "signed",
@ -33,7 +53,7 @@ impl Command for BitsRol {
) )
.named( .named(
"number-bytes", "number-bytes",
SyntaxShape::String, SyntaxShape::Int,
"the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`",
Some('n'), Some('n'),
) )
@ -41,7 +61,7 @@ impl Command for BitsRol {
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Bitwise rotate left for ints." "Bitwise rotate left for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -58,27 +78,22 @@ impl Command for BitsRol {
let head = call.head; let head = call.head;
let bits: usize = call.req(engine_state, stack, 0)?; let bits: usize = call.req(engine_state, stack, 0)?;
let signed = call.has_flag(engine_state, stack, "signed")?; let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<String>> = let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?; call.get_flag(engine_state, stack, "number-bytes")?;
let bytes_len = get_number_bytes(number_bytes.as_ref()); let number_size = get_number_bytes(number_bytes, head)?;
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,
});
}
}
// This doesn't match explicit nulls // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map(
move |value| operate(value, bits, head, signed, bytes_len), let args = Arguments {
engine_state.ctrlc.clone(), signed,
) number_size,
bits,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -96,61 +111,82 @@ impl Command for BitsRol {
Span::test_data(), 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<T: Display + PrimInt>(val: T, bits: u32, span: Span) -> Value fn action(input: &Value, args: &Arguments, span: Span) -> Value {
where let Arguments {
i64: std::convert::TryFrom<T>, signed,
{ number_size,
let rotate_result = i64::try_from(val.rotate_left(bits)); bits,
match rotate_result { } = *args;
Ok(val) => Value::int(val, span),
Err(_) => Value::error( match input {
Value::Int { val, .. } => {
use InputNumType::*;
let val = *val;
let bits = bits as u32;
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 { ShellError::GenericError {
error: "Rotate left result beyond the range of 64 bit signed number".into(), error: "result out of range for specified number".into(),
msg: format!( msg: format!(
"{val} of the specified number of bytes rotate left {bits} bits exceed limit" "rotating left by {bits} is out of range for the value {val}"
), ),
span: Some(span), span: Some(span),
help: None, help: None,
inner: vec![], inner: vec![],
}, },
span, 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),
};
fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { Value::int(int, span)
let span = value.span();
match value {
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_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),
} }
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::<Vec<u8>>();
bytes.rotate_left(byte_shift);
Value::binary(bytes, span)
} }
// Propagate errors by explicitly matching them before the final case. // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => value, Value::Error { .. } => input.clone(),
other => Value::error( other => Value::error(
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "int".into(), exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: span,
src_span: other.span(), src_span: other.span(),
}, },
head, span,
), ),
} }
} }

View File

@ -1,12 +1,26 @@
use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; 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_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::Spanned;
use nu_protocol::{ 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<Vec<CellPath>> {
None
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct BitsRor; pub struct BitsRor;
@ -20,11 +34,17 @@ impl Command for BitsRor {
Signature::build("bits ror") Signature::build("bits ror")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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") .required("bits", SyntaxShape::Int, "number of bits to rotate right")
.switch( .switch(
"signed", "signed",
@ -33,7 +53,7 @@ impl Command for BitsRor {
) )
.named( .named(
"number-bytes", "number-bytes",
SyntaxShape::String, SyntaxShape::Int,
"the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`",
Some('n'), Some('n'),
) )
@ -41,7 +61,7 @@ impl Command for BitsRor {
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Bitwise rotate right for ints." "Bitwise rotate right for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -58,103 +78,119 @@ impl Command for BitsRor {
let head = call.head; let head = call.head;
let bits: usize = call.req(engine_state, stack, 0)?; let bits: usize = call.req(engine_state, stack, 0)?;
let signed = call.has_flag(engine_state, stack, "signed")?; let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<String>> = let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?; call.get_flag(engine_state, stack, "number-bytes")?;
let bytes_len = get_number_bytes(number_bytes.as_ref()); let number_size = get_number_bytes(number_bytes, head)?;
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,
});
}
}
// This doesn't match explicit nulls // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map(
move |value| operate(value, bits, head, signed, bytes_len), let args = Arguments {
engine_state.ctrlc.clone(), signed,
) number_size,
bits,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "Rotate right a number with 60 bits", description: "rotate right a number with 2 bits",
example: "17 | bits ror 60", example: "17 | bits ror 2",
result: Some(Value::test_int(272)), result: Some(Value::test_int(68)),
}, },
Example { Example {
description: "Rotate right a list of numbers of one byte", description: "rotate right a list of numbers of two bytes",
example: "[15 33 92] | bits ror 2 --number-bytes '1'", example: "[15 33 92] | bits ror 2 --number-bytes 2",
result: Some(Value::list( result: Some(Value::list(
vec![ vec![
Value::test_int(195), Value::test_int(49155),
Value::test_int(72), Value::test_int(16392),
Value::test_int(23), Value::test_int(23),
], ],
Span::test_data(), 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<T: Display + PrimInt>(val: T, bits: u32, span: Span) -> Value fn action(input: &Value, args: &Arguments, span: Span) -> Value {
where let Arguments {
i64: std::convert::TryFrom<T>, signed,
{ number_size,
let rotate_result = i64::try_from(val.rotate_right(bits)); bits,
match rotate_result { } = *args;
Ok(val) => Value::int(val, span),
Err(_) => Value::error( match input {
Value::Int { val, .. } => {
use InputNumType::*;
let val = *val;
let bits = bits as u32;
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 { ShellError::GenericError {
error: "Rotate right result beyond the range of 64 bit signed number".into(), error: "result out of range for specified number".into(),
msg: format!( msg: format!(
"{val} of the specified number of bytes rotate right {bits} bits exceed limit" "rotating right by {bits} is out of range for the value {val}"
), ),
span: Some(span), span: Some(span),
help: None, help: None,
inner: vec![], inner: vec![],
}, },
span, 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),
};
fn operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { Value::int(int, span)
let span = value.span();
match value {
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_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),
} }
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::<Vec<u8>>();
bytes.rotate_right(byte_shift);
Value::binary(bytes, span)
} }
// Propagate errors by explicitly matching them before the final case. // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => value, Value::Error { .. } => input.clone(),
other => Value::error( other => Value::error(
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "int".into(), exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: span,
src_span: other.span(), src_span: other.span(),
}, },
head, span,
), ),
} }
} }

View File

@ -1,12 +1,27 @@
use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; 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_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::Spanned;
use nu_protocol::{ 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::iter;
use std::fmt::Display;
struct Arguments {
signed: bool,
bits: usize,
number_size: NumberBytes,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
None
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct BitsShl; pub struct BitsShl;
@ -20,11 +35,17 @@ impl Command for BitsShl {
Signature::build("bits shl") Signature::build("bits shl")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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") .required("bits", SyntaxShape::Int, "number of bits to shift left")
.switch( .switch(
"signed", "signed",
@ -33,7 +54,7 @@ impl Command for BitsShl {
) )
.named( .named(
"number-bytes", "number-bytes",
SyntaxShape::String, SyntaxShape::Int,
"the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`",
Some('n'), Some('n'),
) )
@ -41,7 +62,7 @@ impl Command for BitsShl {
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Bitwise shift left for ints." "Bitwise shift left for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -58,27 +79,22 @@ impl Command for BitsShl {
let head = call.head; let head = call.head;
let bits: usize = call.req(engine_state, stack, 0)?; let bits: usize = call.req(engine_state, stack, 0)?;
let signed = call.has_flag(engine_state, stack, "signed")?; let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<String>> = let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?; call.get_flag(engine_state, stack, "number-bytes")?;
let bytes_len = get_number_bytes(number_bytes.as_ref()); let number_size = get_number_bytes(number_bytes, head)?;
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,
});
}
}
// This doesn't match explicit nulls // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map(
move |value| operate(value, bits, head, signed, bytes_len), let args = Arguments {
engine_state.ctrlc.clone(), signed,
) number_size,
bits,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -86,17 +102,17 @@ impl Command for BitsShl {
Example { Example {
description: "Shift left a number by 7 bits", description: "Shift left a number by 7 bits",
example: "2 | bits shl 7", example: "2 | bits shl 7",
result: Some(Value::test_int(256)), result: Some(Value::test_int(0)),
}, },
Example { Example {
description: "Shift left a number with 1 byte by 7 bits", description: "Shift left a number with 2 byte by 7 bits",
example: "2 | bits shl 7 --number-bytes '1'", example: "2 | bits shl 7 --number-bytes 2",
result: Some(Value::test_int(0)), result: Some(Value::test_int(256)),
}, },
Example { Example {
description: "Shift left a signed number by 1 bit", description: "Shift left a signed number by 1 bit",
example: "0x7F | bits shl 1 --signed", example: "0x7F | bits shl 1 --signed",
result: Some(Value::test_int(254)), result: Some(Value::test_int(-2)),
}, },
Example { Example {
description: "Shift left a list of numbers", description: "Shift left a list of numbers",
@ -106,75 +122,88 @@ impl Command for BitsShl {
Span::test_data(), 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<T: CheckedShl + Display + Copy>(val: T, bits: u32, span: Span) -> Value fn action(input: &Value, args: &Arguments, span: Span) -> Value {
where let Arguments {
i64: std::convert::TryFrom<T>, signed,
{ number_size,
match val.checked_shl(bits) { bits,
Some(val) => { } = *args;
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 operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { match input {
let span = value.span();
match value {
Value::Int { val, .. } => { Value::Int { val, .. } => {
use InputNumType::*; use InputNumType::*;
// let bits = (((bits % 64) + 64) % 64) as u32; let val = *val;
let bits = bits as u32; let bits = bits as u64;
let input_type = get_input_num_type(val, signed, number_size);
match input_type { let input_num_type = get_input_num_type(val, signed, number_size);
One => get_shift_left(val as u8, bits, span), let int = match input_num_type {
Two => get_shift_left(val as u16, bits, span), One => ((val as u8) << bits) as i64,
Four => get_shift_left(val as u32, bits, span), Two => ((val as u16) << bits) as i64,
Eight => get_shift_left(val as u64, bits, span), Four => ((val as u32) << bits) as i64,
SignedOne => get_shift_left(val as i8, bits, span), Eight => {
SignedTwo => get_shift_left(val as i16, bits, span), let Ok(i) = i64::try_from((val as u64) << bits) else {
SignedFour => get_shift_left(val as i32, bits, span), return Value::error(
SignedEight => get_shift_left(val, bits, span), 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::<Vec<u8>>();
Value::binary(bytes, span)
} }
// Propagate errors by explicitly matching them before the final case. // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => value, Value::Error { .. } => input.clone(),
other => Value::error( other => Value::error(
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "int".into(), exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: span,
src_span: other.span(), src_span: other.span(),
}, },
head, span,
), ),
} }
} }

View File

@ -1,12 +1,27 @@
use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes}; 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_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::ast::CellPath;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::Spanned;
use nu_protocol::{ 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::iter;
use std::fmt::Display;
struct Arguments {
signed: bool,
bits: usize,
number_size: NumberBytes,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
None
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct BitsShr; pub struct BitsShr;
@ -20,11 +35,17 @@ impl Command for BitsShr {
Signature::build("bits shr") Signature::build("bits shr")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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") .required("bits", SyntaxShape::Int, "number of bits to shift right")
.switch( .switch(
"signed", "signed",
@ -33,7 +54,7 @@ impl Command for BitsShr {
) )
.named( .named(
"number-bytes", "number-bytes",
SyntaxShape::String, SyntaxShape::Int,
"the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`", "the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`",
Some('n'), Some('n'),
) )
@ -41,7 +62,7 @@ impl Command for BitsShr {
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Bitwise shift right for ints." "Bitwise shift right for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -58,27 +79,22 @@ impl Command for BitsShr {
let head = call.head; let head = call.head;
let bits: usize = call.req(engine_state, stack, 0)?; let bits: usize = call.req(engine_state, stack, 0)?;
let signed = call.has_flag(engine_state, stack, "signed")?; let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<String>> = let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?; call.get_flag(engine_state, stack, "number-bytes")?;
let bytes_len = get_number_bytes(number_bytes.as_ref()); let number_size = get_number_bytes(number_bytes, head)?;
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,
});
}
}
// This doesn't match explicit nulls // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map(
move |value| operate(value, bits, head, signed, bytes_len), let args = Arguments {
engine_state.ctrlc.clone(), signed,
) number_size,
bits,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -96,75 +112,75 @@ impl Command for BitsShr {
Span::test_data(), 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<T: CheckedShr + Display + Copy>(val: T, bits: u32, span: Span) -> Value fn action(input: &Value, args: &Arguments, span: Span) -> Value {
where let Arguments {
i64: std::convert::TryFrom<T>, signed,
{ number_size,
match val.checked_shr(bits) { bits,
Some(val) => { } = *args;
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 operate(value: Value, bits: usize, head: Span, signed: bool, number_size: NumberBytes) -> Value { match input {
let span = value.span();
match value {
Value::Int { val, .. } => { Value::Int { val, .. } => {
use InputNumType::*; use InputNumType::*;
// let bits = (((bits % 64) + 64) % 64) as u32; let val = *val;
let bits = bits as u32; let bits = bits as u32;
let input_type = get_input_num_type(val, signed, number_size); let input_num_type = get_input_num_type(val, signed, number_size);
match input_type {
One => get_shift_right(val as u8, bits, span), let int = match input_num_type {
Two => get_shift_right(val as u16, bits, span), One => ((val as u8) >> bits) as i64,
Four => get_shift_right(val as u32, bits, span), Two => ((val as u16) >> bits) as i64,
Eight => get_shift_right(val as u64, bits, span), Four => ((val as u32) >> bits) as i64,
SignedOne => get_shift_right(val as i8, bits, span), Eight => ((val as u64) >> bits) as i64,
SignedTwo => get_shift_right(val as i16, bits, span), SignedOne => ((val as i8) >> bits) as i64,
SignedFour => get_shift_right(val as i32, bits, span), SignedTwo => ((val as i16) >> bits) as i64,
SignedEight => get_shift_right(val, bits, span), 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::<Vec<u8>>();
Value::binary(bytes, span)
} }
// Propagate errors by explicitly matching them before the final case. // Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => value, Value::Error { .. } => input.clone(),
other => Value::error( other => Value::error(
ShellError::OnlySupportsThisInputType { ShellError::OnlySupportsThisInputType {
exp_input_type: "int".into(), exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(), wrong_type: other.get_type().to_string(),
dst_span: head, dst_span: span,
src_span: other.span(), src_span: other.span(),
}, },
head, span,
), ),
} }
} }

View File

@ -1,8 +1,9 @@
use super::binary_op;
use nu_engine::CallExt; use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -17,17 +18,33 @@ impl Command for BitsXor {
Signature::build("bits xor") Signature::build("bits xor")
.input_output_types(vec![ .input_output_types(vec![
(Type::Int, Type::Int), (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::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) .category(Category::Bits)
} }
fn usage(&self) -> &str { fn usage(&self) -> &str {
"Performs bitwise xor for ints." "Performs bitwise xor for ints or binary values."
} }
fn search_terms(&self) -> Vec<&str> { fn search_terms(&self) -> Vec<&str> {
@ -42,13 +59,32 @@ impl Command for BitsXor {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; 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::<Spanned<String>>(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 // This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) { if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head }); return Err(ShellError::PipelineEmpty { dst_span: head });
} }
input.map( 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(), engine_state.ctrlc.clone(),
) )
} }
@ -61,35 +97,34 @@ impl Command for BitsXor {
result: Some(Value::test_int(0)), result: Some(Value::test_int(0)),
}, },
Example { 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", example: "[8 3 2] | bits xor 2",
result: Some(Value::list( result: Some(Value::test_list(vec![
vec![Value::test_int(10), Value::test_int(1), Value::test_int(0)], Value::test_int(10),
Span::test_data(), 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -65,7 +65,7 @@ fn bits_shift_left_negative() -> TestResult {
fn bits_shift_left_list() -> TestResult { fn bits_shift_left_list() -> TestResult {
run_test( run_test(
"[1 2 7 32 9 10] | bits shl 3 | str join '.'", "[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 { fn bits_rotate_left_list() -> TestResult {
run_test( run_test(
"[1 2 7 32 9 10] | bits rol 3 | str join '.'", "[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 { fn bits_rotate_right_list() -> TestResult {
run_test( run_test(
"[1 2 7 32 23 10] | bits ror 60 | str join '.'", "[1 2 7 32 23 10] | bits ror 60 | str join '.'",
"16.32.112.512.368.160", "16.32.112.2.113.160",
) )
} }