use std::fmt::Display;
use super::Record;
use crate::error::{Result, SclsError};
#[derive(PartialEq, Eq)]
enum Expect {
Header,
ChunkOrManifest,
Eof,
}
impl Display for Expect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Header => "HDR",
Self::ChunkOrManifest => "CHUNK or MANIFEST",
Self::Eof => "end of file",
}
)
}
}
pub(super) struct RecordSequence(Expect);
impl RecordSequence {
pub fn new() -> Self {
Self(Expect::Header)
}
pub fn update(&mut self, state: &Record) -> Result<()> {
let expected = &self.0;
match (expected, state) {
(_, Record::Unknown { .. }) => {}
(Expect::Header, Record::Header(_)) => self.0 = Expect::ChunkOrManifest,
(Expect::Header, record) => {
return Err(SclsError::UnexpectedRecord {
expected: expected.to_string(),
found: record.to_string(),
});
}
(Expect::ChunkOrManifest, Record::Chunk(_)) => {}
(Expect::ChunkOrManifest, Record::Manifest(_)) => self.0 = Expect::Eof,
(Expect::ChunkOrManifest, record) => {
return Err(SclsError::UnexpectedRecord {
expected: expected.to_string(),
found: record.to_string(),
});
}
(Expect::Eof, record) => {
return Err(SclsError::UnexpectedRecord {
expected: expected.to_string(),
found: record.to_string(),
});
}
};
Ok(())
}
pub fn finalise(&self) -> Result<()> {
if self.0 != Expect::Eof {
return Err(SclsError::UnexpectedEof {
expected: self.0.to_string(),
});
}
Ok(())
}
}
impl Default for RecordSequence {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::sync::LazyLock;
use proptest::prelude::*;
use crate::error::SclsError;
use crate::hash::{Digest, HASH_SIZE};
use crate::records::{Chunk, Header, Manifest, Summary};
use super::*;
const DUMMY_DIGEST: Digest = Digest::new([0x00; HASH_SIZE]);
const DUMMY_HEADER: Record = Record::Header(Header::current());
static DUMMY_CHUNK: LazyLock<Record> = LazyLock::new(|| {
let mut payload = Vec::new();
payload.extend_from_slice(&0u64.to_be_bytes());
payload.push(0x00); payload.extend_from_slice(&4u32.to_be_bytes()); payload.extend_from_slice(b"test"); payload.extend_from_slice(&1u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(DUMMY_DIGEST.as_bytes());
let payload_len = payload.len() as u32;
let mut cursor = Cursor::new(payload);
let chunk = Chunk::parse(&mut cursor, 0, payload_len).unwrap();
Record::Chunk(chunk)
});
const DUMMY_MANIFEST: Record = Record::Manifest(Manifest {
slot_no: 0,
total_entries: 0,
total_chunks: 0,
root_hash: DUMMY_DIGEST,
namespace_info: Vec::new(),
prev_manifest: 0,
summary: Summary {
created_at: String::new(),
tool: String::new(),
comment: None,
},
});
fn valid_sequence() -> impl Strategy<Value = Vec<Record>> {
let chunk = &*DUMMY_CHUNK;
prop::collection::vec(Just(chunk.clone()), 0..=5).prop_map(|chunks| {
let mut seq = vec![DUMMY_HEADER];
seq.extend(chunks);
seq.push(DUMMY_MANIFEST);
seq
})
}
fn invalid_sequence() -> impl Strategy<Value = Vec<Record>> {
prop::collection::vec(
prop::sample::select(vec![
DUMMY_HEADER,
DUMMY_CHUNK.clone(),
DUMMY_MANIFEST,
Record::Unknown {
record_type: 0xff,
data: vec![],
},
]),
0..=10,
)
.prop_filter("sequence must be invalid", |seq| {
let mut sequence = RecordSequence::new();
seq.iter().any(|record| sequence.update(record).is_err())
|| sequence.finalise().is_err()
})
}
proptest! {
#[test]
fn valid_record_sequence(records in valid_sequence()) {
let mut sequence = RecordSequence::new();
for record in records {
prop_assert!(sequence.update(&record).is_ok());
}
prop_assert!(sequence.finalise().is_ok());
}
#[test]
fn invalid_record_sequence(records in invalid_sequence()) {
let mut sequence = RecordSequence::new();
let status = records
.iter()
.try_for_each(|record| sequence.update(record));
match status {
Ok(()) => prop_assert!(sequence.finalise().is_err()),
Err(SclsError::UnexpectedRecord { .. }) => {}
Err(e) => prop_assert!(false, "unexpected error: {e}"),
}
}
}
}