diff --git a/README.md b/README.md index fce786f..60cac00 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ The built-in blocks provided by Protoflow are listed below: | [`Const`] | Sends a constant value. | | [`Count`] | Counts the number of messages it receives, while optionally passing them through. | | [`Decode`] | Decodes messages from a byte stream. | +| [`DecodeHex`] | Decodes hexadecimal stream to byte stream. | | [`DecodeJSON`] | Decodes JSON messages from a byte stream. | | [`Delay`] | Passes messages through while delaying them by a fixed or random duration. | | [`Drop`] | Discards all messages it receives. | @@ -213,6 +214,28 @@ block-beta protoflow execute Decode encoding=text ``` +#### [`DecodeHex`] + +A block that decodes a hexadecimal byte stream into bytes. + +```mermaid +block-beta + columns 7 + Source space:2 DecodeHex space:2 Sink + Source-- "input" -->DecodeHex + DecodeHex-- "output" -->Sink + + classDef block height:48px,padding:8px; + classDef hidden visibility:none; + class DecodeHex block + class Source hidden + class Sink hidden +``` + +```bash +protoflow execute DecodeHex +``` + #### [`DecodeJSON`] A block that decodes JSON messages from a byte stream. @@ -578,6 +601,7 @@ git clone https://github.com/AsimovPlatform/protoflow.git [`Const`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Const.html [`Count`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Count.html [`Decode`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Decode.html +[`DecodeHex`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.DecodeHex.html [`DecodeJSON`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.DecodeJson.html [`Delay`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Delay.html [`Drop`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Drop.html diff --git a/lib/protoflow-blocks/doc/io/decode_hex.mmd b/lib/protoflow-blocks/doc/io/decode_hex.mmd new file mode 100644 index 0000000..50b9aac --- /dev/null +++ b/lib/protoflow-blocks/doc/io/decode_hex.mmd @@ -0,0 +1,11 @@ +block-beta + columns 7 + Source space:2 DecodeHex space:2 Sink + Source-- "input" -->DecodeHex + DecodeHex-- "output" -->Sink + + classDef block height:48px,padding:8px; + classDef hidden visibility:none; + class DecodeHex block + class Source hidden + class Sink hidden \ No newline at end of file diff --git a/lib/protoflow-blocks/doc/io/decode_hex.seq.mmd b/lib/protoflow-blocks/doc/io/decode_hex.seq.mmd new file mode 100644 index 0000000..67d49f5 --- /dev/null +++ b/lib/protoflow-blocks/doc/io/decode_hex.seq.mmd @@ -0,0 +1,21 @@ +sequenceDiagram + autonumber + participant BlockA as Another block + participant DecodeHex.input as DecodeHex.input port + participant DecodeHex as DecodeHex block + participant DecodeHex.output as DecodeHex.output port + participant BlockB as Another block + + BlockA-->>DecodeHex: Connect + DecodeHex-->>BlockB: Connect + + loop DecodeHex process + BlockA->>DecodeHex: Message (Hexadecimal Bytes) + DecodeHex->>DecodeHex: Decode from hexadecimal + DecodeHex->>BlockB: Message (Bytes) + end + + BlockA-->>DecodeHex: Disconnect + DecodeHex-->>DecodeHex.input: Close + DecodeHex-->>DecodeHex.output: Close + DecodeHex-->>BlockB: Disconnect diff --git a/lib/protoflow-blocks/src/block_config.rs b/lib/protoflow-blocks/src/block_config.rs index 11be39c..6929653 100644 --- a/lib/protoflow-blocks/src/block_config.rs +++ b/lib/protoflow-blocks/src/block_config.rs @@ -49,7 +49,7 @@ impl<'de> serde::Deserialize<'de> for BlockConfig { .map(BlockConfig::Hash) .unwrap(), - "Decode" | "Encode" | "EncodeHex" | "EncodeJSON" => { + "Decode" | "DecodeHex" | "Encode" | "EncodeHex" | "EncodeJSON" => { IoBlockConfig::deserialize(value.clone()) .map(BlockConfig::Io) .unwrap() diff --git a/lib/protoflow-blocks/src/block_tag.rs b/lib/protoflow-blocks/src/block_tag.rs index a9d526e..3eb6e66 100644 --- a/lib/protoflow-blocks/src/block_tag.rs +++ b/lib/protoflow-blocks/src/block_tag.rs @@ -23,6 +23,7 @@ pub enum BlockTag { Hash, // IoBlocks Decode, + DecodeHex, Encode, EncodeHex, EncodeJson, @@ -66,6 +67,7 @@ impl BlockTag { #[cfg(feature = "hash")] Hash => "Hash", Decode => "Decode", + DecodeHex => "DecodeHex", Encode => "Encode", EncodeHex => "EncodeHex", EncodeJson => "EncodeJSON", @@ -102,6 +104,7 @@ impl FromStr for BlockTag { #[cfg(feature = "hash")] "Hash" => Hash, "Decode" => Decode, + "DecodeHex" => DecodeHex, "Encode" => Encode, "EncodeHex" => EncodeHex, "EncodeJSON" => EncodeJson, @@ -149,6 +152,7 @@ impl BlockInstantiation for BlockTag { #[cfg(feature = "hash")] Hash => Box::new(super::Hash::with_system(system, None)), Decode => Box::new(super::Decode::::with_system(system, None)), + DecodeHex => Box::new(super::DecodeHex::with_system(system)), Encode => Box::new(super::Encode::::with_system(system, None)), EncodeHex => Box::new(super::EncodeHex::with_system(system)), EncodeJson => Box::new(super::EncodeJson::with_system(system)), diff --git a/lib/protoflow-blocks/src/blocks/io.rs b/lib/protoflow-blocks/src/blocks/io.rs index 17452ec..898838f 100644 --- a/lib/protoflow-blocks/src/blocks/io.rs +++ b/lib/protoflow-blocks/src/blocks/io.rs @@ -13,6 +13,7 @@ pub mod io { pub trait IoBlocks { fn decode(&mut self) -> Decode; + fn decode_hex(&mut self) -> DecodeHex; fn decode_json(&mut self) -> DecodeJson; fn decode_with(&mut self, encoding: Encoding) -> Decode; @@ -37,6 +38,7 @@ pub mod io { #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum IoBlockTag { Decode, + DecodeHex, Encode, EncodeHex, DecodeJson, @@ -51,7 +53,10 @@ pub mod io { output: OutputPortName, encoding: Option, }, - + DecodeHex { + input: InputPortName, + output: OutputPortName, + }, #[cfg_attr(feature = "serde", serde(rename = "DecodeJSON"))] DecodeJson { input: InputPortName, @@ -81,6 +86,7 @@ pub mod io { use IoBlockConfig::*; Cow::Borrowed(match self { Decode { .. } => "Decode", + DecodeHex { .. } => "DecodeHex", DecodeJson { .. } => "DecodeJSON", Encode { .. } => "Encode", EncodeHex { .. } => "EncodeHex", @@ -94,6 +100,7 @@ pub mod io { use IoBlockConfig::*; match self { Decode { output, .. } + | DecodeHex { output, .. } | DecodeJson { output, .. } | Encode { output, .. } | EncodeHex { output, .. } @@ -111,6 +118,7 @@ pub mod io { Decode { encoding, .. } => { Box::new(super::Decode::::with_system(system, *encoding)) } + DecodeHex { .. } => Box::new(super::DecodeHex::with_system(system)), DecodeJson { .. } => Box::new(super::DecodeJson::with_system(system)), Encode { encoding, .. } => { Box::new(super::Encode::::with_system(system, *encoding)) @@ -133,6 +141,9 @@ pub mod io { mod encode_hex; pub use encode_hex::*; + mod decode_hex; + pub use decode_hex::*; + mod encode_json; pub use encode_json::*; } diff --git a/lib/protoflow-blocks/src/blocks/io/decode_hex.rs b/lib/protoflow-blocks/src/blocks/io/decode_hex.rs new file mode 100644 index 0000000..40157a5 --- /dev/null +++ b/lib/protoflow-blocks/src/blocks/io/decode_hex.rs @@ -0,0 +1,145 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{ + prelude::{Bytes, Vec}, + IoBlocks, StdioConfig, StdioError, StdioSystem, System, +}; +use protoflow_core::{Block, BlockResult, BlockRuntime, InputPort, OutputPort}; +use protoflow_derive::Block; +use simple_mermaid::mermaid; + +/// A block that decodes a hexadecimal byte stream to byte. +/// +/// # Block Diagram +#[doc = mermaid!("../../../doc/io/decode_hex.mmd")] +/// +/// # Sequence Diagram +#[doc = mermaid!("../../../doc/io/decode_hex.seq.mmd" framed)] +/// +/// # Examples +/// +/// ## Using the block in a system +/// +/// ```rust +/// # use protoflow_blocks::*; +/// # fn main() { +/// System::build(|s| { +/// let stdin = s.read_stdin(); +/// let hex_decoder = s.decode_hex(); +/// let stdout = s.write_stdout(); +/// s.connect(&stdin.output, &hex_decoder.input); +/// s.connect(&hex_decoder.output, &stdout.input); +/// }); +/// # } +/// ``` +/// +/// ## Running the block via the CLI +/// +/// ```console +/// $ protoflow execute DecodeHex +/// ``` +/// +#[derive(Block, Clone)] +pub struct DecodeHex { + /// The input text stream. + #[input] + pub input: InputPort, + + /// The output byte stream. + #[output] + pub output: OutputPort, +} + +impl DecodeHex { + pub fn new(input: InputPort, output: OutputPort) -> Self { + Self { input, output } + } + + pub fn with_system(system: &System) -> Self { + use crate::SystemBuilding; + Self::new(system.input(), system.output()) + } +} + +impl Block for DecodeHex { + fn execute(&mut self, runtime: &dyn BlockRuntime) -> BlockResult { + runtime.wait_for(&self.input)?; + + while let Some(message) = self.input.recv()? { + let decoded = hex_to_bytes(&message); + self.output.send(&decoded)?; + } + + Ok(()) + } +} +fn hex_to_bytes(hex_message: &Bytes) -> Bytes { + let mut decoded = Vec::with_capacity(hex_message.len() / 2); + + for chunk in hex_message.chunks_exact(2) { + let high = chunk[0]; + let low = chunk[1]; + decoded.push((hex_value(high) << 4) | hex_value(low)); + } + + Bytes::from(decoded) +} + +#[inline(always)] +fn hex_value(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => panic!("Invalid hex character"), + } +} + +#[cfg(feature = "std")] +impl StdioSystem for DecodeHex { + fn build_system(config: StdioConfig) -> Result { + use crate::SystemBuilding; + + config.reject_any()?; + + Ok(System::build(|s| { + let stdin = config.read_stdin(s); + let hex_decoder = s.decode_hex(); + let stdout = config.write_stdout(s); + s.connect(&stdin.output, &hex_decoder.input); + s.connect(&hex_decoder.output, &stdout.input); + })) + } +} + +#[cfg(test)] +mod tests { + use super::DecodeHex; + use crate::{SysBlocks, System, SystemBuilding}; + + #[test] + fn instantiate_block() { + // Check that the block is constructible: + let _ = System::build(|s| { + let _ = s.block(DecodeHex::new(s.input(), s.output())); + }); + } + + #[test] + #[ignore] + fn test_encode_decode_hex() { + use super::*; + + let _ = System::run(|s| { + let stdin = s.read_stdin(); + let hex_encoder = s.encode_hex(); + s.connect(&stdin.output, &hex_encoder.input); + + let hex_decoder = s.decode_hex(); + s.connect(&hex_encoder.output, &hex_decoder.input); + + let stdout = s.write_stdout(); + s.connect(&hex_decoder.output, &stdout.input); + }); + } +} diff --git a/lib/protoflow-blocks/src/lib.rs b/lib/protoflow-blocks/src/lib.rs index 17b35c6..56d8620 100644 --- a/lib/protoflow-blocks/src/lib.rs +++ b/lib/protoflow-blocks/src/lib.rs @@ -65,6 +65,7 @@ pub fn build_stdio_system( "Hash" => Hash::build_system(config)?, // IoBlocks "Decode" => Decode::build_system(config)?, + "DecodeHex" => DecodeHex::build_system(config)?, "DecodeJSON" => DecodeJson::build_system(config)?, "Encode" => Encode::build_system(config)?, "EncodeHex" => EncodeHex::build_system(config)?, diff --git a/lib/protoflow-blocks/src/system.rs b/lib/protoflow-blocks/src/system.rs index 3b2ddac..b850013 100644 --- a/lib/protoflow-blocks/src/system.rs +++ b/lib/protoflow-blocks/src/system.rs @@ -5,9 +5,9 @@ use crate::{ prelude::{fmt, Arc, Box, FromStr, Rc, String, ToString}, types::{DelayType, Encoding}, - AllBlocks, Buffer, Const, CoreBlocks, Count, Decode, DecodeJson, Delay, Drop, Encode, - EncodeHex, EncodeJson, FlowBlocks, HashBlocks, IoBlocks, MathBlocks, Random, ReadDir, ReadEnv, - ReadFile, ReadStdin, SysBlocks, TextBlocks, WriteFile, WriteStderr, WriteStdout, + AllBlocks, Buffer, Const, CoreBlocks, Count, Decode, DecodeHex, DecodeJson, Delay, Drop, + Encode, EncodeHex, EncodeJson, FlowBlocks, HashBlocks, IoBlocks, MathBlocks, Random, ReadDir, + ReadEnv, ReadFile, ReadStdin, SysBlocks, TextBlocks, WriteFile, WriteStderr, WriteStdout, }; use protoflow_core::{ Block, BlockID, BlockResult, InputPort, Message, OutputPort, PortID, PortResult, Process, @@ -148,6 +148,10 @@ impl IoBlocks for System { self.0.block(Decode::::with_system(self, None)) } + fn decode_hex(&mut self) -> DecodeHex { + self.0.block(DecodeHex::with_system(self)) + } + fn decode_json(&mut self) -> DecodeJson { self.0.block(DecodeJson::with_system(self)) }