crc_fast/
lib.rs

1// Copyright 2025 Don MacAskill. Licensed under MIT or Apache-2.0.
2
3//! `crc-fast`
4//! ===========
5//!
6//! Hardware-accelerated CRC calculation for
7//! [all known CRC-32 and CRC-64 variants](https://reveng.sourceforge.io/crc-catalogue/all.htm)
8//! using SIMD intrinsics which can exceed 100GiB/s for CRC-32 and 50GiB/s for CRC-64 on modern
9//! systems.
10//!
11//! # Other languages
12//!
13//! Supplies a C-compatible shared library for use with other non-Rust languages. See
14//! [PHP extension](https://github.com/awesomized/crc-fast-php-ext) example.
15//!
16//! # Background
17//!
18//! The implementation is based on Intel's
19//! [Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction](https://web.archive.org/web/20131224125630/https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf),
20//! white paper though it folds 8-at-a-time, like other modern implementations, rather than the
21//! 4-at-a-time as in Intel's paper.
22//!
23//! Works on `aarch64`, `x86_64`, and `x86` architectures, and is hardware-accelerated and optimized
24//! for each architecture.
25//!
26//! Inspired by [`crc32fast`](https://crates.io/crates/crc32fast),
27//! [`crc64fast`](https://crates.io/crates/crc64fast),
28//! and [`crc64fast-nvme`](https://crates.io/crates/crc64fast-nvme), each of which only accelerates
29//! a single, different CRC variant, and all of them were "reflected" variants.
30//!
31//! In contrast, this library accelerates _every known variant_ (and should accelerate any future
32//! variants without changes), including all the "non-reflected" variants.
33//!
34//! # Usage
35//!
36//! ## Digest
37//!
38//! Implements the [digest::DynDigest](https://docs.rs/digest/latest/digest/trait.DynDigest.html)
39//! trait for easier integration with existing code.
40//!
41//! ```rust
42//! use crc_fast::{Digest, CrcAlgorithm::Crc32IsoHdlc};
43//!
44//! let mut digest = Digest::new(Crc32IsoHdlc);
45//! digest.update(b"1234");
46//! digest.update(b"56789");
47//! let checksum = digest.finalize();
48//!
49//! assert_eq!(checksum, 0xcbf43926);
50//! ```
51//!
52//! ## Digest Write
53//!
54//! Implements the [std::io::Write](https://doc.rust-lang.org/std/io/trait.Write.html) trait for
55//! easier integration with existing code.
56//!
57//! ```no_run
58//! use std::env;
59//! use std::fs::File;
60//! use crc_fast::{Digest, CrcAlgorithm::Crc32IsoHdlc};
61//!
62//! // for example/test purposes only, use your own file path
63//! let binding = env::current_dir().expect("missing working dir").join("crc-check.txt");
64//! let file_on_disk = binding.to_str().unwrap();
65//!
66//! // actual usage
67//! let mut digest = Digest::new(Crc32IsoHdlc);
68//! let mut file = File::open(file_on_disk).unwrap();
69//! std::io::copy(&mut file, &mut digest).unwrap();
70//! let checksum = digest.finalize();
71//!
72//! assert_eq!(checksum, 0xcbf43926);
73//! ```
74//! ## checksum
75//!```rust
76//! use crc_fast::{checksum, CrcAlgorithm::Crc32IsoHdlc};
77//!
78//! let checksum = checksum(Crc32IsoHdlc, b"123456789");
79//!
80//! assert_eq!(checksum, 0xcbf43926);
81//! ```
82//!
83//! ## checksum_combine
84//!```rust
85//! use crc_fast::{checksum, checksum_combine, CrcAlgorithm::Crc32IsoHdlc};
86//!
87//! let checksum_1 = checksum(Crc32IsoHdlc, b"1234");
88//! let checksum_2 = checksum(Crc32IsoHdlc, b"56789");
89//! let checksum = checksum_combine(Crc32IsoHdlc, checksum_1, checksum_2, 5);
90//!
91//! assert_eq!(checksum, 0xcbf43926);
92//! ```
93//!
94//! ## checksum_file
95//!```rust
96//! use std::env;
97//! use crc_fast::{checksum_file, CrcAlgorithm::Crc32IsoHdlc};
98//!
99//! // for example/test purposes only, use your own file path
100//! let binding = env::current_dir().expect("missing working dir").join("crc-check.txt");
101//! let file_on_disk = binding.to_str().unwrap();
102//!
103//! let checksum = checksum_file(Crc32IsoHdlc, file_on_disk, None);
104//!
105//! assert_eq!(checksum.unwrap(), 0xcbf43926);
106//! ```
107
108// if VPCLMULQDQ is enabled, enable extra AVX512 features
109#![cfg_attr(
110    feature = "vpclmulqdq",
111    feature(avx512_target_feature, stdarch_x86_avx512)
112)]
113
114use crate::crc32::consts::{
115    CRC32_AIXM, CRC32_AUTOSAR, CRC32_BASE91_D, CRC32_BZIP2, CRC32_CD_ROM_EDC, CRC32_CKSUM,
116    CRC32_ISCSI, CRC32_ISO_HDLC, CRC32_JAMCRC, CRC32_MEF, CRC32_MPEG_2, CRC32_XFER,
117};
118use crate::crc64::consts::{
119    CRC64_ECMA_182, CRC64_GO_ISO, CRC64_MS, CRC64_NVME, CRC64_REDIS, CRC64_WE, CRC64_XZ,
120};
121use crate::structs::{Calculator, CrcParams};
122use crate::traits::CrcCalculator;
123use digest::{DynDigest, InvalidBufferSize};
124use std::fs::File;
125use std::io::{Read, Write};
126
127mod algorithm;
128mod arch;
129mod bindings;
130mod combine;
131mod consts;
132mod crc32;
133mod crc64;
134mod enums;
135mod ffi;
136mod generate;
137mod structs;
138mod test;
139mod traits;
140
141/// Supported CRC-32 and CRC-64 variants
142#[derive(Debug, Clone, Copy)]
143pub enum CrcAlgorithm {
144    Crc32Aixm,
145    Crc32Autosar,
146    Crc32Base91D,
147    Crc32Bzip2,
148    Crc32CdRomEdc,
149    Crc32Cksum,
150    Crc32Iscsi,
151    Crc32IsoHdlc,
152    Crc32Jamcrc,
153    Crc32Mef,
154    Crc32Mpeg2,
155    Crc32Xfer,
156    Crc64Ecma182,
157    Crc64GoIso,
158    Crc64Ms,
159    Crc64Nvme,
160    Crc64Redis,
161    Crc64We,
162    Crc64Xz,
163}
164
165/// Type alias for a function pointer that represents a CRC calculation function.
166///
167/// The function takes the following parameters:
168/// - `state`: The current state of the CRC computation.
169/// - `data`: A slice of bytes to be processed.
170/// - `params`: The parameters for the CRC computation, such as polynomial, initial value, etc.
171///
172/// The function returns the updated state after processing the data.
173type CalculatorFn = fn(
174    u64,       // state
175    &[u8],     // data
176    CrcParams, // CRC implementation parameters
177) -> u64;
178
179/// Represents a CRC Digest, which is used to compute CRC checksums.
180///
181/// The `Digest` struct maintains the state of the CRC computation, including
182/// the current state, the amount of data processed, the CRC parameters, and
183/// the calculator function used to perform the CRC calculation.
184#[derive(Copy, Clone, Debug)]
185pub struct Digest {
186    /// The current state of the CRC computation.
187    state: u64,
188
189    /// The total amount of data processed so far.
190    amount: u64,
191
192    /// The parameters for the CRC computation, such as polynomial, initial value, etc.
193    params: CrcParams,
194
195    /// The function used to perform the CRC calculation.
196    calculator: CalculatorFn,
197}
198
199impl DynDigest for Digest {
200    #[inline(always)]
201    fn update(&mut self, data: &[u8]) {
202        self.update(data);
203    }
204
205    #[inline(always)]
206    fn finalize_into(self, buf: &mut [u8]) -> Result<(), InvalidBufferSize> {
207        if buf.len() != self.output_size() {
208            return Err(InvalidBufferSize);
209        }
210
211        let result = self.finalize();
212        let bytes = if self.output_size() == 4 {
213            result.to_be_bytes()[4..].to_vec() // Take last 4 bytes for 32-bit CRC
214        } else {
215            result.to_be_bytes().to_vec() // Use all 8 bytes for 64-bit CRC
216        };
217        buf.copy_from_slice(&bytes[..self.output_size()]);
218
219        Ok(())
220    }
221
222    #[inline(always)]
223    fn finalize_into_reset(&mut self, out: &mut [u8]) -> Result<(), InvalidBufferSize> {
224        if out.len() != self.output_size() {
225            return Err(InvalidBufferSize);
226        }
227        let result = self.finalize();
228        self.reset();
229        let bytes = if self.output_size() == 4 {
230            result.to_be_bytes()[4..].to_vec() // Take last 4 bytes for 32-bit CRC
231        } else {
232            result.to_be_bytes().to_vec() // Use all 8 bytes for 64-bit CRC
233        };
234        out.copy_from_slice(&bytes[..self.output_size()]);
235        Ok(())
236    }
237
238    #[inline(always)]
239    fn reset(&mut self) {
240        self.reset();
241    }
242
243    #[inline(always)]
244    fn output_size(&self) -> usize {
245        self.params.width as usize / 8
246    }
247
248    fn box_clone(&self) -> Box<dyn DynDigest> {
249        Box::new(*self)
250    }
251}
252
253impl Digest {
254    /// Creates a new `Digest` instance for the specified CRC algorithm.
255    #[inline(always)]
256    pub fn new(algorithm: CrcAlgorithm) -> Self {
257        let (calculator, params) = get_calculator_params(algorithm);
258
259        Self {
260            state: params.init,
261            amount: 0,
262            params,
263            calculator,
264        }
265    }
266
267    /// Updates the CRC state with the given data.
268    #[inline(always)]
269    pub fn update(&mut self, data: &[u8]) {
270        self.state = (self.calculator)(self.state, data, self.params);
271        self.amount += data.len() as u64;
272    }
273
274    /// Finalizes the CRC computation and returns the result.
275    #[inline(always)]
276    pub fn finalize(&self) -> u64 {
277        self.state ^ self.params.xorout
278    }
279
280    /// Finalizes the CRC computation, resets the state, and returns the result.
281    #[inline(always)]
282    pub fn finalize_reset(&mut self) -> u64 {
283        let result = self.finalize();
284        self.reset();
285
286        result
287    }
288
289    /// Resets the CRC state to its initial value.
290    #[inline(always)]
291    pub fn reset(&mut self) {
292        self.state = self.params.init;
293        self.amount = 0;
294    }
295
296    /// Combines the CRC state with a second `Digest` instance.
297    #[inline(always)]
298    pub fn combine(&mut self, other: &Self) {
299        self.amount += other.amount;
300        let other_crc = other.finalize();
301
302        // note the xorout for the input, since it's already been applied so it has to be removed,
303        // and then re-adding it on the final output
304        self.state = combine::checksums(
305            self.state ^ self.params.xorout,
306            other_crc,
307            other.amount,
308            self.params,
309        ) ^ self.params.xorout;
310    }
311
312    /// Gets the amount of data processed so far
313    #[inline(always)]
314    pub fn get_amount(&self) -> u64 {
315        self.amount
316    }
317}
318
319impl Write for Digest {
320    #[inline(always)]
321    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
322        self.update(buf);
323        Ok(buf.len())
324    }
325
326    #[inline(always)]
327    fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
328        let len: usize = bufs
329            .iter()
330            .map(|buf| {
331                self.update(buf);
332                buf.len()
333            })
334            .sum();
335
336        Ok(len)
337    }
338
339    #[inline(always)]
340    fn flush(&mut self) -> std::io::Result<()> {
341        Ok(())
342    }
343
344    #[inline(always)]
345    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
346        self.update(buf);
347
348        Ok(())
349    }
350}
351
352/// Computes the CRC checksum for the given data using the specified algorithm.
353///
354///```rust
355/// use crc_fast::{checksum, CrcAlgorithm::Crc32IsoHdlc};
356/// let checksum = checksum(Crc32IsoHdlc, b"123456789");
357///
358/// assert_eq!(checksum, 0xcbf43926);
359/// ```
360#[inline(always)]
361pub fn checksum(algorithm: CrcAlgorithm, buf: &[u8]) -> u64 {
362    let (calculator, params) = get_calculator_params(algorithm);
363
364    calculator(params.init, buf, params) ^ params.xorout
365}
366
367/// Computes the CRC checksum for the given file using the specified algorithm.
368///
369/// Appears to be much faster (~2X) than using Writer and io::*, at least on Apple M2 Ultra
370///
371/// # Errors
372///
373/// This function will return an error if the file cannot be read.
374///
375/// # Examples
376/// ### checksum_file
377///```no_run
378/// use std::env;
379/// use crc_fast::{checksum_file, CrcAlgorithm::Crc32IsoHdlc};
380///
381/// // for example/test purposes only, use your own file path
382/// let binding = env::current_dir().expect("missing working dir").join("crc-check.txt");
383/// let file_on_disk = binding.to_str().unwrap();
384///
385/// let checksum = checksum_file(Crc32IsoHdlc, file_on_disk, None);
386///
387/// assert_eq!(checksum.unwrap(), 0xcbf43926);
388/// ```
389#[inline(always)]
390pub fn checksum_file(
391    algorithm: CrcAlgorithm,
392    path: &str,
393    chunk_size: Option<usize>,
394) -> Result<u64, std::io::Error> {
395    let mut digest = Digest::new(algorithm);
396    let mut file = File::open(path)?;
397
398    // 512KiB KiB was fastest in my benchmarks on an Apple M2 Ultra
399    //
400    // 4KiB ~7GiB/s
401    // 64KiB ~22 GiB/s
402    // 512KiB ~24 GiB/s
403    let chunk_size = chunk_size.unwrap_or(524288);
404
405    let mut buf = vec![0; chunk_size];
406
407    while let Ok(n) = file.read(&mut buf) {
408        if n == 0 {
409            break;
410        }
411        digest.update(&buf[..n]);
412    }
413
414    Ok(digest.finalize())
415}
416
417/// Combines two CRC checksums using the specified algorithm.
418///
419/// # Examples
420///```rust
421/// use crc_fast::{checksum, checksum_combine, CrcAlgorithm::Crc32IsoHdlc};
422///
423/// let checksum_1 = checksum(Crc32IsoHdlc, b"1234");
424/// let checksum_2 = checksum(Crc32IsoHdlc, b"56789");
425/// let checksum = checksum_combine(Crc32IsoHdlc, checksum_1, checksum_2, 5);
426///
427/// assert_eq!(checksum, 0xcbf43926);
428/// ```
429#[inline(always)]
430pub fn checksum_combine(
431    algorithm: CrcAlgorithm,
432    checksum1: u64,
433    checksum2: u64,
434    checksum2_len: u64,
435) -> u64 {
436    let params = get_calculator_params(algorithm).1;
437
438    combine::checksums(checksum1, checksum2, checksum2_len, params)
439}
440
441/// Returns the target used to calculate the CRC checksum for the specified algorithm.
442///
443/// # Examples
444///```rust
445/// use crc_fast::{get_calculator_target, CrcAlgorithm::Crc32IsoHdlc};
446///
447/// let target = get_calculator_target(Crc32IsoHdlc);
448/// ```
449pub fn get_calculator_target(algorithm: CrcAlgorithm) -> String {
450    match algorithm {
451        CrcAlgorithm::Crc32IsoHdlc => {
452            #[cfg(optimized_crc32_iso_hdlc)]
453            unsafe {
454                bindings::get_iso_hdlc_target()
455            }
456            #[cfg(not(optimized_crc32_iso_hdlc))]
457            arch::get_target()
458        }
459        CrcAlgorithm::Crc32Iscsi => {
460            #[cfg(optimized_crc32_iscsi)]
461            unsafe {
462                bindings::get_iscsi_target()
463            }
464            #[cfg(not(optimized_crc32_iscsi))]
465            arch::get_target()
466        }
467        _ => arch::get_target(),
468    }
469}
470
471/// Returns the calculator function and parameters for the specified CRC algorithm.
472#[inline(always)]
473fn get_calculator_params(algorithm: CrcAlgorithm) -> (CalculatorFn, CrcParams) {
474    match algorithm {
475        CrcAlgorithm::Crc32Aixm => (Calculator::calculate as CalculatorFn, CRC32_AIXM),
476        CrcAlgorithm::Crc32Autosar => (Calculator::calculate as CalculatorFn, CRC32_AUTOSAR),
477        CrcAlgorithm::Crc32Base91D => (Calculator::calculate as CalculatorFn, CRC32_BASE91_D),
478        CrcAlgorithm::Crc32Bzip2 => (Calculator::calculate as CalculatorFn, CRC32_BZIP2),
479        CrcAlgorithm::Crc32CdRomEdc => (Calculator::calculate as CalculatorFn, CRC32_CD_ROM_EDC),
480        CrcAlgorithm::Crc32Cksum => (Calculator::calculate as CalculatorFn, CRC32_CKSUM),
481        CrcAlgorithm::Crc32Iscsi => (crc32_iscsi_calculator as CalculatorFn, CRC32_ISCSI),
482        CrcAlgorithm::Crc32IsoHdlc => (crc32_iso_hdlc_calculator as CalculatorFn, CRC32_ISO_HDLC),
483        CrcAlgorithm::Crc32Jamcrc => (Calculator::calculate as CalculatorFn, CRC32_JAMCRC),
484        CrcAlgorithm::Crc32Mef => (Calculator::calculate as CalculatorFn, CRC32_MEF),
485        CrcAlgorithm::Crc32Mpeg2 => (Calculator::calculate as CalculatorFn, CRC32_MPEG_2),
486        CrcAlgorithm::Crc32Xfer => (Calculator::calculate as CalculatorFn, CRC32_XFER),
487        CrcAlgorithm::Crc64Ecma182 => (Calculator::calculate as CalculatorFn, CRC64_ECMA_182),
488        CrcAlgorithm::Crc64GoIso => (Calculator::calculate as CalculatorFn, CRC64_GO_ISO),
489        CrcAlgorithm::Crc64Ms => (Calculator::calculate as CalculatorFn, CRC64_MS),
490        CrcAlgorithm::Crc64Nvme => (Calculator::calculate as CalculatorFn, CRC64_NVME),
491        CrcAlgorithm::Crc64Redis => (Calculator::calculate as CalculatorFn, CRC64_REDIS),
492        CrcAlgorithm::Crc64We => (Calculator::calculate as CalculatorFn, CRC64_WE),
493        CrcAlgorithm::Crc64Xz => (Calculator::calculate as CalculatorFn, CRC64_XZ),
494    }
495}
496
497/// Calculates the CRC-32/ISCSI ("crc32c" in many, but not all, implementations) checksum.
498///
499/// By default, uses an external optimized C implementation, but can be switched to an internal
500/// SIMD-only implementation by using the `internal_simd_only` feature flag.
501///
502/// The external optimized implementation is also tunable via feature flags.
503#[inline(always)]
504fn crc32_iscsi_calculator(state: u64, data: &[u8], params: CrcParams) -> u64 {
505    #[cfg(optimized_crc32_iscsi)]
506    {
507        bindings::crc32_iscsi(state, data, params)
508    }
509
510    #[cfg(not(optimized_crc32_iscsi))]
511    {
512        Calculator::calculate(state, data, params)
513    }
514}
515
516/// Calculates the CRC-32/ISO-HDLC ("crc32" in many, but not all, implementations) checksum.
517///
518/// By default, uses an external optimized C implementation, but can be switched to an internal
519/// SIMD-only implementation by using the `internal_simd_only` feature flag.
520///
521/// The external optimized implementation is also tunable via feature flags.#[inline(always)]
522fn crc32_iso_hdlc_calculator(state: u64, data: &[u8], params: CrcParams) -> u64 {
523    #[cfg(optimized_crc32_iso_hdlc)]
524    {
525        // Call the FFI function for CRC-32/ISO-HDLC for large (>1KiB) data payloads
526        #[cfg(target_arch = "x86_64")]
527        {
528            if data.len() > 1024 && std::arch::is_x86_feature_detected!("vpclmulqdq") {
529                return bindings::crc32_iso_hdlc(state, data, params);
530            }
531
532            // our internal SIMD implementation for small (<1KiB) data payloads is faster,
533            // only for CRC-32/ISO_HDLC on non-VPCLMULQDQ platforms
534            Calculator::calculate(state, data, params)
535        }
536
537        #[cfg(not(target_arch = "x86_64"))]
538        // Call the FFI function for CRC-32/ISO-HDLC for all payloads non-x86_64
539        return bindings::crc32_iso_hdlc(state, data, params);
540    }
541
542    #[cfg(not(optimized_crc32_iso_hdlc))]
543    {
544        Calculator::calculate(state, data, params)
545    }
546}
547
548#[cfg(test)]
549mod lib {
550    #![allow(unused)]
551
552    use super::*;
553    use crate::test::consts::{TEST_ALL_CONFIGS, TEST_CHECK_STRING};
554    use cbindgen::Language::{Cxx, C};
555    use cbindgen::Style::Both;
556    use rand::{rng, Rng};
557    use std::fs::{read, write};
558
559    #[test]
560    fn test_checksum_check() {
561        for config in TEST_ALL_CONFIGS {
562            assert_eq!(
563                checksum(config.get_algorithm(), TEST_CHECK_STRING),
564                config.get_check()
565            );
566        }
567    }
568
569    #[test]
570    fn test_checksum_reference() {
571        for config in TEST_ALL_CONFIGS {
572            assert_eq!(
573                checksum(config.get_algorithm(), TEST_CHECK_STRING),
574                config.checksum_with_reference(TEST_CHECK_STRING)
575            );
576        }
577    }
578
579    #[test]
580    fn test_digest_updates_check() {
581        for config in TEST_ALL_CONFIGS {
582            let mut digest = Digest::new(config.get_algorithm());
583            digest.update(b"123");
584            digest.update(b"456");
585            digest.update(b"789");
586            let result = digest.finalize();
587
588            assert_eq!(result, config.get_check());
589        }
590    }
591
592    #[test]
593    fn test_small_all_lengths() {
594        let mut rng = rng();
595
596        // Test each CRC-64 variant
597        for config in TEST_ALL_CONFIGS {
598            // Test each length from 1 to 255
599            for len in 1..=255 {
600                // Generate random data for this length
601                let mut data = vec![0u8; len];
602                rng.fill(&mut data[..]);
603
604                // Calculate expected CRC using the reference implementation
605                let expected = config.checksum_with_reference(&data);
606
607                let result = checksum(config.get_algorithm(), &data);
608
609                assert_eq!(result, expected);
610            }
611        }
612    }
613
614    #[test]
615    fn test_medium_lengths() {
616        let mut rng = rng();
617
618        // Test each CRC-64 variant
619        for config in TEST_ALL_CONFIGS {
620            // Test each length from 256 to 1024, which should fold and include handling remainders
621            for len in 256..=1024 {
622                // Generate random data for this length
623                let mut data = vec![0u8; len];
624                rng.fill(&mut data[..]);
625
626                // Calculate expected CRC using the reference implementation
627                let expected = config.checksum_with_reference(&data);
628
629                let result = checksum(config.get_algorithm(), &data);
630
631                assert_eq!(result, expected);
632            }
633        }
634    }
635
636    #[test]
637    fn test_large_lengths() {
638        let mut rng = rng();
639
640        // Test each CRC-64 variant
641        for config in TEST_ALL_CONFIGS {
642            // Test 1 MiB just before, at, and just after the folding boundaries
643            for len in 1048575..1048577 {
644                // Generate random data for this length
645                let mut data = vec![0u8; len];
646                rng.fill(&mut data[..]);
647
648                // Calculate expected CRC using the reference implementation
649                let expected = config.checksum_with_reference(&data);
650
651                let result = checksum(config.get_algorithm(), &data);
652
653                assert_eq!(result, expected);
654            }
655        }
656    }
657
658    #[test]
659    fn test_combine() {
660        for config in TEST_ALL_CONFIGS {
661            let algorithm = config.get_algorithm();
662            let check = config.get_check();
663
664            // checksums
665            let checksum1 = checksum(algorithm, "1234".as_ref());
666            let checksum2 = checksum(algorithm, "56789".as_ref());
667
668            // checksum_combine()
669            assert_eq!(checksum_combine(algorithm, checksum1, checksum2, 5), check,);
670
671            // Digest
672            let mut digest1 = Digest::new(algorithm);
673            digest1.update("1234".as_ref());
674
675            let mut digest2 = Digest::new(algorithm);
676            digest2.update("56789".as_ref());
677
678            digest1.combine(&digest2);
679
680            assert_eq!(digest1.finalize(), check)
681        }
682    }
683
684    #[test]
685    fn test_checksum_file() {
686        // Create a test file with repeating zeros
687        let test_file_path = "test/test_crc32_hash_file.bin";
688        let data = vec![0u8; 1024 * 1024]; // 1 MiB of zeros
689        if let Err(e) = std::fs::write(test_file_path, &data) {
690            eprintln!("Skipping test due to write error: {}", e);
691            return;
692        }
693
694        for config in TEST_ALL_CONFIGS {
695            let result = checksum_file(config.get_algorithm(), test_file_path, None).unwrap();
696            assert_eq!(result, config.checksum_with_reference(&data));
697        }
698
699        std::fs::remove_file(test_file_path).unwrap();
700    }
701
702    #[test]
703    fn test_writer() {
704        // Create a test file with repeating zeros
705        let test_file_path = "test/test_crc32_writer_file.bin";
706        let data = vec![0u8; 1024 * 1024]; // 1 MiB of zeros
707        if let Err(e) = std::fs::write(test_file_path, &data) {
708            eprintln!("Skipping test due to write error: {}", e);
709            return;
710        }
711
712        for config in TEST_ALL_CONFIGS {
713            let mut digest = Digest::new(config.get_algorithm());
714            let mut file = File::open(test_file_path).unwrap();
715            std::io::copy(&mut file, &mut digest).unwrap();
716            assert_eq!(digest.finalize(), config.checksum_with_reference(&data));
717        }
718
719        std::fs::remove_file(test_file_path).unwrap();
720    }
721    #[test]
722    fn test_digest_reset() {
723        for config in TEST_ALL_CONFIGS {
724            let mut digest = Digest::new(config.get_algorithm());
725            digest.update(b"42");
726            digest.reset();
727            digest.update(TEST_CHECK_STRING);
728            assert_eq!(digest.finalize(), config.get_check());
729        }
730    }
731
732    #[test]
733    fn test_digest_finalize_reset() {
734        for config in TEST_ALL_CONFIGS {
735            let check = config.get_check();
736
737            let mut digest = Digest::new(config.get_algorithm());
738            digest.update(TEST_CHECK_STRING);
739            assert_eq!(digest.finalize_reset(), check);
740
741            digest.update(TEST_CHECK_STRING);
742            assert_eq!(digest.finalize(), check);
743        }
744    }
745
746    #[test]
747    fn test_digest_finalize_into() {
748        for config in TEST_ALL_CONFIGS {
749            let mut digest = Digest::new(config.get_algorithm());
750            digest.update(TEST_CHECK_STRING);
751
752            match digest.params.width {
753                32 => {
754                    let mut output = [0u8; 4];
755                    digest.finalize_into(&mut output).unwrap();
756                    let result = u32::from_be_bytes(output) as u64;
757                    assert_eq!(result, config.get_check());
758                }
759                64 => {
760                    let mut output = [0u8; 8];
761                    digest.finalize_into(&mut output).unwrap();
762                    let result = u64::from_be_bytes(output);
763                    assert_eq!(result, config.get_check());
764                }
765                _ => panic!("Unsupported CRC width"),
766            }
767        }
768    }
769
770    #[test]
771    fn test_digest_finalize_into_reset() {
772        for config in TEST_ALL_CONFIGS {
773            let mut digest = Digest::new(config.get_algorithm());
774            digest.update(TEST_CHECK_STRING);
775
776            let mut output: Vec<u8> = match digest.params.width {
777                32 => vec![0u8; 4],
778                64 => vec![0u8; 8],
779                _ => panic!("Unsupported CRC width"),
780            };
781
782            digest.finalize_into_reset(&mut output).unwrap();
783            let result = match output.len() {
784                4 => u32::from_be_bytes(output.try_into().unwrap()) as u64,
785                8 => u64::from_be_bytes(output.try_into().unwrap()),
786                _ => panic!("Unsupported CRC width"),
787            };
788            assert_eq!(result, config.get_check());
789
790            digest.update(TEST_CHECK_STRING);
791            assert_eq!(digest.finalize(), config.get_check());
792        }
793    }
794
795    /// Tests whether the FFI header is up-to-date
796    #[test]
797    fn test_ffi_header() -> Result<(), String> {
798        #[cfg(target_os = "windows")]
799        {
800            // Skip this test on Windows, since CRLF vs LF is a PITA
801            eprintln!("Skipping test on Windows");
802
803            return Ok(());
804        }
805
806        #[cfg(not(target_os = "windows"))]
807        {
808            const HEADER: &str = "libcrc_fast.h";
809
810            let crate_dir =
811                std::env::var("CARGO_MANIFEST_DIR").map_err(|error| error.to_string())?;
812
813            let mut expected = Vec::new();
814            cbindgen::Builder::new()
815                .with_crate(crate_dir)
816                .with_include_guard("CRC_FAST_H")
817                .with_header("/* crc_fast library C/C++ API - Copyright 2025 Don MacAskill */\n/* This header is auto-generated. Do not edit directly. */\n")
818                // exclude internal implementation functions
819                .exclude_item("crc32_iscsi_impl")
820                .exclude_item("crc32_iso_hdlc_impl")
821                .exclude_item("get_iscsi_target")
822                .exclude_item("get_iso_hdlc_target")
823                .exclude_item("ISO_HDLC_TARGET")
824                .exclude_item("ISCSI_TARGET")
825                .exclude_item("CrcParams")
826                .rename_item("Digest", "CrcFastDigest")
827                .with_style(Both)
828                // generate C header
829                .with_language(C)
830                // with C++ compatibility
831                .with_cpp_compat(true)
832                .generate()
833                .map_err(|error| error.to_string())?
834                .write(&mut expected);
835
836            // Convert the expected bytes to string for pattern replacement, since cbindgen
837            // generates an annoying amount of empty contiguous newlines
838            let header_content = String::from_utf8(expected).map_err(|error| error.to_string())?;
839
840            // Replace excessive newlines (3 or more consecutive newlines) with 2 newlines
841            let regex = regex::Regex::new(r"\n{3,}").map_err(|error| error.to_string())?;
842            let cleaned_content = regex.replace_all(&header_content, "\n\n").to_string();
843
844            // Convert back to bytes
845            expected = cleaned_content.into_bytes();
846
847            let actual = read(HEADER).map_err(|error| error.to_string())?;
848
849            if expected != actual {
850                write(HEADER, expected).map_err(|error| error.to_string())?;
851                return Err(format!(
852                    "{HEADER} is not up-to-date, commit the generated file and try again"
853                ));
854            }
855
856            Ok(())
857        }
858    }
859
860    /// Tests whether the CRC-32/ISO-HDLC bindings are up-to-date
861    #[test]
862    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
863    fn test_crc32_iso_hdlc_bindings() -> Result<(), String> {
864        build_bindgen("crc32_iso_hdlc", "src/bindings/crc32_iso_hdlc.rs")
865    }
866
867    /// Tests whether the CRC-32/ISCSI bindings are up-to-date
868    #[test]
869    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))]
870    fn test_crc32_iscsi_bindings() -> Result<(), String> {
871        build_bindgen("crc32_iscsi", "src/bindings/crc32_iscsi.rs")
872    }
873
874    fn build_bindgen(name: &str, bindings_path: &str) -> Result<(), String> {
875        // Getting the Rust cross compile toolchain working on x86 such that it builds correctly
876        // _and_ can validate the header output via bindgen is non-obvious. Since I doubt many
877        // people are actually doing development work on x86, as opposed to x86_64 or aarch64,
878        // I'm just going to skip the bindgen tests on x86. The important tests (do these
879        // CRC-32 variants actually work?) is covered by the other tests, this is just a
880        // development artifact test.
881
882        #[cfg(target_arch = "x86")]
883        {
884            eprintln!("Skipping test on x86 for {} to {}", name, bindings_path);
885
886            return Ok(());
887        }
888
889        // Skip this test on Windows, since CRLF vs LF is a PITA
890        #[cfg(target_os = "windows")]
891        {
892            // Skip this test on Windows, since CRLF vs LF is a PITA
893            eprintln!("Skipping test on Windows");
894
895            return Ok(());
896        }
897
898        #[cfg(not(any(target_arch = "x86", target_os = "windows")))]
899        {
900            let bindings = bindgen::Builder::default()
901                .header(format!("include/{name}.h"))
902                .allowlist_function("crc32_iscsi_impl")
903                .allowlist_function("get_iscsi_target")
904                .allowlist_var("ISCSI_TARGET")
905                .allowlist_function("crc32_iso_hdlc_impl")
906                .allowlist_function("get_iso_hdlc_target")
907                .allowlist_var("ISO_HDLC_TARGET")
908                .generate()
909                .expect("Unable to generate bindings");
910
911            let expected = bindings.to_string().into_bytes();
912
913            let actual = read(bindings_path).map_err(|error| error.to_string())?;
914
915            if expected != actual {
916                bindings
917                    .write_to_file(bindings_path)
918                    .expect("Couldn't write bindings to SRC!");
919
920                return Err(format!(
921                    "{bindings_path} is not up-to-date, commit the generated file and try again"
922                ));
923            }
924
925            Ok(())
926        }
927    }
928}