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