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        SimpleRng(seed.wrapping_add(1))
74    }
75    /// Next pseudorandom byte.
76    pub fn next_u8(&mut self) -> u8 {
77        self.0 ^= self.0 << 13;
78        self.0 ^= self.0 >> 7;
79        self.0 ^= self.0 << 17;
80        self.0 as u8
81    }
82    /// Fill `buf` with pseudorandom bytes.
83    pub fn fill(&mut self, buf: &mut [u8]) {
84        for b in buf.iter_mut() {
85            *b = self.next_u8();
86        }
87    }
88}
89
90/// RLNC Encoder.
91#[cfg(feature = "alloc")]
92pub struct Encoder {
93    generation_size: usize,
94    symbol_size: usize,
95}
96
97#[cfg(feature = "alloc")]
98impl Encoder {
99    /// Create a new encoder.
100    ///
101    /// - `generation_size` (`k`): number of source symbols per generation.
102    /// - `symbol_size` (`n`): bytes per source symbol.
103    pub fn new(generation_size: usize, symbol_size: usize) -> Result<Self, RlncError> {
104        if generation_size == 0 || symbol_size == 0 {
105            return Err(RlncError::InvalidParameters);
106        }
107        Ok(Encoder {
108            generation_size,
109            symbol_size,
110        })
111    }
112
113    /// Generation size (`k`).
114    pub fn generation_size(&self) -> usize {
115        self.generation_size
116    }
117    /// Symbol size in bytes (`n`).
118    pub fn symbol_size(&self) -> usize {
119        self.symbol_size
120    }
121
122    /// Encode one random-linear coded packet.
123    ///
124    /// Uses cache-blocked multi-source AXPY ([`kernel::axpy_multi`]) for
125    /// better DRAM behaviour when `k` is moderate and `symbol_size` is large.
126    ///
127    /// `source` must contain exactly `generation_size` slices, each of
128    /// length `symbol_size`.  `rng` provides the random coefficients.
129    pub fn encode_random(
130        &self,
131        source: &[&[u8]],
132        rng: &mut SimpleRng,
133    ) -> Result<CodedPacket, RlncError> {
134        self.validate_source(source)?;
135
136        let k = self.generation_size;
137        let n = self.symbol_size;
138
139        let mut coeffs = AlignedBuffer::zeroed(k);
140        let mut payload = AlignedBuffer::zeroed(n);
141
142        rng.fill(coeffs.as_mut_slice());
143
144        // Avoid all-zero coefficient vector (bounded retries — M4)
145        let mut retries = 0u32;
146        while coeffs.as_slice().iter().all(|&c| c == 0) {
147            retries += 1;
148            if retries >= 100 {
149                coeffs.as_mut_slice()[0] = 1;
150                break;
151            }
152            rng.fill(coeffs.as_mut_slice());
153        }
154
155        kernel::axpy_multi(coeffs.as_slice(), source, payload.as_mut_slice());
156
157        Ok(CodedPacket {
158            coefficients: coeffs,
159            payload,
160        })
161    }
162
163    /// Produce a **systematic** coded packet for source symbol `index`.
164    pub fn encode_systematic(
165        &self,
166        source: &[&[u8]],
167        index: usize,
168    ) -> Result<CodedPacket, RlncError> {
169        self.validate_source(source)?;
170        if index >= self.generation_size {
171            return Err(RlncError::IndexOutOfRange {
172                index,
173                max: self.generation_size,
174            });
175        }
176
177        let mut coeffs = AlignedBuffer::zeroed(self.generation_size);
178        coeffs.as_mut_slice()[index] = 1;
179        let payload = AlignedBuffer::from_slice(source[index]);
180
181        Ok(CodedPacket {
182            coefficients: coeffs,
183            payload,
184        })
185    }
186
187    fn validate_source(&self, source: &[&[u8]]) -> Result<(), RlncError> {
188        if source.len() != self.generation_size {
189            return Err(RlncError::SourceCountMismatch {
190                expected: self.generation_size,
191                got: source.len(),
192            });
193        }
194        for s in source {
195            if s.len() != self.symbol_size {
196                return Err(RlncError::SourceSizeMismatch {
197                    expected: self.symbol_size,
198                    got: s.len(),
199                });
200            }
201        }
202        Ok(())
203    }
204}
205
206#[cfg(test)]
207#[cfg(feature = "alloc")]
208mod tests {
209    use super::*;
210    use crate::aligned::ALIGN;
211
212    #[test]
213    fn systematic_packet_is_copy() {
214        let k = 4usize;
215        let n = 8usize;
216        let symbols: Vec<Vec<u8>> = (0..k as u8).map(|i| vec![i * 10; n]).collect();
217        let refs: Vec<&[u8]> = symbols.iter().map(Vec::as_slice).collect();
218
219        let enc = Encoder::new(k, n).unwrap();
220        for (i, symbol) in symbols.iter().enumerate() {
221            let pkt = enc.encode_systematic(&refs, i).unwrap();
222            assert_eq!(pkt.payload.as_slice(), symbol.as_slice());
223            assert_eq!(pkt.coefficients.as_slice()[i], 1);
224            assert!(pkt
225                .coefficients
226                .as_slice()
227                .iter()
228                .enumerate()
229                .all(|(j, &c)| if j == i { c == 1 } else { c == 0 }));
230        }
231    }
232
233    #[test]
234    fn random_encode_non_zero_payload() {
235        let k = 4usize;
236        let n = 16usize;
237        let symbols: Vec<Vec<u8>> = (1u8..=k as u8).map(|i| vec![i; n]).collect();
238        let refs: Vec<&[u8]> = symbols.iter().map(Vec::as_slice).collect();
239
240        let enc = Encoder::new(k, n).unwrap();
241        let mut rng = SimpleRng::new(42);
242        let pkt = enc.encode_random(&refs, &mut rng).unwrap();
243        assert_eq!(pkt.payload.len(), n);
244        assert_eq!(pkt.coefficients.len(), k);
245    }
246
247    #[test]
248    fn coded_packet_payload_is_aligned() {
249        let k = 4usize;
250        let n = 128usize;
251        let symbols: Vec<Vec<u8>> = (0..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(0xBEEF);
256        let pkt = enc.encode_random(&refs, &mut rng).unwrap();
257        assert_eq!(
258            pkt.payload.as_ptr() as usize % ALIGN,
259            0,
260            "CodedPacket.payload must be {ALIGN}-byte aligned"
261        );
262        assert_eq!(
263            pkt.coefficients.as_ptr() as usize % ALIGN,
264            0,
265            "CodedPacket.coefficients must be {ALIGN}-byte aligned"
266        );
267    }
268
269    #[test]
270    fn coded_packet_into_from_vecs_roundtrip() {
271        let pkt = CodedPacket::from_slices(&[1, 2, 3, 4], &[9, 8, 7, 6]);
272        let (c, p) = pkt.into_vecs();
273        assert_eq!(c, vec![1, 2, 3, 4]);
274        assert_eq!(p, vec![9, 8, 7, 6]);
275        let pkt2 = CodedPacket::from_vecs(c, p);
276        assert_eq!(pkt2.coefficients_slice(), &[1, 2, 3, 4]);
277        assert_eq!(pkt2.payload_slice(), &[9, 8, 7, 6]);
278    }
279
280    #[test]
281    fn new_rejects_zero_params() {
282        assert!(Encoder::new(0, 16).is_err());
283        assert!(Encoder::new(4, 0).is_err());
284        assert!(matches!(
285            Encoder::new(0, 0),
286            Err(crate::error::RlncError::InvalidParameters)
287        ));
288    }
289
290    #[test]
291    fn encode_rejects_source_count_mismatch() {
292        let enc = Encoder::new(2, 4).unwrap();
293        let s0 = [1u8; 4];
294        let mut rng = SimpleRng::new(1);
295        let err = enc.encode_random(&[&s0], &mut rng).unwrap_err();
296        match err {
297            crate::error::RlncError::SourceCountMismatch {
298                expected: 2,
299                got: 1,
300            } => {}
301            other => panic!("unexpected {other:?}"),
302        }
303    }
304
305    #[test]
306    fn encode_rejects_source_size_mismatch() {
307        let enc = Encoder::new(2, 4).unwrap();
308        let s0 = [1u8; 4];
309        let s1 = [2u8; 3];
310        let mut rng = SimpleRng::new(1);
311        let err = enc.encode_random(&[&s0, &s1], &mut rng).unwrap_err();
312        match err {
313            crate::error::RlncError::SourceSizeMismatch {
314                expected: 4,
315                got: 3,
316            } => {}
317            other => panic!("unexpected {other:?}"),
318        }
319    }
320
321    #[test]
322    fn systematic_index_out_of_range() {
323        let enc = Encoder::new(2, 4).unwrap();
324        let s0 = [1u8; 4];
325        let s1 = [2u8; 4];
326        let err = enc.encode_systematic(&[&s0, &s1], 2).unwrap_err();
327        match err {
328            crate::error::RlncError::IndexOutOfRange { index: 2, max: 2 } => {}
329            other => panic!("unexpected {other:?}"),
330        }
331    }
332}