Skip to main content

fp_runtime/asupersync/
codec.rs

1use serde::{Deserialize, Serialize};
2
3use crate::asupersync::{config::AsupersyncConfig, error::AsupersyncError};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct ArtifactPayload {
7    pub artifact_id: String,
8    pub bytes: Vec<u8>,
9    pub expected_digest: Option<String>,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct EncodedArtifact {
14    pub artifact_id: String,
15    pub source_len: usize,
16    pub encoded_bytes: Vec<u8>,
17    pub repair_symbols: u32,
18}
19
20pub trait ArtifactCodec {
21    fn encode(
22        &self,
23        payload: &ArtifactPayload,
24        config: &AsupersyncConfig,
25    ) -> Result<EncodedArtifact, AsupersyncError>;
26
27    fn decode(
28        &self,
29        encoded: &EncodedArtifact,
30        config: &AsupersyncConfig,
31    ) -> Result<ArtifactPayload, AsupersyncError>;
32}
33
34#[derive(Debug, Clone, Copy, Default)]
35pub struct PassthroughCodec;
36
37impl ArtifactCodec for PassthroughCodec {
38    fn encode(
39        &self,
40        payload: &ArtifactPayload,
41        config: &AsupersyncConfig,
42    ) -> Result<EncodedArtifact, AsupersyncError> {
43        if config.max_repair_symbols == 0 {
44            return Err(AsupersyncError::Configuration(
45                "max_repair_symbols must be greater than zero",
46            ));
47        }
48
49        Ok(EncodedArtifact {
50            artifact_id: payload.artifact_id.clone(),
51            source_len: payload.bytes.len(),
52            encoded_bytes: payload.bytes.clone(),
53            repair_symbols: config.max_repair_symbols,
54        })
55    }
56
57    fn decode(
58        &self,
59        encoded: &EncodedArtifact,
60        _config: &AsupersyncConfig,
61    ) -> Result<ArtifactPayload, AsupersyncError> {
62        if encoded.repair_symbols == 0 {
63            return Err(AsupersyncError::Codec(
64                "repair_symbols must be greater than zero".to_string(),
65            ));
66        }
67        if encoded.source_len > encoded.encoded_bytes.len() {
68            return Err(AsupersyncError::Codec(
69                "source_len exceeds encoded payload length".to_string(),
70            ));
71        }
72        let bytes = encoded
73            .encoded_bytes
74            .get(..encoded.source_len)
75            .ok_or_else(|| {
76                AsupersyncError::Codec("source_len exceeds encoded payload length".to_string())
77            })?
78            .to_vec();
79
80        Ok(ArtifactPayload {
81            artifact_id: encoded.artifact_id.clone(),
82            bytes,
83            expected_digest: None,
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::{ArtifactCodec, ArtifactPayload, EncodedArtifact, PassthroughCodec};
91    use crate::asupersync::{config::AsupersyncConfig, error::AsupersyncError};
92
93    #[test]
94    fn passthrough_codec_rejects_zero_repair_symbols_on_encode_20jod() {
95        let codec = PassthroughCodec;
96        let config = AsupersyncConfig {
97            max_repair_symbols: 0,
98            ..AsupersyncConfig::default()
99        };
100        let payload = ArtifactPayload {
101            artifact_id: "artifact-20jod".to_string(),
102            bytes: b"payload".to_vec(),
103            expected_digest: None,
104        };
105
106        let err = codec.encode(&payload, &config).err();
107        assert!(matches!(err, Some(AsupersyncError::Configuration(_))));
108    }
109
110    #[test]
111    fn passthrough_codec_rejects_zero_repair_symbols_on_decode_20jod() {
112        let codec = PassthroughCodec;
113        let config = AsupersyncConfig::default();
114        let encoded = EncodedArtifact {
115            artifact_id: "artifact-20jod".to_string(),
116            source_len: 7,
117            encoded_bytes: b"payload".to_vec(),
118            repair_symbols: 0,
119        };
120
121        let err = codec.decode(&encoded, &config).err();
122        assert!(
123            matches!(err, Some(AsupersyncError::Codec(message)) if message.contains("repair_symbols"))
124        );
125    }
126
127    #[test]
128    fn passthrough_codec_round_trip_preserves_payload_with_manifest_20jod()
129    -> Result<(), AsupersyncError> {
130        let codec = PassthroughCodec;
131        let config = AsupersyncConfig {
132            max_repair_symbols: 4,
133            ..AsupersyncConfig::default()
134        };
135        let payload = ArtifactPayload {
136            artifact_id: "artifact-20jod".to_string(),
137            bytes: b"payload".to_vec(),
138            expected_digest: Some("digest".to_string()),
139        };
140
141        let encoded = codec.encode(&payload, &config)?;
142        assert_eq!(encoded.repair_symbols, 4);
143
144        let decoded = codec.decode(&encoded, &config)?;
145        assert_eq!(decoded.artifact_id, payload.artifact_id);
146        assert_eq!(decoded.bytes, payload.bytes);
147        assert_eq!(decoded.expected_digest, None);
148        Ok(())
149    }
150}