Skip to main content

syncular_protocol/
snapshot_chunk.rs

1use crate::{ProtocolError, Result, SnapshotChunkRef, SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1};
2
3pub const SNAPSHOT_CHUNK_COMPRESSION_GZIP: &str = "gzip";
4
5pub fn validate_snapshot_chunk_format(chunk: &SnapshotChunkRef) -> Result<()> {
6    if chunk.compression != SNAPSHOT_CHUNK_COMPRESSION_GZIP {
7        return Err(ProtocolError::message(format!(
8            "unsupported snapshot chunk compression: {}",
9            chunk.compression
10        )));
11    }
12    if chunk.encoding != SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1 {
13        return Err(ProtocolError::message(format!(
14            "unsupported snapshot chunk encoding: {}",
15            chunk.encoding
16        )));
17    }
18    Ok(())
19}
20
21pub fn validate_snapshot_chunk_hash_hex(chunk: &SnapshotChunkRef, actual_hash: &str) -> Result<()> {
22    if actual_hash != chunk.sha256 {
23        return Err(snapshot_chunk_hash_mismatch(chunk, actual_hash));
24    }
25    Ok(())
26}
27
28pub fn validate_snapshot_chunk_hash_bytes(
29    chunk: &SnapshotChunkRef,
30    actual_hash: &[u8],
31) -> Result<()> {
32    let expected_hash = decode_snapshot_chunk_sha256(chunk)?;
33    if actual_hash != expected_hash.as_slice() {
34        return Err(snapshot_chunk_hash_mismatch(
35            chunk,
36            &hex::encode(actual_hash),
37        ));
38    }
39    Ok(())
40}
41
42pub fn decode_snapshot_chunk_sha256(chunk: &SnapshotChunkRef) -> Result<[u8; 32]> {
43    let decoded = hex::decode(&chunk.sha256).map_err(|err| {
44        ProtocolError::message(format!("decode snapshot chunk expected hash: {err}"))
45    })?;
46    decoded.try_into().map_err(|_| {
47        ProtocolError::message(format!(
48            "snapshot chunk expected hash must decode to 32 bytes: {}",
49            chunk.sha256
50        ))
51    })
52}
53
54fn snapshot_chunk_hash_mismatch(chunk: &SnapshotChunkRef, actual_hash: &str) -> ProtocolError {
55    ProtocolError::message(format!(
56        "snapshot chunk hash mismatch: expected {}, got {}",
57        chunk.sha256, actual_hash
58    ))
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use sha2::{Digest, Sha256};
65
66    #[test]
67    fn validates_snapshot_chunk_format_and_hash() {
68        let compressed = b"chunk-body";
69        let chunk = SnapshotChunkRef {
70            id: "chunk-1".to_string(),
71            byte_length: compressed.len() as i64,
72            sha256: hex::encode(Sha256::digest(compressed)),
73            encoding: SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1.to_string(),
74            compression: SNAPSHOT_CHUNK_COMPRESSION_GZIP.to_string(),
75        };
76
77        validate_snapshot_chunk_format(&chunk).expect("format");
78        validate_snapshot_chunk_hash_bytes(&chunk, &Sha256::digest(compressed)).expect("bytes");
79        validate_snapshot_chunk_hash_hex(&chunk, &hex::encode(Sha256::digest(compressed)))
80            .expect("hex");
81    }
82}