Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codec/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ pub mod net;
pub mod primitives;
pub mod tuple;
pub mod vec;
pub mod zeroed;
125 changes: 125 additions & 0 deletions codec/src/types/zeroed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Zero-padded data codec implementation.
use crate::{error::Error, util::at_least, EncodeSize, Read, Write};
use bytes::{Buf, BufMut};

/// A codec for zero-padded data of a specified length.
///
/// When decoding, it validates that all bytes in the specified range are zero,
/// returning an error if any non-zero bytes are found.
#[derive(Debug, Clone, PartialEq)]
pub struct Zeroed {
n: usize,
}
Comment on lines +11 to +13
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to have Zeroed<const N: usize> similar to the FixedSize type in utils/? Then the read_cfg Read::Cfgtype could be()`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it looks like that in stable Rust currently, only constants that are independent of generic type resolution can be used in const generics.

Associated constants of traits (e.g. V::SIZE) are type-dependent and therefore cannot currently appear in const generic arguments. This is the tracking issue on rust to enable generic const expres

image


impl Zeroed {
/// Creates a new `Zeroed` instance for `n` bytes.
pub fn new(n: usize) -> Self {
Self { n }
}
}

impl Read for Zeroed {
type Cfg = usize;

/// Reads and validates zero-padded data from the buffer.
///
/// The configuration specifies the expected number of zero bytes to read.
/// This method validates that all bytes in the specified range are zero,
/// returning an error if any non-zero bytes are encountered.
///
/// ```rust
/// use commonware_codec::{Read, EncodeSize};
/// use commonware_codec::types::zeroed::Zeroed;
///
/// let mut buf = &[0u8, 0u8, 0u8, 0u8][..];
/// let zeroed = Zeroed::read_cfg(&mut buf, &4).unwrap();
/// assert_eq!(zeroed.encode_size(), 4);
/// ```
fn read_cfg(buf: &mut impl Buf, n: &Self::Cfg) -> Result<Self, Error> {
at_least(buf, *n)?;

let mut remaining = *n;
while remaining > 0 {
let chunk = buf.chunk();
let len: usize = chunk.len().min(remaining);
if chunk[..len].iter().any(|&b| b != 0) {
return Err(Error::Invalid("Zeroed", "bytes are not zero"));
}
buf.advance(len);
remaining -= len;
}

Ok(Self { n: *n })
}
}

impl Write for Zeroed {
/// Writes zero bytes to the buffer.
///
/// This method writes exactly `n` zero bytes to the provided buffer
fn write(&self, buf: &mut impl BufMut) {
buf.put_bytes(0, self.n);
}
}

impl EncodeSize for Zeroed {
/// Returns the encoded size of the zero-padded data.
fn encode_size(&self) -> usize {
self.n
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::Encode;
use bytes::BytesMut;

#[test]
fn test_zeroed() {
let zeroed = Zeroed::new(4);
let encoded = zeroed.encode();
assert_eq!(encoded.len(), 4);
}

#[test]
fn test_read() {
let mut buf = &[0u8, 0u8, 0u8, 0u8][..];
let zeroed = Zeroed::read_cfg(&mut buf, &4).unwrap();
assert_eq!(zeroed.encode_size(), 4);
}

#[test]
fn test_faulty_read_not_zero() {
let mut buf = &[0u8, 0u8, 0u8, 1u8][..];
assert!(matches!(
Zeroed::read_cfg(&mut buf, &4),
Err(Error::Invalid("Zeroed", "bytes are not zero"))
));
}

#[test]
fn test_faulty_read_too_short() {
let mut buf = &[0u8, 0u8, 0u8][..];
assert!(matches!(
Zeroed::read_cfg(&mut buf, &4),
Err(Error::EndOfBuffer)
));
}

#[test]
fn test_write() {
let zeroed = Zeroed::new(4);
let mut buf = BytesMut::new();
zeroed.write(&mut buf);
assert_eq!(buf.len(), 4);
assert!(buf.iter().all(|&b| b == 0));
}

#[test]
fn test_encode_size() {
let zeroed = Zeroed::new(4);
assert_eq!(zeroed.encode_size(), 4);
}
}
26 changes: 6 additions & 20 deletions storage/src/store/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use crate::mmr::Location;
use bytes::{Buf, BufMut};
use commonware_codec::{
util::at_least, varint::UInt, Codec, CodecFixed, EncodeSize, Error as CodecError, FixedSize,
Read, ReadExt, Write,
types::zeroed::Zeroed, util::at_least, varint::UInt, Codec, CodecFixed, EncodeSize,
Error as CodecError, FixedSize, Read, ReadExt, Write,
};
use commonware_utils::{hex, Array};
use std::{
Expand Down Expand Up @@ -245,7 +245,7 @@ impl<K: Array, V: CodecFixed> Write for Fixed<K, V> {
DELETE_CONTEXT.write(buf);
k.write(buf);
// Pad with 0 up to [Self::SIZE]
buf.put_bytes(0, V::SIZE);
Zeroed::new(V::SIZE).write(buf);
}
Fixed::Update(k, v) => {
UPDATE_CONTEXT.write(buf);
Expand All @@ -256,7 +256,7 @@ impl<K: Array, V: CodecFixed> Write for Fixed<K, V> {
COMMIT_FLOOR_CONTEXT.write(buf);
buf.put_slice(&floor_loc.to_be_bytes());
// Pad with 0 up to [Self::SIZE]
buf.put_bytes(0, Self::SIZE - 1 - u64::SIZE);
Zeroed::new(Self::SIZE - 1 - u64::SIZE).write(buf);
}
}
}
Expand Down Expand Up @@ -322,26 +322,12 @@ impl<K: Array, V: CodecFixed> Read for Fixed<K, V> {
DELETE_CONTEXT => {
let key = K::read(buf)?;
// Check that the value is all zeroes
for _ in 0..V::SIZE {
if u8::read(buf)? != 0 {
return Err(CodecError::Invalid(
"storage::adb::operation::Fixed",
"delete value non-zero",
));
}
}
Zeroed::read_cfg(buf, &V::SIZE)?;
Ok(Self::Delete(key))
}
COMMIT_FLOOR_CONTEXT => {
let floor_loc = u64::read(buf)?;
for _ in 0..(Self::SIZE - 1 - u64::SIZE) {
if u8::read(buf)? != 0 {
return Err(CodecError::Invalid(
"storage::adb::operation::Fixed",
"commit value non-zero",
));
}
}
Zeroed::read_cfg(buf, &(Self::SIZE - 1 - u64::SIZE))?;
let floor_loc = Location::new(floor_loc).ok_or_else(|| {
CodecError::Invalid(
"storage::adb::operation::Fixed",
Expand Down