178 lines
5.4 KiB
Rust
178 lines
5.4 KiB
Rust
//! Utility functions to quickly encode and decode `&[u8]` to and from framed messages.
|
|
//!
|
|
//! Framed messages consist of the following items:
|
|
//!
|
|
//! ```txt
|
|
//! | len: u32 of data.len() | data: binary data |
|
|
//! ```
|
|
//!
|
|
//! The data stored after the length consists of the following items:
|
|
//!
|
|
//! ```txt
|
|
//! | checksum: [u8; 32] sha256 hash of `raw_data` | raw_data: &[u8] |
|
|
//! ```
|
|
|
|
use std::io::{Read, Write};
|
|
|
|
#[cfg(feature = "async")]
|
|
pub mod asyncext;
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum DecodeError {
|
|
/// There were not enough bytes to determine the length of the data slice.
|
|
#[error("Invalid length: {0}")]
|
|
InvalidLength(std::array::TryFromSliceError),
|
|
|
|
/// There were not enough bytes to read a checksum of the data slice.
|
|
#[error("Invalid checksum: {0} bytes")]
|
|
InvalidChecksum(std::array::TryFromSliceError),
|
|
|
|
/// There were not enough bytes to read the rest of the data.
|
|
#[error("Incorrect length of internal data: {0}, expected at least: {1}")]
|
|
IncorrectLength(usize, u32),
|
|
|
|
/// The provided checksum of the data did not match the locally-generated checksum.
|
|
#[error("Checksum did not match: Their {0} != Our {1}")]
|
|
BadChecksum(String, String),
|
|
|
|
/// Data could not be read from the input source.
|
|
#[error("Data could not be read from the input source: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum EncodeError {
|
|
/// The given input was larger than could be encoded by this protocol.
|
|
#[error("Input too large to encode: {0}")]
|
|
InputTooLarge(usize),
|
|
|
|
/// Data could not be written to the output sink.
|
|
#[error("Data could not be written to the output sink: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
|
|
let mut hashobj = Sha256::new();
|
|
hashobj.update(data);
|
|
hashobj.finalize().to_vec()
|
|
}
|
|
|
|
/// Encode a given `&[u8]` to a framed message.
|
|
///
|
|
/// # Errors
|
|
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
|
/// constraint on a protocol level.
|
|
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
|
let mut output = vec![];
|
|
try_encode_to(data, &mut output)?;
|
|
Ok(output)
|
|
}
|
|
|
|
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
|
let hash = hash(data);
|
|
let len = hash.len() + data.len();
|
|
let len = u32::try_from(len).map_err(|_| EncodeError::InputTooLarge(len))?;
|
|
writable.write_all(&len.to_be_bytes())?;
|
|
writable.write_all(&hash)?;
|
|
writable.write_all(data)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
|
|
let checksum: &[u8; 32] = &data[..32]
|
|
.try_into()
|
|
.map_err(DecodeError::InvalidChecksum)?;
|
|
|
|
let content = &data[32..];
|
|
let our_checksum = hash(content);
|
|
if our_checksum != checksum {
|
|
return Err(DecodeError::BadChecksum(
|
|
hex::encode(checksum),
|
|
hex::encode(our_checksum),
|
|
));
|
|
}
|
|
Ok(content)
|
|
}
|
|
|
|
/// Decode a framed message into a `Vec<u8>`.
|
|
///
|
|
/// # Errors
|
|
/// An error may be returned if:
|
|
/// * The given `data` does not contain enough data to parse a length,
|
|
/// * The given `data` does not contain the given length's worth of data,
|
|
/// * The given `data` has a checksum that does not match what we build locally.
|
|
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
|
try_decode_from(&mut &data[..])
|
|
}
|
|
|
|
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
|
let mut bytes = 0u32.to_be_bytes();
|
|
readable.read_exact(&mut bytes)?;
|
|
let len = u32::from_be_bytes(bytes);
|
|
let mut data = vec![0u8; len as usize];
|
|
readable.read_exact(&mut data)?;
|
|
let content = verify_checksum(&data)?;
|
|
Ok(content.to_vec())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{try_decode, try_encode, DecodeError};
|
|
|
|
const LEN_SIZE: usize = (u32::BITS / 8) as usize;
|
|
|
|
#[test]
|
|
fn stable_interface() {
|
|
let data = (0..255).collect::<Vec<u8>>();
|
|
insta::assert_debug_snapshot!(try_encode(&data[..]));
|
|
}
|
|
|
|
#[test]
|
|
fn equivalency() -> Result<(), DecodeError> {
|
|
let data = (0..255).collect::<Vec<u8>>();
|
|
assert_eq!(try_decode(&try_encode(&data[..]).unwrap())?, data);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn allows_extra_data() -> Result<(), DecodeError> {
|
|
let data = (0..255).collect::<Vec<u8>>();
|
|
let mut encoded = try_encode(&data[..]).unwrap();
|
|
// Throw on some extra data
|
|
encoded.extend(0..255);
|
|
assert_eq!(try_decode(&try_encode(&data[..]).unwrap())?, data);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn error_on_smaller_data() {
|
|
// Data sliced by 1 byte
|
|
let data = (0..255).collect::<Vec<u8>>();
|
|
let encoded = try_encode(&data[..]).unwrap();
|
|
let error = try_decode(&encoded[..data.len() - 1]);
|
|
assert!(error.is_err());
|
|
|
|
// Data includes length and checksum
|
|
let error = try_decode(&encoded[..LEN_SIZE + 256 / 8]);
|
|
assert!(error.is_err());
|
|
|
|
// Data only includes length
|
|
let data = 12u32.to_be_bytes();
|
|
let error = try_decode(&data[..]);
|
|
assert!(error.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn error_on_invalid_checksum() {
|
|
let data = (0..255).collect::<Vec<u8>>();
|
|
let mut encoded = try_encode(&data[..]).unwrap();
|
|
assert_ne!(encoded[LEN_SIZE + 1], 0);
|
|
encoded[LEN_SIZE + 1] = 0;
|
|
|
|
let error = try_decode(&data[..]);
|
|
assert!(error.is_err());
|
|
}
|
|
}
|