Skip to main content

rlnc_simdx/
encoder.rs

1//! RLNC Encoder — produces random linear coded packets over GF(2⁸).
2//!
3//! `CodedPacket.payload` is backed by an [`AlignedBuffer`] so that all
4//! intermediate re-encoding and decoding operations on the payload always
5//! pass 64-byte aligned pointers to the SIMD kernels.
6
7#[cfg(feature = "alloc")]
8extern crate alloc;
9#[cfg(feature = "alloc")]
10use alloc::vec::Vec;
11
12use crate::aligned::AlignedBuffer;
13use crate::error::RlncError;
14use crate::kernel;
15
16/// A single coded packet: GF(2⁸) coefficient vector + 64-byte-aligned payload.
17#[cfg(feature = "alloc")]
18#[derive(Debug, Clone)]
19pub struct CodedPacket {
20    /// Length == `generation_size`.  The i-th byte is the coefficient
21    /// applied to source symbol i.
22    pub coefficients: AlignedBuffer,
23    /// Length == `symbol_size`, 64-byte aligned.
24    pub payload: AlignedBuffer,
25}
26
27#[cfg(feature = "alloc")]
28impl CodedPacket {
29    /// Coefficient vector as a byte slice.
30    #[inline]
31    pub fn coefficients_slice(&self) -> &[u8] {
32        self.coefficients.as_slice()
33    }
34
35    /// Payload as a byte slice.
36    #[inline]
37    pub fn payload_slice(&self) -> &[u8] {
38        self.payload.as_slice()
39    }
40
41    /// Copy coefficients and payload into owned `Vec`s (for FFI / unaligned consumers).
42    pub fn into_vecs(self) -> (Vec<u8>, Vec<u8>) {
43        (self.coefficients.into_vec(), self.payload.into_vec())
44    }
45
46    /// Build a packet from coefficient and payload slices (copies into aligned buffers).
47    pub fn from_slices(coefficients: &[u8], payload: &[u8]) -> Self {
48        Self {
49            coefficients: AlignedBuffer::from_slice(coefficients),
50            payload: AlignedBuffer::from_slice(payload),
51        }
52    }
53
54    /// Build a packet from owned `Vec`s (copies into aligned buffers).
55    pub fn from_vecs(coefficients: Vec<u8>, payload: Vec<u8>) -> Self {
56        Self {
57            coefficients: AlignedBuffer::from_slice(&coefficients),
58            payload: AlignedBuffer::from_slice(&payload),
59        }
60    }
61}
62
63/// Simple LFSR-based PRNG for coding coefficients — **not a CSPRNG**.
64///
65/// Use only for RLNC coefficient generation in non-adversarial settings.
66/// For adversarial environments, inject entropy from a cryptographic RNG
67/// outside this crate. See the crate-level security warning.
68pub struct SimpleRng(u64);
69
70impl SimpleRng {
71    /// Create a new RNG from `seed`.
72    pub fn new(seed: u64) -> Self {
73        let state = seed.wrapping_add(1);
74        SimpleRng(if state == 0 {
75            0x9E37_79B9_7F4A_7C15
76        } else {
77            state
78        })
79    }
80    /// Next pseudorandom byte.
81    pub fn next_u8(&mut self) -> u8 {
82        self.0 ^= self.0 << 13;
83        self.0 ^= self.0 >> 7;
84        self.0 ^= self.0 << 17;
85        self.0 as u8
86    }
87    /// Fill `buf` with pseudorandom bytes.
88    pub fn fill(&mut self, buf: &mut [u8]) {
89        for b in buf.iter_mut() {
90            *b = self.next_u8();
91        }
92    }
93}
94
95/// RLNC Encoder.
96#[cfg(feature = "alloc")]
97pub struct Encoder {
98    generation_size: usize,
99    symbol_size: usize,
100}
101
102#[cfg(feature = "alloc")]
103impl Encoder {
104    /// Create a new encoder.
105    ///
106    /// - `generation_size` (`k`): number of source symbols per generation.
107    /// - `symbol_size` (`n`): bytes per source symbol.
108    pub fn new(generation_size: usize, symbol_size: usize) -> Result<Self, RlncError> {
109        if generation_size == 0 || symbol_size == 0 {
110            return Err(RlncError::InvalidParameters);
111        }
112        Ok(Encoder {
113            generation_size,
114            symbol_size,
115        })
116    }
117
118    /// Generation size (`k`).
119    pub fn generation_size(&self) -> usize {
120        self.generation_size
121    }
122    /// Symbol size in bytes (`n`).
123    pub fn symbol_size(&self) -> usize {
124        self.symbol_size
125    }
126
127    /// Encode one random-linear coded packet.
128    ///
129    /// Uses adaptive multi-source AXPY ([`kernel::axpy_multi`]): cache-blocked
130    /// kernels for small symbols and fused GFNI kernels for large symbols.
131    ///
132    /// `source` must contain exactly `generation_size` slices, each of
133    /// length `symbol_size`.  `rng` provides the random coefficients.
134    pub fn encode_random(
135        &self,
136        source: &[&[u8]],
137        rng: &mut SimpleRng,
138    ) -> Result<CodedPacket, RlncError> {
139        self.validate_source(source)?;
140
141        let k = self.generation_size;
142        let n = self.symbol_size;
143
144        let mut coeffs = AlignedBuffer::zeroed(k);
145        let mut payload = AlignedBuffer::zeroed(n);
146
147        rng.fill(coeffs.as_mut_slice());
148
149        // Avoid all-zero coefficient vector (bounded retries — M4)
150        let mut retries = 0u32;
151        while coeffs.as_slice().iter().all(|&c| c == 0) {
152            retries += 1;
153            if retries >= 100 {
154                coeffs.as_mut_slice()[0] = 1;
155                break;
156            }
157            rng.fill(coeffs.as_mut_slice());
158        }
159
160        kernel::axpy_multi(coeffs.as_slice(), source, payload.as_mut_slice());
161
162        Ok(CodedPacket {
163            coefficients: coeffs,
164            payload,
165        })
166    }
167
168    /// Produce a **systematic** coded packet for source symbol `index`.
169    pub fn encode_systematic(
170        &self,
171        source: &[&[u8]],
172        index: usize,
173    ) -> Result<CodedPacket, RlncError> {
174        self.validate_source(source)?;
175        if index >= self.generation_size {
176            return Err(RlncError::IndexOutOfRange {
177                index,
178                max: self.generation_size,
179            });
180        }
181
182        let mut coeffs = AlignedBuffer::zeroed(self.generation_size);
183        coeffs.as_mut_slice()[index] = 1;
184        let payload = AlignedBuffer::from_slice(source[index]);
185
186        Ok(CodedPacket {
187            coefficients: coeffs,
188            payload,
189        })
190    }
191
192    fn validate_source(&self, source: &[&[u8]]) -> Result<(), RlncError> {
193        if source.len() != self.generation_size {
194            return Err(RlncError::SourceCountMismatch {
195                expected: self.generation_size,
196                got: source.len(),
197            });
198        }
199        for s in source {
200            if s.len() != self.symbol_size {
201                return Err(RlncError::SourceSizeMismatch {
202                    expected: self.symbol_size,
203                    got: s.len(),
204                });
205            }
206        }
207        Ok(())
208    }
209}
210
211#[cfg(test)]
212#[cfg(feature = "alloc")]
213mod tests {
214    use super::*;
215    use crate::aligned::ALIGN;
216
217    #[test]
218    fn systematic_packet_is_copy() {
219        let k = 4usize;
220        let n = 8usize;
221        let symbols: Vec<Vec<u8>> = (0..k as u8).map(|i| vec![i * 10; n]).collect();
222        let refs: Vec<&[u8]> = symbols.iter().map(Vec::as_slice).collect();
223
224        let enc = Encoder::new(k, n).unwrap();
225        for (i, symbol) in symbols.iter().enumerate() {
226            let pkt = enc.encode_systematic(&refs, i).unwrap();
227            assert_eq!(pkt.payload.as_slice(), symbol.as_slice());
228            assert_eq!(pkt.coefficients.as_slice()[i], 1);
229            assert!(pkt
230                .coefficients
231                .as_slice()
232                .iter()
233                .enumerate()
234                .all(|(j, &c)| if j == i { c == 1 } else { c == 0 }));
235        }
236    }
237
238    #[test]
239    fn max_seed_does_not_enter_zero_state() {
240        let mut rng = SimpleRng::new(u64::MAX);
241        assert_ne!(rng.next_u8(), 0);
242        let mut bytes = [0u8; 16];
243        rng.fill(&mut bytes);
244        assert!(bytes.iter().any(|&byte| byte != 0));
245    }
246
247    #[test]
248    fn random_encode_non_zero_payload() {
249        let k = 4usize;
250        let n = 16usize;
251        let symbols: Vec<Vec<u8>> = (1u8..=k as u8).map(|i| vec![i; n]).collect();
252        let refs: Vec<&[u8]> = symbols.iter().map(Vec::as_slice).collect();
253
254        let enc = Encoder::new(k, n).unwrap();
255        let mut rng = SimpleRng::new(42);
256        let pkt = enc.encode_random(&refs, &mut rng).unwrap();
257        assert_eq!(pkt.payload.len(), n);
258        assert_eq!(pkt.coefficients.len(), k);
259    }
260
261    #[test]
262    fn coded_packet_payload_is_aligned() {
263        let k = 4usize;
264        let n = 128usize;
265        let symbols: Vec<Vec<u8>> = (0..k as u8).map(|i| vec![i; n]).collect();
266        let refs: Vec<&[u8]> = symbols.iter().map(Vec::as_slice).collect();
267
268        let enc = Encoder::new(k, n).unwrap();
269        let mut rng = SimpleRng::new(0xBEEF);
270        let pkt = enc.encode_random(&refs, &mut rng).unwrap();
271        assert_eq!(
272            pkt.payload.as_ptr() as usize % ALIGN,
273            0,
274            "CodedPacket.payload must be {ALIGN}-byte aligned"
275        );
276        assert_eq!(
277            pkt.coefficients.as_ptr() as usize % ALIGN,
278            0,
279            "CodedPacket.coefficients must be {ALIGN}-byte aligned"
280        );
281    }
282
283    #[test]
284    fn coded_packet_into_from_vecs_roundtrip() {
285        let pkt = CodedPacket::from_slices(&[1, 2, 3, 4], &[9, 8, 7, 6]);
286        let (c, p) = pkt.into_vecs();
287        assert_eq!(c, vec![1, 2, 3, 4]);
288        assert_eq!(p, vec![9, 8, 7, 6]);
289        let pkt2 = CodedPacket::from_vecs(c, p);
290        assert_eq!(pkt2.coefficients_slice(), &[1, 2, 3, 4]);
291        assert_eq!(pkt2.payload_slice(), &[9, 8, 7, 6]);
292    }
293
294    #[test]
295    fn new_rejects_zero_params() {
296        assert!(Encoder::new(0, 16).is_err());
297        assert!(Encoder::new(4, 0).is_err());
298        assert!(matches!(
299            Encoder::new(0, 0),
300            Err(crate::error::RlncError::InvalidParameters)
301        ));
302    }
303
304    #[test]
305    fn encode_rejects_source_count_mismatch() {
306        let enc = Encoder::new(2, 4).unwrap();
307        let s0 = [1u8; 4];
308        let mut rng = SimpleRng::new(1);
309        let err = enc.encode_random(&[&s0], &mut rng).unwrap_err();
310        match err {
311            crate::error::RlncError::SourceCountMismatch {
312                expected: 2,
313                got: 1,
314            } => {}
315            other => panic!("unexpected {other:?}"),
316        }
317    }
318
319    #[test]
320    fn encode_rejects_source_size_mismatch() {
321        let enc = Encoder::new(2, 4).unwrap();
322        let s0 = [1u8; 4];
323        let s1 = [2u8; 3];
324        let mut rng = SimpleRng::new(1);
325        let err = enc.encode_random(&[&s0, &s1], &mut rng).unwrap_err();
326        match err {
327            crate::error::RlncError::SourceSizeMismatch {
328                expected: 4,
329                got: 3,
330            } => {}
331            other => panic!("unexpected {other:?}"),
332        }
333    }
334
335    #[test]
336    fn systematic_index_out_of_range() {
337        let enc = Encoder::new(2, 4).unwrap();
338        let s0 = [1u8; 4];
339        let s1 = [2u8; 4];
340        let err = enc.encode_systematic(&[&s0, &s1], 2).unwrap_err();
341        match err {
342            crate::error::RlncError::IndexOutOfRange { index: 2, max: 2 } => {}
343            other => panic!("unexpected {other:?}"),
344        }
345    }
346}