Skip to main content

rustfs_erasure_codec/core/
codec.rs

1extern crate alloc;
2
3use alloc::vec;
4use alloc::vec::Vec;
5
6use hashlink::LruCache;
7#[cfg(feature = "std")]
8use parking_lot::Mutex;
9use smallvec::SmallVec;
10#[cfg(not(feature = "std"))]
11use spin::Mutex;
12
13use crate::Field;
14use crate::errors::Error;
15use crate::matrix::Matrix;
16
17use super::{
18    CodecFamily, CodecOptions, DATA_DECODE_MATRIX_CACHE_MAX_CAPACITY,
19    DATA_DECODE_MATRIX_CACHE_MIN_CAPACITY, MatrixMode, ReedSolomon,
20};
21#[cfg(feature = "std")]
22use super::{
23    ReconstructionCacheMetrics, ReconstructionCacheStats, RuntimeProfileMetrics,
24    RuntimeProfileStats,
25};
26
27impl<F: Field> ReedSolomon<F> {
28    pub(crate) fn normalize_inversion_cache_capacity(
29        data_shards: usize,
30        parity_shards: usize,
31        requested_capacity: usize,
32    ) -> usize {
33        if requested_capacity > 0 {
34            return requested_capacity;
35        }
36
37        Self::recommended_inversion_cache_capacity(data_shards, parity_shards)
38    }
39
40    pub(crate) fn derive_inversion_cache_capacity(
41        data_shards: usize,
42        parity_shards: usize,
43    ) -> usize {
44        let total_shards = data_shards.saturating_add(parity_shards);
45        let heuristic = total_shards
46            .saturating_mul(parity_shards.max(1))
47            .saturating_mul(2);
48        let rounded = heuristic
49            .checked_next_power_of_two()
50            .unwrap_or(DATA_DECODE_MATRIX_CACHE_MAX_CAPACITY);
51
52        rounded.clamp(
53            DATA_DECODE_MATRIX_CACHE_MIN_CAPACITY,
54            DATA_DECODE_MATRIX_CACHE_MAX_CAPACITY,
55        )
56    }
57
58    pub(crate) fn get_parity_rows(&self) -> SmallVec<[&[F::Elem]; 32]> {
59        let mut parity_rows = SmallVec::with_capacity(self.parity_shard_count);
60        let matrix = &self.matrix;
61        for i in self.data_shard_count..self.total_shard_count {
62            parity_rows.push(matrix.get_row(i));
63        }
64
65        parity_rows
66    }
67
68    pub(crate) fn build_matrix(
69        data_shards: usize,
70        total_shards: usize,
71    ) -> Result<Matrix<F>, Error> {
72        let vandermonde = Matrix::vandermonde(total_shards, data_shards);
73
74        let top = vandermonde.sub_matrix(0, 0, data_shards, data_shards);
75        let top_inverted = top.invert().map_err(|_| Error::InvalidCustomMatrix)?;
76
77        vandermonde
78            .multiply(&top_inverted)
79            .map_err(|_| Error::InvalidCustomMatrix)
80    }
81
82    pub(crate) fn build_cauchy_matrix(
83        data_shards: usize,
84        total_shards: usize,
85    ) -> Result<Matrix<F>, Error> {
86        let mut result = Matrix::new(total_shards, data_shards);
87
88        for r in 0..total_shards {
89            if r < data_shards {
90                result.set(r, r, F::one());
91            } else {
92                for c in 0..data_shards {
93                    let denominator = F::add(F::nth(r), F::nth(c));
94                    if denominator == F::zero() {
95                        return Err(Error::InvalidCustomMatrix);
96                    }
97                    result.set(r, c, F::div(F::one(), denominator));
98                }
99            }
100        }
101
102        Ok(result)
103    }
104
105    pub(crate) fn build_jerasure_like_matrix(
106        data_shards: usize,
107        total_shards: usize,
108    ) -> Result<Matrix<F>, Error> {
109        let mut vm = Matrix::vandermonde(total_shards, data_shards);
110
111        vm.set(0, 0, F::one());
112        for i in 1..data_shards {
113            vm.set(0, i, F::zero());
114        }
115
116        for i in 0..data_shards.saturating_sub(1) {
117            vm.set(total_shards - 1, i, F::zero());
118        }
119        vm.set(total_shards - 1, data_shards - 1, F::one());
120
121        for i in 0..data_shards {
122            let mut r = i;
123            while r < total_shards && vm.get(r, i) == F::zero() {
124                r += 1;
125            }
126            if r != i {
127                vm.swap_rows(r, i);
128            } else if vm.get(i, i) == F::zero() {
129                return Err(Error::InvalidCustomMatrix);
130            }
131
132            let scale = match vm.get(i, i) {
133                diagonal if diagonal == F::zero() => {
134                    return Err(Error::InvalidCustomMatrix);
135                }
136                diagonal if diagonal != F::one() => F::div(F::one(), diagonal),
137                _ => F::one(),
138            };
139            if scale != F::one() {
140                for row in 0..total_shards {
141                    vm.set(row, i, F::mul(vm.get(row, i), scale));
142                }
143            }
144
145            for j in 0..data_shards {
146                let value = vm.get(i, j);
147                if j != i && value != F::zero() {
148                    for row in 0..total_shards {
149                        vm.set(
150                            row,
151                            j,
152                            F::add(vm.get(row, j), F::mul(value, vm.get(row, i))),
153                        );
154                    }
155                }
156            }
157        }
158
159        for j in 0..data_shards {
160            let value = vm.get(data_shards, j);
161            if value == F::zero() {
162                return Err(Error::InvalidCustomMatrix);
163            }
164
165            if value != F::one() {
166                let scale = F::div(F::one(), value);
167                for row in data_shards..total_shards {
168                    vm.set(row, j, F::mul(vm.get(row, j), scale));
169                }
170            }
171        }
172
173        for row in (data_shards + 1)..total_shards {
174            let value = vm.get(row, 0);
175            if value == F::zero() {
176                return Err(Error::InvalidCustomMatrix);
177            }
178
179            if value != F::one() {
180                let scale = F::div(F::one(), value);
181                for col in 0..data_shards {
182                    vm.set(row, col, F::mul(vm.get(row, col), scale));
183                }
184            }
185        }
186
187        Ok(vm)
188    }
189
190    pub(crate) fn build_custom_matrix(
191        data_shards: usize,
192        total_shards: usize,
193        custom_matrix: &[Vec<F::Elem>],
194    ) -> Result<Matrix<F>, Error> {
195        let parity_shards = total_shards.saturating_sub(data_shards);
196        if custom_matrix.len() < parity_shards {
197            return Err(Error::InvalidCustomMatrix);
198        }
199        if custom_matrix
200            .iter()
201            .take(parity_shards)
202            .any(|row| row.len() < data_shards)
203        {
204            return Err(Error::InvalidCustomMatrix);
205        }
206
207        let mut result = Matrix::new(total_shards, data_shards);
208        for row in 0..data_shards {
209            result.set(row, row, F::one());
210        }
211        for (offset, row) in custom_matrix.iter().take(parity_shards).enumerate() {
212            for (col, value) in row.iter().take(data_shards).enumerate() {
213                result.set(data_shards + offset, col, *value);
214            }
215        }
216
217        Ok(result)
218    }
219
220    pub(crate) fn build_matrix_with_options(
221        data_shards: usize,
222        total_shards: usize,
223        options: CodecOptions,
224    ) -> Result<Matrix<F>, Error> {
225        if options.codec_family != CodecFamily::Classic {
226            return Err(Error::UnsupportedCodecFamily);
227        }
228
229        match options.matrix_mode {
230            MatrixMode::Vandermonde => Self::build_matrix(data_shards, total_shards),
231            MatrixMode::Cauchy => Self::build_cauchy_matrix(data_shards, total_shards),
232            MatrixMode::JerasureLike => Self::build_jerasure_like_matrix(data_shards, total_shards),
233            MatrixMode::Custom => Err(Error::InvalidCustomMatrix),
234        }
235    }
236
237    /// Create a new codec with default options.
238    ///
239    /// Returns [`Error::TooFewDataShards`] or [`Error::TooFewParityShards`] if
240    /// either count is zero, or [`Error::TooManyShards`] if the total exceeds the field order.
241    pub fn new(data_shards: usize, parity_shards: usize) -> Result<ReedSolomon<F>, Error> {
242        Self::with_options(data_shards, parity_shards, CodecOptions::default())
243    }
244
245    /// Create a new codec with explicit [`CodecOptions`].
246    pub fn with_options(
247        data_shards: usize,
248        parity_shards: usize,
249        mut options: CodecOptions,
250    ) -> Result<ReedSolomon<F>, Error> {
251        if data_shards == 0 {
252            return Err(Error::TooFewDataShards);
253        }
254        if parity_shards == 0 {
255            return Err(Error::TooFewParityShards);
256        }
257        let total_shards = data_shards
258            .checked_add(parity_shards)
259            .ok_or(Error::TooManyShards)?;
260
261        // Resolve optional Leopard auto-selection before any family-dependent
262        // check. With `LeopardMode::Disabled` (the default) or an explicit
263        // non-Classic family this is a no-op, so prior behaviour is preserved
264        // byte-for-byte.
265        options.codec_family = super::leopard::resolve_codec_family(
266            options.codec_family,
267            options.leopard_mode,
268            super::leopard::is_byte_field::<F>(),
269            total_shards,
270            parity_shards,
271        );
272
273        // Family-aware shard cap: Classic is bounded by the field order, while
274        // LeopardGF16 supports up to 65536 shards even though it runs on the
275        // GF(2^8) field. A plain `total > F::ORDER` here would make an (explicit
276        // or auto-selected) GF16 codec with more than 256 shards unconstructible.
277        if total_shards > super::leopard::max_total_shards_for_family::<F>(options.codec_family) {
278            return Err(Error::TooManyShards);
279        }
280
281        super::leopard::validate_leopard_family::<F>(
282            options.codec_family,
283            data_shards,
284            parity_shards,
285        )?;
286
287        options.inversion_cache_capacity = Self::normalize_inversion_cache_capacity(
288            data_shards,
289            parity_shards,
290            options.inversion_cache_capacity,
291        );
292
293        let matrix = match options.codec_family {
294            CodecFamily::Classic => {
295                Self::build_matrix_with_options(data_shards, total_shards, options)?
296            }
297            CodecFamily::LeopardGF8 => Self::build_matrix(data_shards, total_shards)?,
298            // LeopardGF16 encodes and reconstructs entirely with GF(2^16) FFT
299            // tables and never consults this GF(2^8) matrix (see
300            // `encode_leopard_sep_inner` / `leopard_gf16_reconstruct`). Building a
301            // Vandermonde here would call `Field::nth` past the 256-element
302            // GF(2^8) range for total > 256, and would needlessly allocate a
303            // total×data matrix (up to ~65536 rows). Use an empty placeholder.
304            CodecFamily::LeopardGF16 => Matrix::new(0, 0),
305        };
306        let family_state = super::leopard::build_family_state(
307            options.codec_family,
308            data_shards,
309            parity_shards,
310            &matrix,
311        )?;
312        #[cfg(feature = "std")]
313        let policy_cache = Self::resolve_policy_cache_with_options(options);
314
315        Ok(ReedSolomon {
316            data_shard_count: data_shards,
317            parity_shard_count: parity_shards,
318            total_shard_count: total_shards,
319            codec_family: options.codec_family,
320            family_state,
321            matrix,
322            options,
323            #[cfg(feature = "std")]
324            policy_cache,
325            data_decode_matrix_cache: Mutex::new(LruCache::new(options.inversion_cache_capacity)),
326            #[cfg(feature = "std")]
327            reconstruction_cache_metrics: ReconstructionCacheMetrics::default(),
328            #[cfg(feature = "std")]
329            runtime_profile_metrics: RuntimeProfileMetrics::default(),
330        })
331    }
332
333    /// Returns the number of data shards.
334    pub fn data_shard_count(&self) -> usize {
335        self.data_shard_count
336    }
337
338    /// Returns the number of parity shards.
339    pub fn parity_shard_count(&self) -> usize {
340        self.parity_shard_count
341    }
342
343    /// Returns the total number of shards (data + parity).
344    pub fn total_shard_count(&self) -> usize {
345        self.total_shard_count
346    }
347
348    /// Returns the codec family (Classic, LeopardGF8, or LeopardGF16).
349    pub fn codec_family(&self) -> CodecFamily {
350        self.codec_family
351    }
352
353    /// Returns the Leopard GF8 setup matrix shape `(rows, cols)`, or `None` if not using LeopardGF8.
354    pub fn leopard_setup_matrix_shape(&self) -> Option<(usize, usize)> {
355        let codec = super::leopard::leopard_gf8_state(&self.family_state).ok()?;
356        Some(codec.setup_shape())
357    }
358
359    /// Returns the inversion cache capacity.
360    pub fn inversion_cache_capacity(&self) -> usize {
361        self.options.inversion_cache_capacity
362    }
363
364    /// Returns the recommended inversion cache capacity for the given shard counts.
365    pub fn recommended_inversion_cache_capacity(data_shards: usize, parity_shards: usize) -> usize {
366        Self::derive_inversion_cache_capacity(data_shards, parity_shards)
367    }
368
369    /// Returns a snapshot of reconstruction cache hit/miss statistics.
370    #[cfg(feature = "std")]
371    pub fn reconstruction_cache_stats(&self) -> ReconstructionCacheStats {
372        self.reconstruction_cache_metrics.snapshot()
373    }
374
375    /// Returns a snapshot of runtime profiling metrics.
376    #[cfg(feature = "std")]
377    pub fn runtime_profile_stats(&self) -> RuntimeProfileStats {
378        self.runtime_profile_metrics.snapshot()
379    }
380
381    /// Resets all runtime profiling counters to zero.
382    #[cfg(feature = "std")]
383    pub fn reset_runtime_profile_stats(&self) {
384        self.runtime_profile_metrics.reset();
385    }
386
387    #[cfg(feature = "std")]
388    pub(crate) fn record_reconstruct_entry_path(&self, parallel: bool) {
389        self.runtime_profile_metrics
390            .record_reconstruct_entry(parallel);
391    }
392
393    #[cfg(feature = "std")]
394    pub(crate) fn record_reconstruct_opt_fallback_serial_path(&self) {
395        self.runtime_profile_metrics
396            .record_reconstruct_opt_fallback_serial();
397    }
398
399    #[cfg(feature = "std")]
400    pub(crate) fn record_reconstruct_runtime(
401        &self,
402        data_only: bool,
403        missing_data_count: usize,
404        missing_parity_count: usize,
405        all_present: bool,
406    ) {
407        self.runtime_profile_metrics.record_reconstruct(
408            data_only,
409            missing_data_count,
410            missing_parity_count,
411            all_present,
412        );
413    }
414
415    #[cfg(feature = "std")]
416    pub(crate) fn record_reconstruct_data_stage_runtime(
417        &self,
418        shard_len: usize,
419        output_count: usize,
420    ) {
421        self.runtime_profile_metrics
422            .record_reconstruct_data_stage(shard_len, output_count);
423    }
424
425    #[cfg(feature = "std")]
426    pub(crate) fn record_reconstruct_parity_stage_runtime(
427        &self,
428        shard_len: usize,
429        output_count: usize,
430    ) {
431        self.runtime_profile_metrics
432            .record_reconstruct_parity_stage(shard_len, output_count);
433    }
434
435    /// Split a contiguous data buffer into `data_shard_count` equal-length shards.
436    ///
437    /// The last shard is zero-padded if `data.len()` is not evenly divisible.
438    pub fn split(&self, data: &[F::Elem]) -> Result<Vec<Vec<F::Elem>>, Error> {
439        let data_shards = self.data_shard_count;
440        let shard_len = if data.is_empty() {
441            0
442        } else {
443            data.len().div_ceil(data_shards)
444        };
445
446        let mut shards = Vec::with_capacity(data_shards);
447        for i in 0..data_shards {
448            let start = i * shard_len;
449            let end = core::cmp::min(start + shard_len, data.len());
450            let mut shard = vec![F::zero(); shard_len];
451            if start < data.len() {
452                shard[..end - start].copy_from_slice(&data[start..end]);
453            }
454            shards.push(shard);
455        }
456
457        Ok(shards)
458    }
459
460    /// Join data shards back into a single contiguous buffer.
461    ///
462    /// Truncates to `out_len` bytes. Requires exactly `data_shard_count` shards.
463    pub fn join<T: AsRef<[F::Elem]>>(
464        &self,
465        shards: &[T],
466        out_len: usize,
467    ) -> Result<Vec<F::Elem>, Error> {
468        check_piece_count!(data => self, shards);
469        check_slices!(multi => shards);
470
471        let available = shards
472            .iter()
473            .map(|shard| shard.as_ref().len())
474            .sum::<usize>();
475        let target_len = core::cmp::min(out_len, available);
476        let mut result = Vec::with_capacity(target_len);
477
478        for shard in shards {
479            let remaining = target_len.saturating_sub(result.len());
480            if remaining == 0 {
481                break;
482            }
483
484            let data = shard.as_ref();
485            let to_take = core::cmp::min(remaining, data.len());
486            result.extend_from_slice(&data[..to_take]);
487        }
488
489        result.truncate(target_len);
490        Ok(result)
491    }
492
493    /// Create a codec with a user-provided encoding matrix.
494    pub fn with_custom_matrix(
495        data_shards: usize,
496        parity_shards: usize,
497        custom_matrix: &[Vec<F::Elem>],
498        mut options: CodecOptions,
499    ) -> Result<ReedSolomon<F>, Error> {
500        if data_shards == 0 {
501            return Err(Error::TooFewDataShards);
502        }
503        if parity_shards == 0 {
504            return Err(Error::TooFewParityShards);
505        }
506        let total_shards = data_shards
507            .checked_add(parity_shards)
508            .ok_or(Error::TooManyShards)?;
509        if total_shards > F::ORDER {
510            return Err(Error::TooManyShards);
511        }
512
513        // A custom matrix is only meaningful for the Classic family — the Leopard
514        // families encode with FFT, not a user matrix. Reject any non-Classic
515        // family up front; this subsumes the Leopard-family precondition check,
516        // since no Leopard codec can be built through this entry point.
517        if options.codec_family != CodecFamily::Classic {
518            return Err(Error::UnsupportedCodecFamily);
519        }
520
521        options.matrix_mode = MatrixMode::Custom;
522        options.inversion_cache_capacity = Self::normalize_inversion_cache_capacity(
523            data_shards,
524            parity_shards,
525            options.inversion_cache_capacity,
526        );
527
528        let matrix = Self::build_custom_matrix(data_shards, total_shards, custom_matrix)?;
529        let family_state = super::leopard::build_family_state(
530            options.codec_family,
531            data_shards,
532            parity_shards,
533            &matrix,
534        )?;
535        #[cfg(feature = "std")]
536        let policy_cache = Self::resolve_policy_cache_with_options(options);
537
538        Ok(ReedSolomon {
539            data_shard_count: data_shards,
540            parity_shard_count: parity_shards,
541            total_shard_count: total_shards,
542            codec_family: options.codec_family,
543            family_state,
544            matrix,
545            options,
546            #[cfg(feature = "std")]
547            policy_cache,
548            data_decode_matrix_cache: Mutex::new(LruCache::new(options.inversion_cache_capacity)),
549            #[cfg(feature = "std")]
550            reconstruction_cache_metrics: ReconstructionCacheMetrics::default(),
551            #[cfg(feature = "std")]
552            runtime_profile_metrics: RuntimeProfileMetrics::default(),
553        })
554    }
555}