Skip to main content

kopis/
ser.rs

1//! Serialization and deserialization routines for ring elements
2
3/// Fast specialization of `deserialize` for the 13-bit case (matrix expansion), which is
4/// by far the hottest width. Processes a whole 13-byte group into 8 coefficients with
5/// fixed shifts and no per-element branching, so it vectorizes well.
6pub(crate) fn deserialize_13(bytes: &[u8; 13 * 256 / 8]) -> [u16; 256] {
7    let mut out = [0u16; 256];
8    // 256 coeffs = 32 groups of 8, each group packed into 13 bytes.
9    for g in 0..32 {
10        let b = &bytes[13 * g..13 * g + 13];
11        // Widen once so the shifts below can't lose high bits.
12        let w = |k: usize| b[k] as u16;
13        let o = 8 * g;
14        out[o] = w(0) | ((w(1) & 0x1f) << 8);
15        out[o + 1] = (w(1) >> 5) | ((w(2)) << 3) | ((w(3) & 0x03) << 11);
16        out[o + 2] = (w(3) >> 2) | ((w(4) & 0x7f) << 6);
17        out[o + 3] = (w(4) >> 7) | ((w(5)) << 1) | ((w(6) & 0x0f) << 9);
18        out[o + 4] = (w(6) >> 4) | ((w(7)) << 4) | ((w(8) & 0x01) << 12);
19        out[o + 5] = (w(8) >> 1) | ((w(9) & 0x3f) << 7);
20        out[o + 6] = (w(9) >> 6) | ((w(10)) << 2) | ((w(11) & 0x07) << 10);
21        out[o + 7] = (w(11) >> 3) | ((w(12)) << 5);
22    }
23    out
24}
25
26/// Fast specialization of `deserialize_generic` for the 10-bit case
27/// (ciphertext/public-key vector unpacking). Processes a 5-byte group into 4 coefficients
28/// with fixed shifts.
29pub(crate) fn deserialize_10(bytes: &[u8; 10 * 256 / 8]) -> [u16; 256] {
30    let mut out = [0u16; 256];
31    // 256 coeffs = 64 groups of 4, each group packed into 5 bytes.
32    for g in 0..64 {
33        let b = &bytes[5 * g..5 * g + 5];
34        let w = |k: usize| b[k] as u16;
35        let o = 4 * g;
36        out[o] = w(0) | ((w(1) & 0x03) << 8);
37        out[o + 1] = (w(1) >> 2) | ((w(2) & 0x0f) << 6);
38        out[o + 2] = (w(2) >> 4) | ((w(3) & 0x3f) << 4);
39        out[o + 3] = (w(3) >> 6) | (w(4) << 2);
40    }
41    out
42}
43
44/// Deserializes the given bitstring into a u16 array. Every element of the array has
45/// `bits_per_elem` bits (must be ≤ 13), encoded in the lower bits of the word.
46#[allow(clippy::needless_range_loop)]
47pub(crate) fn deserialize_generic<const N: usize>(bytes: &[u8], bits_per_elem: usize) -> [u16; N] {
48    // Serialized bitlength must be a multiple of 8, and `bytes` must be the correct length
49    assert_eq!((bits_per_elem * N) % 8, 0);
50    assert_eq!(bytes.len(), bits_per_elem * N / 8);
51
52    let mut out = [0u16; N];
53    let bitmask: u32 = (1 << bits_per_elem) - 1;
54
55    // Sliding window: holds pending bits from the byte stream. We refill from bytes
56    // one at a time and extract elements from the bottom.
57    let mut window: u32 = 0;
58    let mut bits_in_window: usize = 0;
59    let mut byte_pos: usize = 0;
60
61    for idx in 0..N {
62        // Ensure we have enough bits in the window for one element
63        while bits_in_window < bits_per_elem {
64            window |= (bytes[byte_pos] as u32) << bits_in_window;
65            byte_pos += 1;
66            bits_in_window += 8;
67        }
68
69        // Extract the lowest bits_per_elem bits as one element
70        out[idx] = (window & bitmask) as u16;
71        window >>= bits_per_elem;
72        bits_in_window -= bits_per_elem;
73    }
74
75    out
76}
77
78/// Fast specialization of `serialize` for the 10-bit case (ciphertext/public-key vector
79/// packing), which is by far the hottest serialization width. Packs each 4-coefficient
80/// group into 5 bytes with fixed shifts and no per-element branching.
81pub(crate) fn serialize_10(data: &[u16; 256], out_buf: &mut [u8; 10 * 256 / 8]) {
82    // 256 coeffs = 64 groups of 4, each group packed into 5 bytes.
83    for g in 0..64 {
84        let o = 4 * g;
85        // Mask to 10 bits like the generic path does: coefficients may carry garbage in their
86        // high bits, and serialization is where values get reduced mod 2^10
87        let d = |k: usize| data[o + k] & 0x3ff;
88        let b = 5 * g;
89        out_buf[b] = d(0) as u8;
90        out_buf[b + 1] = ((d(0) >> 8) | (d(1) << 2)) as u8;
91        out_buf[b + 2] = ((d(1) >> 6) | (d(2) << 4)) as u8;
92        out_buf[b + 3] = ((d(2) >> 4) | (d(3) << 6)) as u8;
93        out_buf[b + 4] = (d(3) >> 2) as u8;
94    }
95}
96
97// Algorithm 10, POLN2BS
98/// Serializes the given u16 array into a bitstring. Every element of the array has `bits_per_elem`
99/// bits (must be ≤ 13), encoded in the lower bits of the word.
100pub(crate) fn serialize(data: &[u16], out_buf: &mut [u8], bits_per_elem: usize) {
101    assert_eq!(out_buf.len(), bits_per_elem * data.len() / 8);
102
103    let bitmask: u32 = (1 << bits_per_elem) - 1;
104
105    // Sliding window: elements are OR'd in at the current position, and complete bytes
106    // are flushed out from the bottom.
107    let mut window: u32 = 0;
108    let mut bits_in_window: usize = 0;
109    let mut byte_pos: usize = 0;
110
111    for &elem in data.iter() {
112        // Insert this element's bits into the window
113        window |= ((elem as u32) & bitmask) << bits_in_window;
114        bits_in_window += bits_per_elem;
115
116        // Flush all complete bytes
117        while bits_in_window >= 8 {
118            out_buf[byte_pos] = window as u8;
119            window >>= 8;
120            bits_in_window -= 8;
121            byte_pos += 1;
122        }
123    }
124
125    // There should never be any partial bits in the output
126    debug_assert_eq!(bits_in_window, 0);
127}
128
129// The fast 10- and 13-bit paths must agree with the generic sliding-window deserializer.
130#[test]
131fn specialized_deser_matches_generic() {
132    use rand::Rng;
133    let mut rng = rand::rng();
134
135    // Test deserialize_13
136    for _ in 0..100 {
137        let bytes: [u8; 13 * 256 / 8] = rng.random();
138        let generic: [u16; 256] = deserialize_generic(&bytes, 13);
139        let fast = deserialize_13(&bytes);
140        assert_eq!(generic, fast);
141    }
142
143    // Test deserialize_10
144    for _ in 0..100 {
145        let bytes: [u8; 10 * 256 / 8] = rng.random();
146        let generic: [u16; 256] = deserialize_generic(&bytes, 10);
147        let fast = deserialize_10(&bytes);
148        assert_eq!(generic, fast);
149    }
150}
151
152// The fast 10-bit serialization path must agree with the generic sliding-window serializer.
153#[test]
154fn specialized_ser_matches_generic() {
155    use rand::Rng;
156    let mut rng = rand::rng();
157
158    // Use full-range u16 coefficients so the test also covers the 10-bit masking behavior
159    for _ in 0..100 {
160        let data: [u16; 256] = rng.random();
161        let mut generic = [0u8; 10 * 256 / 8];
162        let mut fast = [0u8; 10 * 256 / 8];
163        serialize(&data, &mut generic, 10);
164        serialize_10(&data, &mut fast);
165        assert_eq!(generic, fast);
166    }
167}