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        if data_shards + parity_shards > F::ORDER {
258            return Err(Error::TooManyShards);
259        }
260
261        super::leopard::validate_leopard_family::<F>(
262            options.codec_family,
263            data_shards,
264            parity_shards,
265        )?;
266
267        let total_shards = data_shards + parity_shards;
268
269        options.inversion_cache_capacity = Self::normalize_inversion_cache_capacity(
270            data_shards,
271            parity_shards,
272            options.inversion_cache_capacity,
273        );
274
275        let matrix = match options.codec_family {
276            CodecFamily::Classic => {
277                Self::build_matrix_with_options(data_shards, total_shards, options)?
278            }
279            CodecFamily::LeopardGF8 | CodecFamily::LeopardGF16 => {
280                Self::build_matrix(data_shards, total_shards)?
281            }
282        };
283        let family_state = super::leopard::build_family_state(
284            options.codec_family,
285            data_shards,
286            parity_shards,
287            &matrix,
288        )?;
289        #[cfg(feature = "std")]
290        let policy_cache = Self::resolve_policy_cache_with_options(options);
291
292        Ok(ReedSolomon {
293            data_shard_count: data_shards,
294            parity_shard_count: parity_shards,
295            total_shard_count: total_shards,
296            codec_family: options.codec_family,
297            family_state,
298            matrix,
299            options,
300            #[cfg(feature = "std")]
301            policy_cache,
302            data_decode_matrix_cache: Mutex::new(LruCache::new(options.inversion_cache_capacity)),
303            #[cfg(feature = "std")]
304            reconstruction_cache_metrics: ReconstructionCacheMetrics::default(),
305            #[cfg(feature = "std")]
306            runtime_profile_metrics: RuntimeProfileMetrics::default(),
307        })
308    }
309
310    /// Returns the number of data shards.
311    pub fn data_shard_count(&self) -> usize {
312        self.data_shard_count
313    }
314
315    /// Returns the number of parity shards.
316    pub fn parity_shard_count(&self) -> usize {
317        self.parity_shard_count
318    }
319
320    /// Returns the total number of shards (data + parity).
321    pub fn total_shard_count(&self) -> usize {
322        self.total_shard_count
323    }
324
325    /// Returns the codec family (Classic, LeopardGF8, or LeopardGF16).
326    pub fn codec_family(&self) -> CodecFamily {
327        self.codec_family
328    }
329
330    /// Returns the Leopard GF8 setup matrix shape `(rows, cols)`, or `None` if not using LeopardGF8.
331    pub fn leopard_setup_matrix_shape(&self) -> Option<(usize, usize)> {
332        let codec = super::leopard::leopard_gf8_state(&self.family_state).ok()?;
333        Some(codec.setup_shape())
334    }
335
336    /// Returns the inversion cache capacity.
337    pub fn inversion_cache_capacity(&self) -> usize {
338        self.options.inversion_cache_capacity
339    }
340
341    /// Returns the recommended inversion cache capacity for the given shard counts.
342    pub fn recommended_inversion_cache_capacity(data_shards: usize, parity_shards: usize) -> usize {
343        Self::derive_inversion_cache_capacity(data_shards, parity_shards)
344    }
345
346    /// Returns a snapshot of reconstruction cache hit/miss statistics.
347    #[cfg(feature = "std")]
348    pub fn reconstruction_cache_stats(&self) -> ReconstructionCacheStats {
349        self.reconstruction_cache_metrics.snapshot()
350    }
351
352    /// Returns a snapshot of runtime profiling metrics.
353    #[cfg(feature = "std")]
354    pub fn runtime_profile_stats(&self) -> RuntimeProfileStats {
355        self.runtime_profile_metrics.snapshot()
356    }
357
358    /// Resets all runtime profiling counters to zero.
359    #[cfg(feature = "std")]
360    pub fn reset_runtime_profile_stats(&self) {
361        self.runtime_profile_metrics.reset();
362    }
363
364    #[cfg(feature = "std")]
365    pub(crate) fn record_reconstruct_entry_path(&self, parallel: bool) {
366        self.runtime_profile_metrics
367            .record_reconstruct_entry(parallel);
368    }
369
370    #[cfg(feature = "std")]
371    pub(crate) fn record_reconstruct_opt_fallback_serial_path(&self) {
372        self.runtime_profile_metrics
373            .record_reconstruct_opt_fallback_serial();
374    }
375
376    #[cfg(feature = "std")]
377    pub(crate) fn record_reconstruct_runtime(
378        &self,
379        data_only: bool,
380        missing_data_count: usize,
381        missing_parity_count: usize,
382        all_present: bool,
383    ) {
384        self.runtime_profile_metrics.record_reconstruct(
385            data_only,
386            missing_data_count,
387            missing_parity_count,
388            all_present,
389        );
390    }
391
392    #[cfg(feature = "std")]
393    pub(crate) fn record_reconstruct_data_stage_runtime(
394        &self,
395        shard_len: usize,
396        output_count: usize,
397    ) {
398        self.runtime_profile_metrics
399            .record_reconstruct_data_stage(shard_len, output_count);
400    }
401
402    #[cfg(feature = "std")]
403    pub(crate) fn record_reconstruct_parity_stage_runtime(
404        &self,
405        shard_len: usize,
406        output_count: usize,
407    ) {
408        self.runtime_profile_metrics
409            .record_reconstruct_parity_stage(shard_len, output_count);
410    }
411
412    /// Split a contiguous data buffer into `data_shard_count` equal-length shards.
413    ///
414    /// The last shard is zero-padded if `data.len()` is not evenly divisible.
415    pub fn split(&self, data: &[F::Elem]) -> Result<Vec<Vec<F::Elem>>, Error> {
416        let data_shards = self.data_shard_count;
417        let shard_len = if data.is_empty() {
418            0
419        } else {
420            data.len().div_ceil(data_shards)
421        };
422
423        let mut shards = Vec::with_capacity(data_shards);
424        for i in 0..data_shards {
425            let start = i * shard_len;
426            let end = core::cmp::min(start + shard_len, data.len());
427            let mut shard = vec![F::zero(); shard_len];
428            if start < data.len() {
429                shard[..end - start].copy_from_slice(&data[start..end]);
430            }
431            shards.push(shard);
432        }
433
434        Ok(shards)
435    }
436
437    /// Join data shards back into a single contiguous buffer.
438    ///
439    /// Truncates to `out_len` bytes. Requires exactly `data_shard_count` shards.
440    pub fn join<T: AsRef<[F::Elem]>>(
441        &self,
442        shards: &[T],
443        out_len: usize,
444    ) -> Result<Vec<F::Elem>, Error> {
445        check_piece_count!(data => self, shards);
446        check_slices!(multi => shards);
447
448        let available = shards
449            .iter()
450            .map(|shard| shard.as_ref().len())
451            .sum::<usize>();
452        let target_len = core::cmp::min(out_len, available);
453        let mut result = Vec::with_capacity(target_len);
454
455        for shard in shards {
456            let remaining = target_len.saturating_sub(result.len());
457            if remaining == 0 {
458                break;
459            }
460
461            let data = shard.as_ref();
462            let to_take = core::cmp::min(remaining, data.len());
463            result.extend_from_slice(&data[..to_take]);
464        }
465
466        result.truncate(target_len);
467        Ok(result)
468    }
469
470    /// Create a codec with a user-provided encoding matrix.
471    pub fn with_custom_matrix(
472        data_shards: usize,
473        parity_shards: usize,
474        custom_matrix: &[Vec<F::Elem>],
475        mut options: CodecOptions,
476    ) -> Result<ReedSolomon<F>, Error> {
477        if data_shards == 0 {
478            return Err(Error::TooFewDataShards);
479        }
480        if parity_shards == 0 {
481            return Err(Error::TooFewParityShards);
482        }
483        if data_shards + parity_shards > F::ORDER {
484            return Err(Error::TooManyShards);
485        }
486
487        super::leopard::validate_leopard_family::<F>(
488            options.codec_family,
489            data_shards,
490            parity_shards,
491        )?;
492
493        let total_shards = data_shards + parity_shards;
494        options.matrix_mode = MatrixMode::Custom;
495        options.inversion_cache_capacity = Self::normalize_inversion_cache_capacity(
496            data_shards,
497            parity_shards,
498            options.inversion_cache_capacity,
499        );
500
501        if options.codec_family != CodecFamily::Classic {
502            return Err(Error::UnsupportedCodecFamily);
503        }
504
505        let matrix = Self::build_custom_matrix(data_shards, total_shards, custom_matrix)?;
506        let family_state = super::leopard::build_family_state(
507            options.codec_family,
508            data_shards,
509            parity_shards,
510            &matrix,
511        )?;
512        #[cfg(feature = "std")]
513        let policy_cache = Self::resolve_policy_cache_with_options(options);
514
515        Ok(ReedSolomon {
516            data_shard_count: data_shards,
517            parity_shard_count: parity_shards,
518            total_shard_count: total_shards,
519            codec_family: options.codec_family,
520            family_state,
521            matrix,
522            options,
523            #[cfg(feature = "std")]
524            policy_cache,
525            data_decode_matrix_cache: Mutex::new(LruCache::new(options.inversion_cache_capacity)),
526            #[cfg(feature = "std")]
527            reconstruction_cache_metrics: ReconstructionCacheMetrics::default(),
528            #[cfg(feature = "std")]
529            runtime_profile_metrics: RuntimeProfileMetrics::default(),
530        })
531    }
532}