From 3fd92b64379f83bde4e4ad59c306e5a801e3c49d Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:04:07 -0500 Subject: [PATCH] convert a string to a raw binary string of 0s and 1s (#9534) # Description This PR converts a string into a raw binary represented by a string of 0s and 1s padded to 8 digits with zeros. This is useful for encoding data. ![image](https://github.com/nushell/nushell/assets/343840/66864c79-3da1-4007-a62b-306ed85f4df4) # User-Facing Changes # Tests + Formatting # After Submitting --- crates/nu-cmd-extra/src/extra/bits/into.rs | 273 ++++++++++++++++++ crates/nu-cmd-extra/src/extra/bits/mod.rs | 2 + crates/nu-cmd-extra/src/extra/mod.rs | 27 +- .../nu-command/src/conversions/into/binary.rs | 21 +- 4 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 crates/nu-cmd-extra/src/extra/bits/into.rs diff --git a/crates/nu-cmd-extra/src/extra/bits/into.rs b/crates/nu-cmd-extra/src/extra/bits/into.rs new file mode 100644 index 0000000000..7f8ce49102 --- /dev/null +++ b/crates/nu-cmd-extra/src/extra/bits/into.rs @@ -0,0 +1,273 @@ +use nu_cmd_base::input_handler::{operate, CmdArgument}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, +}; +use num_traits::ToPrimitive; + +pub struct Arguments { + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + +#[derive(Clone)] +pub struct BitsInto; + +impl Command for BitsInto { + fn name(&self) -> &str { + "into bits" + } + + fn signature(&self) -> Signature { + Signature::build("into bits") + .input_output_types(vec![ + (Type::Binary, Type::String), + (Type::Int, Type::String), + (Type::Filesize, Type::String), + (Type::Duration, Type::String), + (Type::String, Type::String), + (Type::Bool, Type::String), + (Type::Date, Type::String), + ]) + .allow_variants_without_examples(true) // TODO: supply exhaustive examples + .rest( + "rest", + SyntaxShape::CellPath, + "for a data structure input, convert data at the given cell paths", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to a binary primitive." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "cast"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_bits(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a binary value into a string, padded to 8 places with 0s", + example: "01b | into bits", + result: Some(Value::String { + val: "00000001".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert an int into a string, padded to 8 places with 0s", + example: "1 | into bits", + result: Some(Value::String { + val: "00000001".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a filesize value into a string, padded to 8 places with 0s", + example: "1b | into bits", + result: Some(Value::String { + val: "00000001".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a duration value into a string, padded to 8 places with 0s", + example: "1ns | into bits", + result: Some(Value::String { + val: "00000001".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a boolean value into a string, padded to 8 places with 0s", + example: "true | into bits", + result: Some(Value::String { + val: "00000001".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a datetime value into a string, padded to 8 places with 0s", + example: "2023-04-17T01:02:03 | into bits", + result: Some(Value::String { + val: "01001101 01101111 01101110 00100000 01000001 01110000 01110010 00100000 00110001 00110111 00100000 00110000 00110001 00111010 00110000 00110010 00111010 00110000 00110011 00100000 00110010 00110000 00110010 00110011".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string into a raw binary string, padded with 0s to 8 places", + example: "'nushell.sh' | into bits", + result: Some(Value::String { + val: "01101110 01110101 01110011 01101000 01100101 01101100 01101100 00101110 01110011 01101000".to_string(), + span: Span::test_data(), + }), + }, + ] + } +} + +fn into_bits( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let cell_paths = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + + match input { + PipelineData::ExternalStream { stdout: None, .. } => Ok(Value::Binary { + val: vec![], + span: head, + } + .into_pipeline_data()), + PipelineData::ExternalStream { + stdout: Some(stream), + .. + } => { + // TODO: in the future, we may want this to stream out, converting each to bytes + let output = stream.into_bytes()?; + Ok(Value::Binary { + val: output.item, + span: head, + } + .into_pipeline_data()) + } + _ => { + let args = Arguments { cell_paths }; + operate(action, args, input, call.head, engine_state.ctrlc.clone()) + } + } +} + +fn convert_to_smallest_number_type(num: i64, span: Span) -> Value { + if let Some(v) = num.to_i8() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } else if let Some(v) = num.to_i16() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } else if let Some(v) = num.to_i32() { + let bytes = v.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } else { + let bytes = num.to_ne_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } +} + +pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value { + match input { + Value::Binary { val, .. } => { + let mut raw_string = "".to_string(); + for ch in val { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } + Value::Int { val, .. } => convert_to_smallest_number_type(*val, span), + Value::Filesize { val, .. } => convert_to_smallest_number_type(*val, span), + Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span), + Value::String { val, .. } => { + let raw_bytes = val.as_bytes(); + let mut raw_string = "".to_string(); + for ch in raw_bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } + Value::Bool { val, .. } => { + let v = >::from(*val); + convert_to_smallest_number_type(v, span) + } + Value::Date { val, .. } => { + let value = val.format("%c").to_string(); + let bytes = value.as_bytes(); + let mut raw_string = "".to_string(); + for ch in bytes { + raw_string.push_str(&format!("{:08b} ", ch)); + } + Value::String { + val: raw_string.trim().to_string(), + span, + } + } + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::Error { + error: Box::new(ShellError::OnlySupportsThisInputType { + exp_input_type: "integer, filesize, string, date, duration, binary or bool".into(), + wrong_type: other.get_type().to_string(), + dst_span: span, + src_span: other.expect_span(), + }), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(BitsInto {}) + } +} diff --git a/crates/nu-cmd-extra/src/extra/bits/mod.rs b/crates/nu-cmd-extra/src/extra/bits/mod.rs index 79db0de629..40f951e1f5 100644 --- a/crates/nu-cmd-extra/src/extra/bits/mod.rs +++ b/crates/nu-cmd-extra/src/extra/bits/mod.rs @@ -1,5 +1,6 @@ mod and; mod bits_; +mod into; mod not; mod or; mod rotate_left; @@ -10,6 +11,7 @@ mod xor; pub use and::BitsAnd; pub use bits_::Bits; +pub use into::BitsInto; pub use not::BitsNot; pub use or::BitsOr; pub use rotate_left::BitsRol; diff --git a/crates/nu-cmd-extra/src/extra/mod.rs b/crates/nu-cmd-extra/src/extra/mod.rs index 4ec81d57a3..ca026baa49 100644 --- a/crates/nu-cmd-extra/src/extra/mod.rs +++ b/crates/nu-cmd-extra/src/extra/mod.rs @@ -1,16 +1,6 @@ mod bits; mod bytes; -pub use bits::Bits; -pub use bits::BitsAnd; -pub use bits::BitsNot; -pub use bits::BitsOr; -pub use bits::BitsRol; -pub use bits::BitsRor; -pub use bits::BitsShl; -pub use bits::BitsShr; -pub use bits::BitsXor; - pub use bytes::Bytes; pub use bytes::BytesAdd; pub use bytes::BytesAt; @@ -24,6 +14,17 @@ pub use bytes::BytesReplace; pub use bytes::BytesReverse; pub use bytes::BytesStartsWith; +pub use bits::Bits; +pub use bits::BitsAnd; +pub use bits::BitsInto; +pub use bits::BitsNot; +pub use bits::BitsOr; +pub use bits::BitsRol; +pub use bits::BitsRor; +pub use bits::BitsShl; +pub use bits::BitsShr; +pub use bits::BitsXor; + use nu_protocol::engine::{EngineState, StateWorkingSet}; pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState { @@ -39,16 +40,18 @@ pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState { }; } + // Bits bind_command! { Bits, BitsAnd, + BitsInto, BitsNot, BitsOr, - BitsXor, BitsRol, BitsRor, BitsShl, - BitsShr + BitsShr, + BitsXor } // Bytes diff --git a/crates/nu-command/src/conversions/into/binary.rs b/crates/nu-command/src/conversions/into/binary.rs index 0cebb6a1e1..fa5ccac20d 100644 --- a/crates/nu-command/src/conversions/into/binary.rs +++ b/crates/nu-command/src/conversions/into/binary.rs @@ -1,4 +1,4 @@ -use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs}; +use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::{ ast::{Call, CellPath}, @@ -7,6 +7,16 @@ use nu_protocol::{ Type, Value, }; +pub struct Arguments { + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + #[derive(Clone)] pub struct SubCommand; @@ -111,7 +121,8 @@ fn into_binary( input: PipelineData, ) -> Result { let head = call.head; - let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let cell_paths = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); match input { PipelineData::ExternalStream { stdout: None, .. } => Ok(Value::Binary { @@ -132,8 +143,8 @@ fn into_binary( .into_pipeline_data()) } _ => { - let arg = CellPathOnlyArgs::from(cell_paths); - operate(action, arg, input, call.head, engine_state.ctrlc.clone()) + let args = Arguments { cell_paths }; + operate(action, args, input, call.head, engine_state.ctrlc.clone()) } } } @@ -154,7 +165,7 @@ fn float_to_endian(n: f64) -> Vec { } } -pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { +pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value { match input { Value::Binary { .. } => input.clone(), Value::Int { val, .. } => Value::Binary {