Skip to content

Instantly share code, notes, and snippets.

@evaporei
Created May 8, 2019 04:03
Show Gist options
  • Save evaporei/98c5e5c8ae63cc4b829b7c211d20b49e to your computer and use it in GitHub Desktop.
Save evaporei/98c5e5c8ae63cc4b829b7c211d20b49e to your computer and use it in GitHub Desktop.
How to parse array of bytes using a state machine (in Rust!)
struct State {
step: Step,
data: Vec<u8>,
}
impl State {
pub fn new() -> Self {
Self { step: Step::Initial, data: vec![] }
}
pub fn parse(&mut self, data: Vec<u8>) -> Result<(), Error> {
// This works like a cursor or a state machine with the step enum
for byte in &data {
match self.step {
Step::Initial => {
if *byte != 1 {
return Err(Error::WrongInitialByte);
}
self.step = Step::Middle;
}
Step::Middle => {
if self.data.len() > 3 {
return Err(Error::DataOverflow);
}
if self.data.len() == 3 {
if *byte != 100 {
return Err(Error::WrongFinalByte);
}
self.step = Step::Finished;
}
self.data.push(*byte);
}
Step::Finished => {}
}
}
if self.step != Step::Finished {
return Ok(());
}
self.process();
Ok(())
}
pub fn process(&mut self) {
// do any logic with complete packet/data
// then empty it
self.data.clear();
// reset, to read more packets
self.step = Step::Initial;
}
}
#[derive(PartialEq)]
enum Step {
Initial,
Middle,
Finished,
}
#[derive(Debug, PartialEq)]
enum Error {
WrongInitialByte,
DataOverflow,
WrongFinalByte,
}
fn main() {
// Let's consider a binary protocol that is based of a list of bytes of length 5.
// The first byte sent is a `1`, and the last, a `100`.
// The ones in the middle don't really matter which numbers they are, however
// they have the main content of the delivery.
// Here we create a state struct, that has no data in it (3 significant bytes)
// of our fictitious protocol. Also, it hasn't parsed anything, so it is on the
// initial step.
let mut state = State::new();
// Here is an example of receiving the full packet at once:
state.parse(vec![1, 22, 42, 33, 100]).unwrap();
// Here, we receive the packet in parts.
// The great thing is that this state machine allows you
// to call `parse` how many times needed, this is useful when dealing
// with separate deliveries of something sent via USB or Bluetooth, for example.
// This works much like a cursor.
state.parse(vec![1, 22]).unwrap();
state.parse(vec![32, 44]).unwrap();
state.parse(vec![100]).unwrap();
// passing the wrong first byte (not 1)
assert_eq!(state.parse(vec![55]), Err(Error::WrongInitialByte));
// editing `data` without `parse`
state.parse(vec![1]).unwrap();
state.parse(vec![74, 26, 31]).unwrap();
state.data.push(25);
assert_eq!(state.parse(vec![100]), Err(Error::DataOverflow));
// reset because of DataOverflow error
state.data.clear();
state.step = Step::Initial;
// passing the wrong final byte (not 100)
state.parse(vec![1]).unwrap();
state.parse(vec![74, 26, 31]).unwrap();
assert_eq!(state.parse(vec![73]), Err(Error::WrongFinalByte));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment