Skip to main content

rustfs_erasure_codec/core/
leopard.rs

1extern crate alloc;
2
3use alloc::vec::Vec;
4
5use crate::Field;
6use crate::errors::Error;
7use crate::matrix::Matrix;
8
9use super::{CodecFamily, LeopardMode};
10
11/// Maximum total shards (`data + parity`) addressable by each Leopard family.
12///
13/// GF(2^8) Leopard is bounded by the byte field order; GF(2^16) Leopard uses
14/// 16-bit arithmetic and reaches 65536. This is the single source of truth for
15/// the family-aware cap enforced in the constructors via
16/// [`max_total_shards_for_family`] (returning [`Error::TooManyShards`]); the
17/// per-family validators check only field/endian preconditions, not the cap.
18pub(crate) const LEOPARD_GF8_MAX_SHARDS: usize = 256;
19pub(crate) const LEOPARD_GF16_MAX_SHARDS: usize = 65536;
20
21/// Parity ceiling at or below which [`LeopardMode::PreferLeopard`] resolves to
22/// GF(2^8) Leopard. Above it a GF(2^8) Leopard codec can be constructed but not
23/// encoded, so resolution falls back to GF(2^16).
24const LEOPARD_GF8_MAX_PARITY: usize = 128;
25
26/// Whether `F` is a byte-oriented field, i.e. `F::Elem` is exactly one byte.
27///
28/// This is the soundness precondition for every Leopard codec: they reinterpret
29/// `F::Elem` slices as raw `u8` bytes (see [`AsLeopardU8`]). Gating on the
30/// element size — rather than `F::ORDER == 256` — is the precise condition and
31/// also rejects any hypothetical field whose order is 256 but whose element has
32/// a wider representation.
33pub(crate) fn is_byte_field<F: Field>() -> bool {
34    core::mem::size_of::<F::Elem>() == 1
35}
36
37/// Trait for safely reinterpreting `F::Elem` as `u8` for leopard encode.
38///
39/// Only implemented for `u8` (i.e., `galois_8::Field`). This enables the generic
40/// `encode_sep` to call the `u8`-specific leopard FFT engine without `unsafe`.
41pub(crate) trait AsLeopardU8: Sized {
42    fn slice_to_u8(slice: &[Self]) -> &[u8];
43    fn slice_to_u8_mut(slice: &mut [Self]) -> &mut [u8];
44}
45
46impl AsLeopardU8 for u8 {
47    #[inline]
48    fn slice_to_u8(slice: &[u8]) -> &[u8] {
49        slice
50    }
51    #[inline]
52    fn slice_to_u8_mut(slice: &mut [u8]) -> &mut [u8] {
53        slice
54    }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub(crate) enum FamilyState<F: Field> {
59    Classic,
60    LeopardGF8(LeopardGF8Codec<F>),
61    LeopardGF16,
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub(crate) struct LeopardGF8Codec<F: Field> {
66    data_shards: usize,
67    parity_shards: usize,
68    total_shards: usize,
69    setup_rows: usize,
70    setup_cols: usize,
71    parity_rows: Vec<Vec<F::Elem>>,
72    _marker: core::marker::PhantomData<F>,
73}
74
75impl<F: Field> LeopardGF8Codec<F> {
76    pub(crate) fn new(
77        data_shards: usize,
78        parity_shards: usize,
79        setup_matrix: Matrix<F>,
80    ) -> Result<Self, Error> {
81        validate_leopard_gf8::<F>(data_shards, parity_shards)?;
82
83        Ok(Self {
84            data_shards,
85            parity_shards,
86            total_shards: data_shards.saturating_add(parity_shards),
87            setup_rows: setup_matrix.row_count(),
88            setup_cols: setup_matrix.col_count(),
89            parity_rows: (data_shards..(data_shards + parity_shards))
90                .map(|row| setup_matrix.get_row(row).to_vec())
91                .collect(),
92            _marker: core::marker::PhantomData,
93        })
94    }
95
96    pub(crate) fn data_shards(&self) -> usize {
97        self.data_shards
98    }
99
100    pub(crate) fn parity_shards(&self) -> usize {
101        self.parity_shards
102    }
103
104    pub(crate) fn total_shards(&self) -> usize {
105        self.total_shards
106    }
107
108    pub(crate) fn setup_shape(&self) -> (usize, usize) {
109        (self.setup_rows, self.setup_cols)
110    }
111
112    pub(crate) fn parity_rows(&self) -> Vec<&[F::Elem]> {
113        self.parity_rows.iter().map(|row| row.as_slice()).collect()
114    }
115}
116
117pub(crate) fn leopard_gf8_state<F: Field>(
118    family_state: &FamilyState<F>,
119) -> Result<&LeopardGF8Codec<F>, Error> {
120    match family_state {
121        FamilyState::LeopardGF8(codec) => Ok(codec),
122        FamilyState::Classic => Err(Error::UnsupportedCodecFamily),
123        FamilyState::LeopardGF16 => Err(Error::UnsupportedLeopardPrototype),
124    }
125}
126
127pub(crate) fn validate_leopard_shard_len(shard_len: usize) -> Result<(), Error> {
128    if shard_len == 0 || !shard_len.is_multiple_of(LEOPARD_SHARD_MULTIPLE) {
129        return Err(Error::IncorrectShardSize);
130    }
131
132    Ok(())
133}
134
135/// Required byte multiple (and cache-line alignment) of every Leopard shard.
136///
137/// Leopard shards must be a non-zero multiple of this value; see
138/// [`validate_leopard_shard_len`]. Equal to
139/// [`SHARD_ALIGNMENT`](crate::galois_8::SHARD_ALIGNMENT).
140pub const LEOPARD_SHARD_MULTIPLE: usize = 64;
141
142/// Computes a per-shard length, in bytes, that Leopard will accept for a payload
143/// of `data_len` bytes spread across `data_shards` data shards.
144///
145/// The result is **always a non-zero multiple of [`LEOPARD_SHARD_MULTIPLE`]**, so
146/// it is guaranteed to pass [`validate_leopard_shard_len`], for every input:
147///
148/// * `data_len == 0` (or any payload smaller than one block) clamps up to
149///   [`LEOPARD_SHARD_MULTIPLE`].
150/// * `data_shards == 0` is treated as a single shard (no divide-by-zero).
151/// * Payloads near [`usize::MAX`] saturate to the largest multiple of
152///   [`LEOPARD_SHARD_MULTIPLE`] that fits in a `usize`, rather than overflowing.
153pub fn leopard_aligned_shard_len(data_len: usize, data_shards: usize) -> usize {
154    // A zero shard count is nonsensical; treat the whole payload as one shard
155    // instead of dividing by zero.
156    let shards = if data_shards == 0 { 1 } else { data_shards };
157
158    // Bytes per shard, rounding the payload up so every byte has a home.
159    // `div_ceil` cannot overflow and `shards >= 1`, so this is total.
160    let per_shard = data_len.div_ceil(shards);
161
162    // Round up to the next multiple of LEOPARD_SHARD_MULTIPLE. `per_shard +
163    // (MULTIPLE - remainder)` can overflow near usize::MAX, so saturate.
164    let remainder = per_shard % LEOPARD_SHARD_MULTIPLE;
165    let rounded = if remainder == 0 {
166        per_shard
167    } else {
168        per_shard.saturating_add(LEOPARD_SHARD_MULTIPLE - remainder)
169    };
170
171    // Guarantee a non-zero result (covers data_len == 0), then floor back onto a
172    // 64-boundary in case the saturation above landed on usize::MAX (which is
173    // not itself a multiple of 64). For all normal inputs this is a no-op.
174    let clamped = rounded.max(LEOPARD_SHARD_MULTIPLE);
175    clamped - (clamped % LEOPARD_SHARD_MULTIPLE)
176}
177
178// Colocated with `leopard_aligned_shard_len`; the dispatch helpers below are
179// unrelated, so allow the "items after test module" style lint here.
180#[cfg(test)]
181#[allow(clippy::items_after_test_module)]
182mod leopard_shard_len_tests {
183    use super::{LEOPARD_SHARD_MULTIPLE, leopard_aligned_shard_len, validate_leopard_shard_len};
184
185    #[test]
186    fn zero_data_len_clamps_to_multiple() {
187        let len = leopard_aligned_shard_len(0, 4);
188        assert_eq!(len, LEOPARD_SHARD_MULTIPLE);
189        assert!(validate_leopard_shard_len(len).is_ok());
190    }
191
192    #[test]
193    fn non_multiple_payload_rounds_up() {
194        // ceil(100 / 4) = 25 bytes/shard -> rounds up to 64.
195        let len = leopard_aligned_shard_len(100, 4);
196        assert_eq!(len, 64);
197        // 65 bytes/shard -> rounds up to 128.
198        let len2 = leopard_aligned_shard_len(65 * 4, 4);
199        assert_eq!(len2, 128);
200        assert!(len.is_multiple_of(LEOPARD_SHARD_MULTIPLE));
201        assert!(validate_leopard_shard_len(len).is_ok());
202        assert!(validate_leopard_shard_len(len2).is_ok());
203    }
204
205    #[test]
206    fn zero_shards_treated_as_one() {
207        // ceil(200 / 1) = 200 -> rounds up to 256.
208        let len = leopard_aligned_shard_len(200, 0);
209        assert_eq!(len, 256);
210        assert!(validate_leopard_shard_len(len).is_ok());
211    }
212
213    #[test]
214    fn near_usize_max_saturates_to_valid_multiple() {
215        let len = leopard_aligned_shard_len(usize::MAX, 1);
216        assert_ne!(len, 0);
217        assert!(len.is_multiple_of(LEOPARD_SHARD_MULTIPLE));
218        assert!(validate_leopard_shard_len(len).is_ok());
219        // Largest 64-multiple representable in a usize.
220        assert_eq!(len, usize::MAX - (usize::MAX % LEOPARD_SHARD_MULTIPLE));
221    }
222}
223
224pub(crate) fn build_family_state<F: Field>(
225    codec_family: CodecFamily,
226    data_shards: usize,
227    parity_shards: usize,
228    setup_matrix: &Matrix<F>,
229) -> Result<FamilyState<F>, Error> {
230    match codec_family {
231        CodecFamily::Classic => Ok(FamilyState::Classic),
232        CodecFamily::LeopardGF8 => Ok(FamilyState::LeopardGF8(LeopardGF8Codec::new(
233            data_shards,
234            parity_shards,
235            {
236                let mut matrix = Matrix::new(setup_matrix.row_count(), setup_matrix.col_count());
237                for row in 0..setup_matrix.row_count() {
238                    for col in 0..setup_matrix.col_count() {
239                        matrix.set(row, col, setup_matrix.get(row, col));
240                    }
241                }
242                matrix
243            },
244        )?)),
245        CodecFamily::LeopardGF16 => Ok(FamilyState::LeopardGF16),
246    }
247}
248
249pub(crate) fn validate_leopard_family<F: Field>(
250    codec_family: CodecFamily,
251    data_shards: usize,
252    parity_shards: usize,
253) -> Result<(), Error> {
254    match codec_family {
255        CodecFamily::Classic => Ok(()),
256        CodecFamily::LeopardGF8 => validate_leopard_gf8::<F>(data_shards, parity_shards),
257        CodecFamily::LeopardGF16 => validate_leopard_gf16::<F>(data_shards, parity_shards),
258    }
259}
260
261/// Maximum representable `data + parity` shard count for a given codec family.
262///
263/// The generic `total > F::ORDER` guard in `with_options` is only correct for
264/// [`CodecFamily::Classic`]: the Leopard codecs run on the GF(2^8) field
265/// (`F::ORDER == 256`) but [`CodecFamily::LeopardGF16`] internally uses GF(2^16)
266/// arithmetic and supports up to 65536 shards. Using this family-aware cap is
267/// what makes an explicit (or auto-selected) `LeopardGF16` codec with more than
268/// 256 total shards constructible at all.
269pub(crate) fn max_total_shards_for_family<F: Field>(codec_family: CodecFamily) -> usize {
270    match codec_family {
271        CodecFamily::Classic => F::ORDER,
272        CodecFamily::LeopardGF8 => LEOPARD_GF8_MAX_SHARDS,
273        CodecFamily::LeopardGF16 => LEOPARD_GF16_MAX_SHARDS,
274    }
275}
276
277/// Resolve the effective [`CodecFamily`] for an optional [`LeopardMode`].
278///
279/// Returns the family the codec should actually build. Auto-selection is only
280/// eligible when the caller left `codec_family` at [`CodecFamily::Classic`] on a
281/// byte-oriented field (`is_byte_field`, i.e. the GF(2^8) field the Leopard
282/// codecs require); otherwise the requested family is returned unchanged. Once
283/// eligible, `mode` alone decides — [`LeopardMode::Disabled`] maps back to
284/// `Classic`, so the default is preserved byte-for-byte.
285///
286/// The mapping over `total = data + parity` mirrors klauspost/reedsolomon:
287///
288/// - [`Disabled`](LeopardMode::Disabled): always Classic.
289/// - [`AsNeeded`](LeopardMode::AsNeeded): Classic while it fits the byte field,
290///   else GF16.
291/// - [`PreferGF16`](LeopardMode::PreferGF16): always GF16.
292/// - [`PreferLeopard`](LeopardMode::PreferLeopard): GF8 when it fits the byte
293///   field and `parity ≤ LEOPARD_GF8_MAX_PARITY`, else GF16.
294pub(crate) fn resolve_codec_family(
295    requested: CodecFamily,
296    mode: LeopardMode,
297    is_byte_field: bool,
298    total_shards: usize,
299    parity_shards: usize,
300) -> CodecFamily {
301    // Only an untouched Classic request on a byte-oriented field is eligible for
302    // auto-selection; everything else passes through unchanged. `mode` is then
303    // the single source of truth for the resolved family.
304    if requested != CodecFamily::Classic || !is_byte_field {
305        return requested;
306    }
307
308    match mode {
309        LeopardMode::Disabled => CodecFamily::Classic,
310        LeopardMode::AsNeeded => {
311            if total_shards <= LEOPARD_GF8_MAX_SHARDS {
312                CodecFamily::Classic
313            } else {
314                CodecFamily::LeopardGF16
315            }
316        }
317        LeopardMode::PreferGF16 => CodecFamily::LeopardGF16,
318        LeopardMode::PreferLeopard => {
319            if total_shards <= LEOPARD_GF8_MAX_SHARDS && parity_shards <= LEOPARD_GF8_MAX_PARITY {
320                CodecFamily::LeopardGF8
321            } else {
322                CodecFamily::LeopardGF16
323            }
324        }
325    }
326}
327
328// These validators check only family/field *preconditions*. The total-shard cap
329// is owned by `max_total_shards_for_family` and enforced in the constructors
330// (returning `Error::TooManyShards`) before any validator runs, so re-checking
331// `total > MAX` here would be unreachable and would return a different error for
332// the same failure — the cap lives in exactly one place.
333fn validate_leopard_gf8<F: Field>(_data_shards: usize, _parity_shards: usize) -> Result<(), Error> {
334    // Soundness gate: Leopard reinterprets `F::Elem` as raw bytes.
335    if !is_byte_field::<F>() {
336        return Err(Error::UnsupportedCodecFamily);
337    }
338    Ok(())
339}
340
341fn validate_leopard_gf16<F: Field>(
342    _data_shards: usize,
343    _parity_shards: usize,
344) -> Result<(), Error> {
345    // Soundness gate: Leopard reinterprets `F::Elem` as raw bytes.
346    if !is_byte_field::<F>() {
347        return Err(Error::UnsupportedCodecFamily);
348    }
349
350    // The GF16 Leopard codec handles bytes as little-endian `u16` split-layout
351    // pairs. Big-endian correctness is not yet verified end to end, so reject at
352    // construction rather than risk silently producing wrong results
353    // (rustfs/backlog#1238). Little-endian builds are unaffected; GF8 Leopard is
354    // byte-oriented and endian-agnostic, so it is not gated here.
355    if cfg!(target_endian = "big") {
356        return Err(Error::UnsupportedCodecFamily);
357    }
358
359    Ok(())
360}
361
362/// Dispatch encode to the Leopard GF8 FFT engine.
363///
364/// Accepts `u8` slices directly. The caller (`encode_leopard_gf8_sep`) is responsible
365/// for converting from `F::Elem` to `u8` (safe because Leopard GF8 is only
366/// instantiated for `galois_8::Field` where `Elem = u8`).
367pub(crate) fn leopard_gf8_encode(
368    data_shards: usize,
369    parity_shards: usize,
370    data: &[&[u8]],
371    parity: &mut [&mut [u8]],
372) -> Result<(), Error> {
373    super::leopard_gf8::encode_with_tables(data_shards, parity_shards, data, parity)?;
374    Ok(())
375}
376
377/// Dispatch encode to the Leopard GF16 FFT engine.
378pub(crate) fn leopard_gf16_encode(
379    data_shards: usize,
380    parity_shards: usize,
381    data: &[&[u8]],
382    parity: &mut [&mut [u8]],
383) -> Result<(), Error> {
384    super::leopard_gf16::encode::encode_with_tables16(data_shards, parity_shards, data, parity)?;
385    Ok(())
386}
387
388/// Dispatch reconstruct to the Leopard GF16 Forney decoder.
389pub(crate) fn leopard_gf16_reconstruct(
390    present: &[bool],
391    outputs: &mut [&mut [u8]],
392    input_data: &[Option<&[u8]>],
393    data_shards: usize,
394    parity_shards: usize,
395) -> Result<(), Error> {
396    let tables = super::leopard_gf16::init_leopard_gf16_tables();
397    super::leopard_gf16::decode::reconstruct_with_tables16(
398        present,
399        outputs,
400        input_data,
401        data_shards,
402        parity_shards,
403        tables,
404    )
405}
406
407#[cfg(test)]
408#[allow(clippy::items_after_test_module)]
409mod auto_activation_tests {
410    use super::{CodecFamily, LeopardMode, max_total_shards_for_family, resolve_codec_family};
411    use crate::{galois_8, galois_16};
412
413    // Resolution helper for the common case: an untouched `Classic` request on a
414    // byte-oriented field, which is where auto-selection actually applies.
415    fn resolve(mode: LeopardMode, total: usize, parity: usize) -> CodecFamily {
416        resolve_codec_family(CodecFamily::Classic, mode, true, total, parity)
417    }
418
419    #[test]
420    fn disabled_is_always_classic() {
421        for &(total, parity) in &[(2usize, 1usize), (256, 1), (257, 1), (65536, 4)] {
422            assert_eq!(
423                resolve(LeopardMode::Disabled, total, parity),
424                CodecFamily::Classic
425            );
426        }
427    }
428
429    #[test]
430    fn as_needed_switches_to_gf16_past_256() {
431        assert_eq!(resolve(LeopardMode::AsNeeded, 2, 1), CodecFamily::Classic);
432        assert_eq!(resolve(LeopardMode::AsNeeded, 256, 1), CodecFamily::Classic);
433        assert_eq!(
434            resolve(LeopardMode::AsNeeded, 257, 1),
435            CodecFamily::LeopardGF16
436        );
437        assert_eq!(
438            resolve(LeopardMode::AsNeeded, 65536, 4),
439            CodecFamily::LeopardGF16
440        );
441    }
442
443    #[test]
444    fn prefer_gf16_is_always_gf16() {
445        assert_eq!(
446            resolve(LeopardMode::PreferGF16, 2, 1),
447            CodecFamily::LeopardGF16
448        );
449        assert_eq!(
450            resolve(LeopardMode::PreferGF16, 256, 1),
451            CodecFamily::LeopardGF16
452        );
453        assert_eq!(
454            resolve(LeopardMode::PreferGF16, 65536, 4),
455            CodecFamily::LeopardGF16
456        );
457    }
458
459    #[test]
460    fn prefer_leopard_uses_gf8_only_when_small_and_low_parity() {
461        // total <= 256 and parity <= 128 -> GF8
462        assert_eq!(
463            resolve(LeopardMode::PreferLeopard, 256, 128),
464            CodecFamily::LeopardGF8
465        );
466        assert_eq!(
467            resolve(LeopardMode::PreferLeopard, 6, 2),
468            CodecFamily::LeopardGF8
469        );
470        // parity > 128 -> GF16 (D5: avoid configs that construct as GF8 but can't encode)
471        assert_eq!(
472            resolve(LeopardMode::PreferLeopard, 256, 129),
473            CodecFamily::LeopardGF16
474        );
475        // total > 256 -> GF16
476        assert_eq!(
477            resolve(LeopardMode::PreferLeopard, 257, 1),
478            CodecFamily::LeopardGF16
479        );
480    }
481
482    #[test]
483    fn explicit_family_is_never_rewritten() {
484        for mode in [
485            LeopardMode::AsNeeded,
486            LeopardMode::PreferGF16,
487            LeopardMode::PreferLeopard,
488        ] {
489            assert_eq!(
490                resolve_codec_family(CodecFamily::LeopardGF8, mode, true, 10, 2),
491                CodecFamily::LeopardGF8
492            );
493            assert_eq!(
494                resolve_codec_family(CodecFamily::LeopardGF16, mode, true, 10, 2),
495                CodecFamily::LeopardGF16
496            );
497        }
498    }
499
500    #[test]
501    fn non_byte_field_ignores_mode() {
502        // A non-byte-oriented field (e.g. GF(2^16) element = 2 bytes) never
503        // auto-activates Leopard; the request stays Classic.
504        assert_eq!(
505            resolve_codec_family(CodecFamily::Classic, LeopardMode::PreferGF16, false, 300, 4),
506            CodecFamily::Classic
507        );
508    }
509
510    #[test]
511    fn family_aware_caps() {
512        // Classic caps at the field order; Leopard caps by family regardless of F::ORDER.
513        assert_eq!(
514            max_total_shards_for_family::<galois_8::Field>(CodecFamily::Classic),
515            256
516        );
517        assert_eq!(
518            max_total_shards_for_family::<galois_16::Field>(CodecFamily::Classic),
519            65536
520        );
521        // Leopard runs on the GF(2^8) field (F::ORDER = 256) but GF16 allows 65536.
522        assert_eq!(
523            max_total_shards_for_family::<galois_8::Field>(CodecFamily::LeopardGF8),
524            256
525        );
526        assert_eq!(
527            max_total_shards_for_family::<galois_8::Field>(CodecFamily::LeopardGF16),
528            65536
529        );
530    }
531}