koopman_checksum/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4// Copyright (c) 2025 the koopman-checksum authors, all rights reserved.
5// See README.md for licensing information.
6
7use core::num::{NonZeroU32, NonZeroU64};
8
9// ============================================================================
10// Constants
11// ============================================================================
12
13/// Recommended modulus for 8-bit Koopman checksum.
14/// Detects all 1-bit and 2-bit errors for data up to 13 bytes.
15pub const MODULUS_8: u32 = 253;
16
17/// Recommended modulus for 16-bit Koopman checksum.
18/// Detects all 1-bit and 2-bit errors for data up to 4092 bytes.
19pub const MODULUS_16: u32 = 65519;
20
21/// Recommended modulus for 32-bit Koopman checksum.
22/// Detects all 1-bit and 2-bit errors for data up to 134,217,720 bytes.
23pub const MODULUS_32: u64 = 4294967291;
24
25/// Modulus for 7-bit Koopman checksum with parity.
26/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 5 bytes.
27pub const MODULUS_7P: u32 = 125;
28
29/// Modulus for 15-bit Koopman checksum with parity.
30/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 2044 bytes.
31pub const MODULUS_15P: u32 = 32749;
32
33/// Modulus for 31-bit Koopman checksum with parity.
34/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 134,217,720 bytes.
35pub const MODULUS_31P: u64 = 2147483629;
36
37const NONZERO_MODULUS_8: NonZeroU32 = NonZeroU32::new(MODULUS_8).unwrap();
38const NONZERO_MODULUS_7P: NonZeroU32 = NonZeroU32::new(MODULUS_7P).unwrap();
39const NONZERO_MODULUS_15P: NonZeroU32 = NonZeroU32::new(MODULUS_15P).unwrap();
40const NONZERO_MODULUS_31P: NonZeroU64 = NonZeroU64::new(MODULUS_31P).unwrap();
41
42// ============================================================================
43// Fast Modular Reduction
44//
45// The moduli are of the form 2^k - c where c is small:
46// - 65519 = 2^16 - 17
47// - 4294967291 = 2^32 - 5
48//
49// This allows fast reduction: x % (2^k - c) ≡ (x >> k) * c + (x & (2^k - 1))
50// ============================================================================
51
52/// Fast reduction for modulus 65519 = 2^16 - 17
53/// Input: x up to (MODULUS_16 - 1) << 16 + 0xFFFF ~= 4_293_918_719 (remains < 2^32)
54#[inline(always)]
55fn fast_mod_65519(x: u32) -> u32 {
56    // First reduction: x = hi * 2^16 + lo, result = hi * 17 + lo
57    let hi: u32 = x >> 16;
58    let lo: u32 = x & 0xFFFF;
59    let r: u32 = hi * 17 + lo;
60    // r < 17 * 256 + 65536 = 69888
61    // Second reduction
62    let hi2: u32 = r >> 16;
63    let lo2: u32 = r & 0xFFFF;
64    let r2: u32 = hi2 * 17 + lo2;
65    // r2 < 17 * 2 + 65536 = 65570
66    if r2 >= MODULUS_16 { r2 - MODULUS_16 } else { r2 }
67}
68
69/// Fast reduction for modulus 4294967291 = 2^32 - 5
70/// Input: x < 2^40 (after shift+add)
71#[inline(always)]
72fn fast_mod_4294967291(x: u64) -> u64 {
73    // x = hi * 2^32 + lo, result = hi * 5 + lo
74    let hi: u64 = x >> 32;
75    let lo: u64 = x & 0xFFFFFFFF;
76    let r: u64 = hi * 5 + lo;
77    // r < 5 * 2^8 + 2^32, need one check
78    if r >= MODULUS_32 { r - MODULUS_32 } else { r }
79}
80
81/// Compute an 8-bit Koopman checksum.
82///
83/// Detects all 1-bit and 2-bit errors for data up to 13 bytes with modulus 253.
84///
85/// # Arguments
86/// * `data` - The data bytes to checksum
87/// * `initial_seed` - Initial seed value
88///
89/// # Returns
90/// 8-bit checksum value, or 0 if data is empty
91///
92/// # Example
93/// ```rust
94/// use koopman_checksum::koopman8;
95///
96/// let checksum = koopman8(b"test data", 0xee);
97/// assert_eq!(koopman8(&[], 0xee), 0); // Empty data returns 0
98/// ```
99#[inline]
100#[must_use]
101pub fn koopman8(data: &[u8], initial_seed: u8) -> u8 {
102    koopman8_with_modulus(data, initial_seed, NONZERO_MODULUS_8)
103}
104
105/// Compute an 8-bit Koopman checksum with a custom modulus.
106///
107/// # Arguments
108/// * `data` - The data bytes to checksum
109/// * `initial_seed` - Initial seed value
110/// * `modulus` - The modulus to use (recommended: 253 or 239). Must be non-zero.
111///
112/// # Returns
113/// 8-bit checksum value, or 0 if data is empty
114///
115/// # Example
116/// ```rust
117/// use std::num::NonZeroU32;
118/// use koopman_checksum::koopman8_with_modulus;
119///
120/// let modulus = NonZeroU32::new(239).unwrap();
121/// let checksum = koopman8_with_modulus(b"test", 0xee, modulus);
122/// ```
123#[inline]
124#[must_use]
125pub fn koopman8_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU32) -> u8 {
126    if data.is_empty() {
127        return 0;
128    }
129
130    let modulus = modulus.get();
131    let mut sum: u32 = (data[0] ^ initial_seed) as u32;
132
133    for &byte in &data[1..] {
134        sum = ((sum << 8) + byte as u32) % modulus;
135    }
136
137    // Append implicit zero byte
138    sum = (sum << 8) % modulus;
139
140    sum as u8
141}
142
143/// Compute a 16-bit Koopman checksum.
144///
145/// Detects all 1-bit and 2-bit errors for data up to 4092 bytes.
146///
147/// # Arguments
148/// * `data` - The data bytes to checksum
149/// * `initial_seed` - Initial seed value
150///
151/// # Returns
152/// 16-bit checksum value, or 0 if data is empty
153///
154/// # Example
155/// ```rust
156/// use koopman_checksum::koopman16;
157///
158/// let checksum = koopman16(b"test data", 0xee);
159/// assert_eq!(koopman16(&[], 0xee), 0); // Empty data returns 0
160/// ```
161#[inline]
162#[must_use]
163pub fn koopman16(data: &[u8], initial_seed: u8) -> u16 {
164    if data.is_empty() {
165        return 0;
166    }
167
168    let mut sum: u64 = (data[0] ^ initial_seed) as u64;
169
170    // Process bytes with delayed modulo reduction every 2 bytes
171    // This reduces the number of modulo operations by half
172    let mut count = 0;
173    for &byte in &data[1..] {
174        sum = (sum << 8) + byte as u64;
175        count += 1;
176        if count == 2 {
177            sum = fast_mod_65519(sum as u32) as u64;
178            count = 0;
179        }
180    }
181
182    // Final reduction if needed
183    if count > 0 {
184        sum = fast_mod_65519(sum as u32) as u64;
185    }
186
187    // Append two implicit zero bytes
188    sum = fast_mod_65519((sum << 8) as u32) as u64;
189    sum = fast_mod_65519((sum << 8) as u32) as u64;
190
191    sum as u16
192}
193
194/// Compute a 16-bit Koopman checksum with a custom modulus.
195///
196/// # Arguments
197/// * `data` - The data bytes to checksum
198/// * `initial_seed` - Initial seed value
199/// * `modulus` - The modulus to use. Must be non-zero.
200///
201/// # Returns
202/// 16-bit checksum value, or 0 if data is empty
203///
204/// # Example
205/// ```rust
206/// use std::num::NonZeroU32;
207/// use koopman_checksum::koopman16_with_modulus;
208///
209/// let modulus = NonZeroU32::new(65519).unwrap();
210/// let checksum = koopman16_with_modulus(b"test", 0xee, modulus);
211/// ```
212#[inline]
213#[must_use]
214pub fn koopman16_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU32) -> u16 {
215    if data.is_empty() {
216        return 0;
217    }
218
219    let modulus = modulus.get();
220    let mut sum: u32 = (data[0] ^ initial_seed) as u32;
221
222    for &byte in &data[1..] {
223        sum = ((sum << 8) + byte as u32) % modulus;
224    }
225
226    // Append two implicit zero bytes
227    sum = (sum << 8) % modulus;
228    sum = (sum << 8) % modulus;
229
230    sum as u16
231}
232
233/// Compute a 32-bit Koopman checksum.
234///
235/// Detects all 1-bit and 2-bit errors for data up to 134,217,720 bytes.
236///
237/// # Arguments
238/// * `data` - The data bytes to checksum
239/// * `initial_seed` - Initial seed value
240///
241/// # Returns
242/// 32-bit checksum value, or 0 if data is empty
243///
244/// # Example
245/// ```rust
246/// use koopman_checksum::koopman32;
247///
248/// let checksum = koopman32(b"test data", 0xee);
249/// assert_eq!(koopman32(&[], 0xee), 0); // Empty data returns 0
250/// ```
251#[inline]
252#[must_use]
253pub fn koopman32(data: &[u8], initial_seed: u8) -> u32 {
254    if data.is_empty() {
255        return 0;
256    }
257
258    let mut sum: u64 = (data[0] ^ initial_seed) as u64;
259
260    // Use fast modular reduction for the default modulus
261    for &byte in &data[1..] {
262        sum = fast_mod_4294967291((sum << 8) + byte as u64);
263    }
264
265    // Append four implicit zero bytes
266    sum = fast_mod_4294967291(sum << 8);
267    sum = fast_mod_4294967291(sum << 8);
268    sum = fast_mod_4294967291(sum << 8);
269    sum = fast_mod_4294967291(sum << 8);
270
271    sum as u32
272}
273
274/// Compute a 32-bit Koopman checksum with a custom modulus.
275///
276/// # Arguments
277/// * `data` - The data bytes to checksum
278/// * `initial_seed` - Initial seed value
279/// * `modulus` - The modulus to use. Must be non-zero.
280///
281/// # Returns
282/// 32-bit checksum value, or 0 if data is empty
283///
284/// # Example
285/// ```rust
286/// use std::num::NonZeroU64;
287/// use koopman_checksum::koopman32_with_modulus;
288///
289/// let modulus = NonZeroU64::new(4294967291).unwrap();
290/// let checksum = koopman32_with_modulus(b"test", 0xee, modulus);
291/// ```
292#[inline]
293#[must_use]
294pub fn koopman32_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU64) -> u32 {
295    if data.is_empty() {
296        return 0;
297    }
298
299    let modulus = modulus.get();
300    let mut sum: u64 = (data[0] ^ initial_seed) as u64;
301
302    for &byte in &data[1..] {
303        sum = ((sum << 8) + byte as u64) % modulus;
304    }
305
306    // Append four implicit zero bytes
307    sum = (sum << 8) % modulus;
308    sum = (sum << 8) % modulus;
309    sum = (sum << 8) % modulus;
310    sum = (sum << 8) % modulus;
311
312    sum as u32
313}
314
315// ============================================================================
316// Parity Variants (HD=4)
317// ============================================================================
318
319/// Compute parity of a byte (number of set bits mod 2).
320#[inline]
321fn parity8(x: u8) -> u8 {
322    (x.count_ones() & 1) as u8
323}
324
325/// Compute an 8-bit Koopman checksum with parity (7-bit checksum + 1 parity bit).
326///
327/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 5 bytes.
328/// Uses modulus 125 for the 7-bit checksum portion.
329///
330/// # Arguments
331/// * `data` - The data bytes to checksum
332/// * `initial_seed` - Initial seed value
333///
334/// # Returns
335/// 8-bit value: 7-bit checksum in upper bits, parity in LSB, or 0 if data is empty
336///
337/// # Example
338/// ```rust
339/// use koopman_checksum::koopman8p;
340///
341/// let checksum = koopman8p(b"test", 0xee);
342/// let parity_bit = checksum & 1;
343/// let checksum_bits = checksum >> 1;
344/// ```
345#[inline]
346#[must_use]
347pub fn koopman8p(data: &[u8], initial_seed: u8) -> u8 {
348    koopman8p_with_modulus(data, initial_seed, NONZERO_MODULUS_7P)
349}
350
351/// Compute an 8-bit Koopman checksum with parity using a custom modulus.
352///
353/// # Arguments
354/// * `data` - The data bytes to checksum
355/// * `initial_seed` - Initial seed value
356/// * `modulus` - The modulus for the 7-bit checksum. Must be non-zero and <= 127.
357///
358/// # Returns
359/// 8-bit value: 7-bit checksum in upper bits, parity in LSB, or 0 if data is empty
360///
361/// # Example
362/// ```rust
363/// use std::num::NonZeroU32;
364/// use koopman_checksum::koopman8p_with_modulus;
365///
366/// let modulus = NonZeroU32::new(125).unwrap();
367/// let checksum = koopman8p_with_modulus(b"test", 0xee, modulus);
368/// ```
369#[inline]
370#[must_use]
371pub fn koopman8p_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU32) -> u8 {
372    if data.is_empty() {
373        return 0;
374    }
375
376    let modulus = modulus.get();
377    let mut sum: u32 = (data[0] ^ initial_seed) as u32;
378    let mut psum: u8 = sum as u8;
379
380    for &byte in &data[1..] {
381        sum = ((sum << 8) + byte as u32) % modulus;
382        psum ^= byte;
383    }
384
385    // Append implicit zero byte
386    sum = (sum << 8) % modulus;
387
388    // Pack: checksum in upper 7 bits, parity in LSB
389    // Parity covers the same byte stream as the checksum core, i.e. data[0] ^ seed
390    ((sum as u8) << 1) | parity8(psum)
391}
392
393/// Compute a 16-bit Koopman checksum with parity (15-bit checksum + 1 parity bit).
394///
395/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 2044 bytes.
396/// Uses modulus 32749 for the 15-bit checksum portion.
397///
398/// # Arguments
399/// * `data` - The data bytes to checksum
400/// * `initial_seed` - Initial seed value
401///
402/// # Returns
403/// 16-bit value: 15-bit checksum in upper bits, parity in LSB, or 0 if data is empty
404///
405/// # Example
406/// ```rust
407/// use koopman_checksum::koopman16p;
408///
409/// let checksum = koopman16p(b"test data", 0xee);
410/// let parity_bit = checksum & 1;
411/// let checksum_bits = checksum >> 1;
412/// ```
413#[inline]
414#[must_use]
415pub fn koopman16p(data: &[u8], initial_seed: u8) -> u16 {
416    koopman16p_with_modulus(data, initial_seed, NONZERO_MODULUS_15P)
417}
418
419/// Compute a 16-bit Koopman checksum with parity using a custom modulus.
420///
421/// # Arguments
422/// * `data` - The data bytes to checksum
423/// * `initial_seed` - Initial seed value
424/// * `modulus` - The modulus for the 15-bit checksum. Must be non-zero and ≤ 32767.
425///
426/// # Returns
427/// 16-bit value: 15-bit checksum in upper bits, parity in LSB, or 0 if data is empty
428///
429/// # Example
430/// ```rust
431/// use std::num::NonZeroU32;
432/// use koopman_checksum::koopman16p_with_modulus;
433///
434/// let modulus = NonZeroU32::new(32749).unwrap();
435/// let checksum = koopman16p_with_modulus(b"test", 0xee, modulus);
436/// ```
437#[inline]
438#[must_use]
439pub fn koopman16p_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU32) -> u16 {
440    if data.is_empty() {
441        return 0;
442    }
443
444    let modulus = modulus.get();
445    let mut sum: u32 = (data[0] ^ initial_seed) as u32;
446    let mut psum: u8 = sum as u8;
447
448    for &byte in &data[1..] {
449        sum = ((sum << 8) + byte as u32) % modulus;
450        psum ^= byte;
451    }
452
453    // Append two implicit zero bytes
454    sum = (sum << 8) % modulus;
455    sum = (sum << 8) % modulus;
456
457    // Pack: checksum in upper 15 bits, parity in LSB
458    // Parity covers the same byte stream as the checksum core, i.e. data[0] ^ seed
459    ((sum as u16) << 1) | (parity8(psum) as u16)
460}
461
462/// Compute a 32-bit Koopman checksum with parity (31-bit checksum + 1 parity bit).
463///
464/// Detects all 1-bit, 2-bit, and 3-bit errors for data up to 134,217,720 bytes.
465/// Uses modulus 2147483629 for the 31-bit checksum portion.
466///
467/// # Arguments
468/// * `data` - The data bytes to checksum
469/// * `initial_seed` - Initial seed value
470///
471/// # Returns
472/// 32-bit value: 31-bit checksum in upper bits, parity in LSB, or 0 if data is empty
473///
474/// # Example
475/// ```rust
476/// use koopman_checksum::koopman32p;
477///
478/// let checksum = koopman32p(b"test data", 0xee);
479/// let parity_bit = checksum & 1;
480/// let checksum_bits = checksum >> 1;
481/// ```
482#[inline]
483#[must_use]
484pub fn koopman32p(data: &[u8], initial_seed: u8) -> u32 {
485    koopman32p_with_modulus(data, initial_seed, NONZERO_MODULUS_31P)
486}
487
488/// Compute a 32-bit Koopman checksum with parity using a custom modulus.
489///
490/// # Arguments
491/// * `data` - The data bytes to checksum
492/// * `initial_seed` - Initial seed value
493/// * `modulus` - The modulus for the 31-bit checksum. Must be non-zero and <= 2^31-1.
494///
495/// # Returns
496/// 32-bit value: 31-bit checksum in upper bits, parity in LSB, or 0 if data is empty
497///
498/// # Example
499/// ```rust
500/// use std::num::NonZeroU64;
501/// use koopman_checksum::koopman32p_with_modulus;
502///
503/// let modulus = NonZeroU64::new(2147483629).unwrap();
504/// let checksum = koopman32p_with_modulus(b"test", 0xee, modulus);
505/// ```
506#[inline]
507#[must_use]
508pub fn koopman32p_with_modulus(data: &[u8], initial_seed: u8, modulus: NonZeroU64) -> u32 {
509    if data.is_empty() {
510        return 0;
511    }
512
513    let modulus = modulus.get();
514    let mut sum: u64 = (data[0] ^ initial_seed) as u64;
515    let mut psum: u8 = sum as u8;
516
517    for &byte in &data[1..] {
518        sum = ((sum << 8) + byte as u64) % modulus;
519        psum ^= byte;
520    }
521
522    // Append four implicit zero bytes
523    sum = (sum << 8) % modulus;
524    sum = (sum << 8) % modulus;
525    sum = (sum << 8) % modulus;
526    sum = (sum << 8) % modulus;
527
528    // Pack: checksum in upper 31 bits, parity in LSB
529    // Parity covers the same byte stream as the checksum core, i.e. data[0] ^ seed
530    ((sum as u32) << 1) | (parity8(psum) as u32)
531}
532
533// ============================================================================
534// Streaming/Incremental API
535// ============================================================================
536
537/// Macro to generate streaming checksum structs.
538/// This reduces code duplication across Koopman8, Koopman16, Koopman32.
539macro_rules! impl_streaming_hasher {
540    (
541        $name:ident,
542        $sum_type:ty,
543        $output_type:ty,
544        $default_modulus_raw:expr,
545        $nonzero_type:ty,
546        $finalize_shifts:expr,
547        $fast_mod:expr
548    ) => {
549        impl Default for $name {
550            fn default() -> Self {
551                Self::new()
552            }
553        }
554
555        impl $name {
556            /// Create a new hasher with the default modulus.
557            #[inline]
558            pub fn new() -> Self {
559                Self {
560                    sum: 0,
561                    modulus: $default_modulus_raw,
562                    seed: 0,
563                    initialized: false,
564                    use_fast_mod: true,
565                }
566            }
567
568            /// Create a new hasher with a custom modulus.
569            ///
570            /// # Arguments
571            /// * `modulus` - The modulus to use. Must be non-zero.
572            ///
573            /// # Example
574            /// ```rust
575            #[doc = concat!("use std::num::", stringify!($nonzero_type), ";")]
576            #[doc = concat!("use koopman_checksum::{", stringify!($name), ", ", stringify!($default_modulus_raw), "};")]
577            ///
578            #[doc = concat!("let modulus = ", stringify!($nonzero_type), "::new(", stringify!($default_modulus_raw), ").unwrap();")]
579            #[doc = concat!("let hasher = ", stringify!($name), "::with_modulus(modulus);")]
580            /// ```
581            #[inline]
582            pub fn with_modulus(modulus: $nonzero_type) -> Self {
583                let modulus_val = modulus.get();
584                Self {
585                    sum: 0,
586                    modulus: modulus_val,
587                    seed: 0,
588                    initialized: false,
589                    use_fast_mod: modulus_val == $default_modulus_raw,
590                }
591            }
592
593            /// Create a new hasher with an initial seed.
594            ///
595            /// # Example
596            /// ```rust
597            #[doc = concat!("use koopman_checksum::", stringify!($name), ";")]
598            ///
599            #[doc = concat!("let hasher = ", stringify!($name), "::with_seed(0xee);")]
600            /// ```
601            #[inline]
602            pub fn with_seed(seed: u8) -> Self {
603                Self {
604                    sum: seed as $sum_type,
605                    modulus: $default_modulus_raw,
606                    seed: seed as $sum_type,
607                    initialized: false,
608                    use_fast_mod: true,
609                }
610            }
611
612            /// Update the checksum with more data.
613            #[inline]
614            pub fn update(&mut self, data: &[u8]) {
615                if data.is_empty() {
616                    return;
617                }
618
619                let mut iter = data.iter();
620
621                if !self.initialized {
622                    if let Some(&first) = iter.next() {
623                        self.sum ^= first as $sum_type;
624                        self.initialized = true;
625                    }
626                }
627
628                if self.use_fast_mod {
629                    for &byte in iter {
630                        self.sum = $fast_mod((self.sum << 8) + byte as $sum_type);
631                    }
632                } else {
633                    for &byte in iter {
634                        self.sum = ((self.sum << 8) + byte as $sum_type) % self.modulus;
635                    }
636                }
637            }
638
639            /// Finalize and return the checksum.
640            ///
641            /// Returns 0 if no data was provided.
642            #[inline]
643            #[must_use]
644            pub fn finalize(self) -> $output_type {
645                if !self.initialized {
646                    return 0;
647                }
648                let mut sum = self.sum;
649                if self.use_fast_mod {
650                    for _ in 0..$finalize_shifts {
651                        sum = $fast_mod(sum << 8);
652                    }
653                } else {
654                    for _ in 0..$finalize_shifts {
655                        sum = (sum << 8) % self.modulus;
656                    }
657                }
658                sum as $output_type
659            }
660
661            /// Reset the hasher to initial state.
662            #[inline]
663            pub fn reset(&mut self) {
664                self.sum = self.seed;
665                self.initialized = false;
666            }
667        }
668    };
669}
670
671/// Incremental Koopman8 checksum calculator.
672///
673/// Allows computing checksums over data that arrives in chunks.
674///
675/// # Example
676/// ```rust
677/// use koopman_checksum::Koopman8;
678///
679/// let mut hasher = Koopman8::new();
680/// hasher.update(b"Hello, ");
681/// hasher.update(b"World!");
682/// let checksum = hasher.finalize();
683/// ```
684#[derive(Clone, Debug)]
685pub struct Koopman8 {
686    sum: u32,
687    modulus: u32,
688    seed: u32,
689    initialized: bool,
690    use_fast_mod: bool,
691}
692
693// Koopman8 doesn't have a fast_mod, so we use a passthrough
694#[inline(always)]
695fn identity_mod_8(x: u32) -> u32 { x % MODULUS_8 }
696
697impl_streaming_hasher!(
698    Koopman8, u32, u8,
699    MODULUS_8, NonZeroU32,
700    1, identity_mod_8
701);
702
703/// Incremental Koopman16 checksum calculator.
704///
705/// Allows computing checksums over data that arrives in chunks.
706/// Uses fast modular reduction when using the default modulus.
707///
708/// # Example
709/// ```rust
710/// use koopman_checksum::Koopman16;
711///
712/// let mut hasher = Koopman16::new();
713/// hasher.update(b"Hello, ");
714/// hasher.update(b"World!");
715/// let checksum = hasher.finalize();
716/// ```
717#[derive(Clone, Debug)]
718pub struct Koopman16 {
719    sum: u32,
720    modulus: u32,
721    seed: u32,
722    initialized: bool,
723    use_fast_mod: bool,
724}
725
726impl_streaming_hasher!(
727    Koopman16, u32, u16,
728    MODULUS_16, NonZeroU32,
729    2, fast_mod_65519
730);
731
732/// Incremental Koopman32 checksum calculator.
733///
734/// Allows computing checksums over data that arrives in chunks.
735/// Uses fast modular reduction when using the default modulus.
736///
737/// # Example
738/// ```rust
739/// use koopman_checksum::Koopman32;
740///
741/// let mut hasher = Koopman32::new();
742/// hasher.update(b"Hello, ");
743/// hasher.update(b"World!");
744/// let checksum = hasher.finalize();
745/// ```
746#[derive(Clone, Debug)]
747pub struct Koopman32 {
748    sum: u64,
749    modulus: u64,
750    seed: u64,
751    initialized: bool,
752    use_fast_mod: bool,
753}
754
755impl_streaming_hasher!(
756    Koopman32, u64, u32,
757    MODULUS_32, NonZeroU64,
758    4, fast_mod_4294967291
759);
760
761// ============================================================================
762// Parity Streaming API
763// ============================================================================
764
765/// Macro to generate streaming parity checksum structs.
766macro_rules! impl_streaming_parity_hasher {
767    (
768        $name:ident,
769        $sum_type:ty,
770        $output_type:ty,
771        $default_modulus_raw:expr,
772        $nonzero_type:ty,
773        $finalize_shifts:expr
774    ) => {
775        impl Default for $name {
776            fn default() -> Self {
777                Self::new()
778            }
779        }
780
781        impl $name {
782            /// Create a new hasher with the default modulus.
783            #[inline]
784            pub fn new() -> Self {
785                Self {
786                    sum: 0,
787                    psum: 0,
788                    modulus: $default_modulus_raw,
789                    seed: 0,
790                    initialized: false,
791                }
792            }
793
794            /// Create a new hasher with a custom modulus.
795            ///
796            /// # Arguments
797            /// * `modulus` - The modulus to use. Must be non-zero.
798            #[inline]
799            pub fn with_modulus(modulus: $nonzero_type) -> Self {
800                Self {
801                    sum: 0,
802                    psum: 0,
803                    modulus: modulus.get(),
804                    seed: 0,
805                    initialized: false,
806                }
807            }
808
809            /// Create a new hasher with an initial seed.
810            #[inline]
811            pub fn with_seed(seed: u8) -> Self {
812                Self {
813                    sum: seed as $sum_type,
814                    psum: seed,
815                    modulus: $default_modulus_raw,
816                    seed: seed as $sum_type,
817                    initialized: false,
818                }
819            }
820
821            /// Update the checksum with more data.
822            #[inline]
823            pub fn update(&mut self, data: &[u8]) {
824                if data.is_empty() {
825                    return;
826                }
827
828                let mut iter = data.iter();
829
830                if !self.initialized {
831                    if let Some(&first) = iter.next() {
832                        self.sum ^= first as $sum_type;
833                        self.psum ^= first;
834                        self.initialized = true;
835                    }
836                }
837
838                for &byte in iter {
839                    self.sum = ((self.sum << 8) + byte as $sum_type) % self.modulus;
840                    self.psum ^= byte;
841                }
842            }
843
844            /// Finalize and return the checksum with parity.
845            ///
846            /// Returns 0 if no data was provided.
847            #[inline]
848            #[must_use]
849            pub fn finalize(self) -> $output_type {
850                if !self.initialized {
851                    return 0;
852                }
853                let mut sum = self.sum;
854                for _ in 0..$finalize_shifts {
855                    sum = (sum << 8) % self.modulus;
856                }
857                // Pack: checksum in upper bits, parity in LSB
858                ((sum as $output_type) << 1) | (parity8(self.psum) as $output_type)
859            }
860
861            /// Reset the hasher to initial state.
862            #[inline]
863            pub fn reset(&mut self) {
864                self.sum = self.seed;
865                self.psum = self.seed as u8;
866                self.initialized = false;
867            }
868        }
869    };
870}
871
872/// Incremental Koopman8P checksum calculator (7-bit checksum + 1 parity bit).
873///
874/// Allows computing checksums over data that arrives in chunks.
875///
876/// # Example
877/// ```rust
878/// use koopman_checksum::Koopman8P;
879///
880/// let mut hasher = Koopman8P::new();
881/// hasher.update(b"Hello");
882/// let checksum = hasher.finalize();
883/// let parity_bit = checksum & 1;
884/// ```
885#[derive(Clone, Debug)]
886pub struct Koopman8P {
887    sum: u32,
888    psum: u8,
889    modulus: u32,
890    seed: u32,
891    initialized: bool,
892}
893
894impl_streaming_parity_hasher!(
895    Koopman8P, u32, u8,
896    MODULUS_7P, NonZeroU32,
897    1
898);
899
900/// Incremental Koopman16P checksum calculator (15-bit checksum + 1 parity bit).
901///
902/// Allows computing checksums over data that arrives in chunks.
903///
904/// # Example
905/// ```rust
906/// use koopman_checksum::Koopman16P;
907///
908/// let mut hasher = Koopman16P::new();
909/// hasher.update(b"Hello, ");
910/// hasher.update(b"World!");
911/// let checksum = hasher.finalize();
912/// let parity_bit = checksum & 1;
913/// ```
914#[derive(Clone, Debug)]
915pub struct Koopman16P {
916    sum: u32,
917    psum: u8,
918    modulus: u32,
919    seed: u32,
920    initialized: bool,
921}
922
923impl_streaming_parity_hasher!(
924    Koopman16P, u32, u16,
925    MODULUS_15P, NonZeroU32,
926    2
927);
928
929/// Incremental Koopman32P checksum calculator (31-bit checksum + 1 parity bit).
930///
931/// Allows computing checksums over data that arrives in chunks.
932///
933/// # Example
934/// ```rust
935/// use koopman_checksum::Koopman32P;
936///
937/// let mut hasher = Koopman32P::new();
938/// hasher.update(b"Hello, ");
939/// hasher.update(b"World!");
940/// let checksum = hasher.finalize();
941/// let parity_bit = checksum & 1;
942/// ```
943#[derive(Clone, Debug)]
944pub struct Koopman32P {
945    sum: u64,
946    psum: u8,
947    modulus: u64,
948    seed: u64,
949    initialized: bool,
950}
951
952impl_streaming_parity_hasher!(
953    Koopman32P, u64, u32,
954    MODULUS_31P, NonZeroU64,
955    4
956);
957
958// ============================================================================
959// Verification Functions
960// ============================================================================
961
962/// Verify data integrity using Koopman8 checksum.
963///
964/// # Arguments
965/// * `data` - The data bytes (excluding checksum)
966/// * `expected` - The expected checksum value
967/// * `initial_seed` - Initial seed used when computing the checksum
968///
969/// # Returns
970/// `true` if the checksum matches, `false` otherwise
971///
972/// # Example
973/// ```rust
974/// use koopman_checksum::{koopman8, verify8};
975///
976/// let data = b"test data";
977/// let checksum = koopman8(data, 0xee);
978/// assert!(verify8(data, checksum, 0xee));
979/// assert!(!verify8(data, checksum.wrapping_add(1), 0));
980/// ```
981#[inline]
982#[must_use]
983pub fn verify8(data: &[u8], expected: u8, initial_seed: u8) -> bool {
984    koopman8(data, initial_seed) == expected
985}
986
987/// Verify data integrity using Koopman16 checksum.
988///
989/// # Arguments
990/// * `data` - The data bytes (excluding checksum)
991/// * `expected` - The expected checksum value
992/// * `initial_seed` - Initial seed used when computing the checksum
993///
994/// # Returns
995/// `true` if the checksum matches, `false` otherwise
996///
997/// # Example
998/// ```rust
999/// use koopman_checksum::{koopman16, verify16};
1000///
1001/// let data = b"test data";
1002/// let checksum = koopman16(data, 0xee);
1003/// assert!(verify16(data, checksum, 0xee));
1004/// ```
1005#[inline]
1006#[must_use]
1007pub fn verify16(data: &[u8], expected: u16, initial_seed: u8) -> bool {
1008    koopman16(data, initial_seed) == expected
1009}
1010
1011/// Verify data integrity using Koopman32 checksum.
1012///
1013/// # Arguments
1014/// * `data` - The data bytes (excluding checksum)
1015/// * `expected` - The expected checksum value
1016/// * `initial_seed` - Initial seed used when computing the checksum
1017///
1018/// # Returns
1019/// `true` if the checksum matches, `false` otherwise
1020///
1021/// # Example
1022/// ```rust
1023/// use koopman_checksum::{koopman32, verify32};
1024///
1025/// let data = b"test data";
1026/// let checksum = koopman32(data, 0xee);
1027/// assert!(verify32(data, checksum, 0xee));
1028/// ```
1029#[inline]
1030#[must_use]
1031pub fn verify32(data: &[u8], expected: u32, initial_seed: u8) -> bool {
1032    koopman32(data, initial_seed) == expected
1033}
1034
1035/// Verify data integrity using Koopman8P checksum (with parity).
1036///
1037/// # Arguments
1038/// * `data` - The data bytes (excluding checksum)
1039/// * `expected` - The expected checksum value (7-bit checksum + 1 parity bit)
1040/// * `initial_seed` - Initial seed used when computing the checksum
1041///
1042/// # Returns
1043/// `true` if the checksum matches, `false` otherwise
1044///
1045/// # Example
1046/// ```rust
1047/// use koopman_checksum::{koopman8p, verify8p};
1048///
1049/// let data = b"test";
1050/// let checksum = koopman8p(data, 0xee);
1051/// assert!(verify8p(data, checksum, 0xee));
1052/// ```
1053#[inline]
1054#[must_use]
1055pub fn verify8p(data: &[u8], expected: u8, initial_seed: u8) -> bool {
1056    koopman8p(data, initial_seed) == expected
1057}
1058
1059/// Verify data integrity using Koopman16P checksum (with parity).
1060///
1061/// # Arguments
1062/// * `data` - The data bytes (excluding checksum)
1063/// * `expected` - The expected checksum value (15-bit checksum + 1 parity bit)
1064/// * `initial_seed` - Initial seed used when computing the checksum
1065///
1066/// # Returns
1067/// `true` if the checksum matches, `false` otherwise
1068///
1069/// # Example
1070/// ```rust
1071/// use koopman_checksum::{koopman16p, verify16p};
1072///
1073/// let data = b"test data";
1074/// let checksum = koopman16p(data, 0xee);
1075/// assert!(verify16p(data, checksum, 0xee));
1076/// ```
1077#[inline]
1078#[must_use]
1079pub fn verify16p(data: &[u8], expected: u16, initial_seed: u8) -> bool {
1080    koopman16p(data, initial_seed) == expected
1081}
1082
1083/// Verify data integrity using Koopman32P checksum (with parity).
1084///
1085/// # Arguments
1086/// * `data` - The data bytes (excluding checksum)
1087/// * `expected` - The expected checksum value (31-bit checksum + 1 parity bit)
1088/// * `initial_seed` - Initial seed used when computing the checksum
1089///
1090/// # Returns
1091/// `true` if the checksum matches, `false` otherwise
1092///
1093/// # Example
1094/// ```rust
1095/// use koopman_checksum::{koopman32p, verify32p};
1096///
1097/// let data = b"test data";
1098/// let checksum = koopman32p(data, 0xee);
1099/// assert!(verify32p(data, checksum, 0xee));
1100/// ```
1101#[inline]
1102#[must_use]
1103pub fn verify32p(data: &[u8], expected: u32, initial_seed: u8) -> bool {
1104    koopman32p(data, initial_seed) == expected
1105}
1106
1107// ============================================================================
1108// Tests
1109// ============================================================================
1110
1111#[cfg(test)]
1112mod tests {
1113    use super::*;
1114    use core::num::NonZeroU32;
1115    use core::num::NonZeroU64;
1116    const NONZERO_MODULUS_16: NonZeroU32 = NonZeroU32::new(MODULUS_16).unwrap();
1117    const NONZERO_MODULUS_32: NonZeroU64 = NonZeroU64::new(MODULUS_32).unwrap();
1118
1119    // Test vectors based on the C reference implementation
1120    const TEST_DATA: &[u8] = b"123456789";
1121
1122    #[test]
1123    fn test_koopman8_empty() {
1124        assert_eq!(koopman8(&[], 0), 0);
1125        assert_eq!(koopman8(&[], 42), 0); // Empty data returns 0 regardless of initial seed
1126    }
1127
1128    #[test]
1129    fn test_koopman8_single_byte() {
1130        // For single byte 0x12: sum = 0x12, then append zero: (0x12 << 8) % 253 = 4608 % 253 = 54
1131        assert_eq!(koopman8(&[0x12], 0), ((0x12u32 << 8) % MODULUS_8) as u8);
1132    }
1133
1134    #[test]
1135    fn test_koopman16_empty() {
1136        assert_eq!(koopman16(&[], 0), 0);
1137        assert_eq!(koopman16(&[], 42), 0); // Empty data returns 0 regardless of initial seed
1138    }
1139
1140    #[test]
1141    fn test_koopman32_empty() {
1142        assert_eq!(koopman32(&[], 0), 0);
1143        assert_eq!(koopman32(&[], 42), 0); // Empty data returns 0 regardless of initial seed
1144    }
1145
1146    #[test]
1147    fn test_streaming_koopman8() {
1148        let full = koopman8(TEST_DATA, 0);
1149
1150        let mut hasher = Koopman8::new();
1151        hasher.update(&TEST_DATA[..4]);
1152        hasher.update(&TEST_DATA[4..]);
1153        let streaming = hasher.finalize();
1154
1155        assert_eq!(full, streaming);
1156    }
1157
1158    #[test]
1159    fn test_streaming_koopman16() {
1160        let full = koopman16(TEST_DATA, 0);
1161
1162        let mut hasher = Koopman16::new();
1163        hasher.update(&TEST_DATA[..4]);
1164        hasher.update(&TEST_DATA[4..]);
1165        let streaming = hasher.finalize();
1166
1167        assert_eq!(full, streaming);
1168    }
1169
1170    #[test]
1171    fn test_streaming_koopman32() {
1172        let full = koopman32(TEST_DATA, 0);
1173
1174        let mut hasher = Koopman32::new();
1175        hasher.update(&TEST_DATA[..4]);
1176        hasher.update(&TEST_DATA[4..]);
1177        let streaming = hasher.finalize();
1178
1179        assert_eq!(full, streaming);
1180    }
1181
1182    #[test]
1183    fn test_seed_affects_result() {
1184        let result0 = koopman16(TEST_DATA, 0);
1185        let result1 = koopman16(TEST_DATA, 1);
1186        assert_ne!(result0, result1);
1187    }
1188
1189    #[test]
1190    fn test_single_bit_detection() {
1191        let original = koopman16(TEST_DATA, 0);
1192
1193        for i in 0..TEST_DATA.len() {
1194            for bit in 0..8 {
1195                let mut corrupted = TEST_DATA.to_vec();
1196                corrupted[i] ^= 1 << bit;
1197                let corrupted_checksum = koopman16(&corrupted, 0);
1198                assert_ne!(original, corrupted_checksum,
1199                    "Failed to detect single bit flip at byte {} bit {}", i, bit);
1200            }
1201        }
1202    }
1203
1204    #[test]
1205    fn test_reference_calculation() {
1206        // Input: [0x12, 0x34, 0x56] with initial seed 0, modulus 253
1207        // Step 1: sum = 0x12 = 18
1208        // Step 2: sum = ((18 << 8) + 0x34) % 253 = 4660 % 253 = 106
1209        // Step 3: sum = ((106 << 8) + 0x56) % 253 = 27222 % 253 = 151
1210        // Final:  sum = (151 << 8) % 253 = 38656 % 253 = 200
1211
1212        let data = [0x12u8, 0x34, 0x56];
1213        let result = koopman8(&data, 0);
1214        assert_eq!(result, 200);
1215    }
1216
1217    // ========================================================================
1218    // Additional tests for parity variants
1219    // ========================================================================
1220
1221    #[test]
1222    fn test_koopman8p_parity_correctness() {
1223        // Verify that the parity bit correctly reflects the parity of data bytes only
1224        // (per the reference C implementation, checksum is NOT included in parity)
1225        let data = b"Test";
1226        let result = koopman8p(data, 0);
1227
1228        // The checksum is in upper 7 bits
1229        let _checksum = result >> 1;
1230        let parity_bit = result & 1;
1231
1232        // Compute expected parity: XOR all data bytes (NOT including checksum)
1233        let mut expected_parity: u8 = 0;
1234        for &byte in data {
1235            expected_parity ^= byte;
1236        }
1237        let expected_parity_bit = expected_parity.count_ones() & 1;
1238
1239        assert_eq!(parity_bit as u32, expected_parity_bit);
1240    }
1241
1242    #[test]
1243    fn test_parity_variants_detect_single_bit_errors() {
1244        let data = b"Test";
1245        let original = koopman16p(data, 0);
1246
1247        for i in 0..data.len() {
1248            for bit in 0..8 {
1249                let mut corrupted = data.to_vec();
1250                corrupted[i] ^= 1 << bit;
1251                let corrupted_checksum = koopman16p(&corrupted, 0);
1252                assert_ne!(original, corrupted_checksum,
1253                    "Failed to detect single bit flip at byte {} bit {}", i, bit);
1254            }
1255        }
1256    }
1257
1258    // ========================================================================
1259    // Tests for custom moduli
1260    // ========================================================================
1261
1262    #[test]
1263    fn test_custom_modulus_8() {
1264        const MODULUS_8_ALT: u32 = 239;
1265        let data = b"test";
1266        let result1 = koopman8_with_modulus(data, 0, NONZERO_MODULUS_8);
1267        let modulus_alt = NonZeroU32::new(MODULUS_8_ALT).unwrap();
1268        let result2 = koopman8_with_modulus(data, 0, modulus_alt);
1269
1270        // Different moduli should (usually) produce different results
1271        // Note: They could theoretically be equal, but very unlikely
1272        assert_ne!(result1, result2);
1273    }
1274
1275    #[test]
1276    fn test_custom_modulus_matches_default() {
1277        let data = b"test data";
1278
1279        assert_eq!(
1280            koopman8(data, 0),
1281            koopman8_with_modulus(data, 0, NONZERO_MODULUS_8)
1282        );
1283        assert_eq!(
1284            koopman16(data, 0),
1285            koopman16_with_modulus(data, 0, NONZERO_MODULUS_16)
1286        );
1287        assert_eq!(
1288            koopman32(data, 0),
1289            koopman32_with_modulus(data, 0, NONZERO_MODULUS_32)
1290        );
1291    }
1292
1293    #[test]
1294    fn test_parity_custom_modulus_matches_default() {
1295        let data = b"test data";
1296
1297        assert_eq!(
1298            koopman8p(data, 0),
1299            koopman8p_with_modulus(data, 0, NONZERO_MODULUS_7P)
1300        );
1301        assert_eq!(
1302            koopman16p(data, 0),
1303            koopman16p_with_modulus(data, 0, NONZERO_MODULUS_15P)
1304        );
1305        assert_eq!(
1306            koopman32p(data, 0),
1307            koopman32p_with_modulus(data, 0, NONZERO_MODULUS_31P)
1308        );
1309    }
1310
1311    #[test]
1312    fn test_streaming_with_seed() {
1313        let data = b"test data";
1314        let seed = 42u8;
1315
1316        // One-shot with seed
1317        let expected = koopman16(data, seed);
1318
1319        // Streaming with seed
1320        let mut hasher = Koopman16::with_seed(seed);
1321        hasher.update(data);
1322        let streaming = hasher.finalize();
1323
1324        assert_eq!(expected, streaming);
1325    }
1326
1327    #[test]
1328    fn test_streaming_with_seed_chunked() {
1329        let data = b"test data for chunked processing";
1330        let seed = 123u8;
1331
1332        let expected = koopman16(data, seed);
1333
1334        let mut hasher = Koopman16::with_seed(seed);
1335        hasher.update(&data[..10]);
1336        hasher.update(&data[10..20]);
1337        hasher.update(&data[20..]);
1338        let streaming = hasher.finalize();
1339
1340        assert_eq!(expected, streaming);
1341    }
1342
1343    // ========================================================================
1344    // Tests for reset behavior
1345    // ========================================================================
1346
1347    #[test]
1348    fn test_reset_without_seed() {
1349        let data = b"test";
1350
1351        let mut hasher = Koopman16::new();
1352        hasher.update(data);
1353        let first = hasher.finalize();
1354
1355        let mut hasher = Koopman16::new();
1356        hasher.update(b"other data");
1357        hasher.reset();
1358        hasher.update(data);
1359        let after_reset = hasher.finalize();
1360
1361        assert_eq!(first, after_reset);
1362    }
1363
1364    #[test]
1365    fn test_reset_preserves_seed() {
1366        let data = b"test";
1367        let seed = 42u8;
1368
1369        // First computation with seed
1370        let mut hasher = Koopman16::with_seed(seed);
1371        hasher.update(data);
1372        let first = hasher.finalize();
1373
1374        // Computation after reset should produce same result
1375        let mut hasher = Koopman16::with_seed(seed);
1376        hasher.update(b"garbage data");
1377        hasher.reset();
1378        hasher.update(data);
1379        let after_reset = hasher.finalize();
1380
1381        assert_eq!(first, after_reset);
1382    }
1383
1384    #[test]
1385    fn test_reset_all_variants() {
1386        let data = b"test";
1387
1388        // Koopman8
1389        let mut h8 = Koopman8::with_seed(10);
1390        h8.update(b"junk");
1391        h8.reset();
1392        h8.update(data);
1393        assert_eq!(h8.finalize(), koopman8(data, 10));
1394
1395        // Koopman16
1396        let mut h16 = Koopman16::with_seed(20);
1397        h16.update(b"junk");
1398        h16.reset();
1399        h16.update(data);
1400        assert_eq!(h16.finalize(), koopman16(data, 20));
1401
1402        // Koopman32
1403        let mut h32 = Koopman32::with_seed(30);
1404        h32.update(b"junk");
1405        h32.reset();
1406        h32.update(data);
1407        assert_eq!(h32.finalize(), koopman32(data, 30));
1408    }
1409
1410    // ========================================================================
1411    // Tests for two-bit error detection
1412    // ========================================================================
1413
1414    #[test]
1415    fn test_two_bit_error_detection() {
1416        // Test that most two-bit errors are detected
1417        // Note: HD=3 means we detect ALL 1-bit and 2-bit errors
1418        let data = b"Test";
1419        let original = koopman16(data, 0);
1420        let mut detected = 0;
1421        let mut total = 0;
1422
1423        for i in 0..data.len() {
1424            for j in i..data.len() {
1425                for bit_i in 0..8 {
1426                    for bit_j in 0..8 {
1427                        if i == j && bit_i == bit_j {
1428                            continue; // Skip single-bit errors
1429                        }
1430                        total += 1;
1431                        let mut corrupted = data.to_vec();
1432                        corrupted[i] ^= 1 << bit_i;
1433                        corrupted[j] ^= 1 << bit_j;
1434                        if koopman16(&corrupted, 0) != original {
1435                            detected += 1;
1436                        }
1437                    }
1438                }
1439            }
1440        }
1441
1442        // Should detect all two-bit errors for data within HD=3 length
1443        assert_eq!(detected, total, "Should detect all two-bit errors");
1444    }
1445
1446    // ========================================================================
1447    // Tests for streaming API edge cases
1448    // ========================================================================
1449
1450    #[test]
1451    fn test_streaming_empty_updates() {
1452        let data = b"test";
1453
1454        let mut hasher = Koopman16::new();
1455        hasher.update(&[]);  // Empty update
1456        hasher.update(data);
1457        hasher.update(&[]);  // Another empty update
1458
1459        assert_eq!(hasher.finalize(), koopman16(data, 0));
1460    }
1461
1462    #[test]
1463    fn test_streaming_byte_by_byte() {
1464        let data = b"test data";
1465
1466        let mut hasher = Koopman16::new();
1467        for &byte in data {
1468            hasher.update(&[byte]);
1469        }
1470
1471        assert_eq!(hasher.finalize(), koopman16(data, 0));
1472    }
1473
1474    #[test]
1475    fn test_finalize_without_data() {
1476        let hasher = Koopman16::new();
1477        assert_eq!(hasher.finalize(), 0);
1478
1479        let hasher_with_seed = Koopman16::with_seed(42);
1480        assert_eq!(hasher_with_seed.finalize(), 0);
1481    }
1482
1483    #[test]
1484    fn test_streaming_parity_koopman8p() {
1485        let data = b"test";
1486        let expected = koopman8p(data, 0);
1487
1488        let mut hasher = Koopman8P::new();
1489        hasher.update(&data[..2]);
1490        hasher.update(&data[2..]);
1491        let streaming = hasher.finalize();
1492
1493        assert_eq!(expected, streaming);
1494    }
1495
1496    #[test]
1497    fn test_streaming_parity_koopman16p() {
1498        let data = b"test data";
1499        let expected = koopman16p(data, 0);
1500
1501        let mut hasher = Koopman16P::new();
1502        hasher.update(&data[..4]);
1503        hasher.update(&data[4..]);
1504        let streaming = hasher.finalize();
1505
1506        assert_eq!(expected, streaming);
1507    }
1508
1509    #[test]
1510    fn test_streaming_parity_koopman32p() {
1511        let data = b"test data for streaming";
1512        let expected = koopman32p(data, 0);
1513
1514        let mut hasher = Koopman32P::new();
1515        hasher.update(&data[..10]);
1516        hasher.update(&data[10..]);
1517        let streaming = hasher.finalize();
1518
1519        assert_eq!(expected, streaming);
1520    }
1521
1522    #[test]
1523    fn test_streaming_parity_with_seed() {
1524        let data = b"test";
1525        let seed = 42u8;
1526
1527        let expected = koopman16p(data, seed);
1528
1529        let mut hasher = Koopman16P::with_seed(seed);
1530        hasher.update(data);
1531        let streaming = hasher.finalize();
1532
1533        assert_eq!(expected, streaming);
1534    }
1535
1536    // ========================================================================
1537    // Tests for parity verification
1538    // ========================================================================
1539
1540    #[test]
1541    fn test_verify_parity() {
1542        let data = b"test data";
1543
1544        let cs8p = koopman8p(data, 0);
1545        assert!(verify8p(data, cs8p, 0));
1546        assert!(!verify8p(data, cs8p.wrapping_add(1), 0));
1547
1548        let cs16p = koopman16p(data, 0);
1549        assert!(verify16p(data, cs16p, 0));
1550        assert!(!verify16p(data, cs16p.wrapping_add(1), 0));
1551
1552        let cs32p = koopman32p(data, 0);
1553        assert!(verify32p(data, cs32p, 0));
1554        assert!(!verify32p(data, cs32p.wrapping_add(1), 0));
1555    }
1556
1557    // ========================================================================
1558    // Tests for streaming with custom modulus
1559    // ========================================================================
1560
1561    #[test]
1562    fn test_streaming_with_custom_modulus() {
1563        let data = b"test data";
1564
1565        // Test that streaming with default modulus matches one-shot
1566        let mut hasher = Koopman16::with_modulus(NONZERO_MODULUS_16);
1567        hasher.update(data);
1568        assert_eq!(hasher.finalize(), koopman16(data, 0));
1569
1570        // Test with a different modulus
1571        let alt_modulus = NonZeroU32::new(32749).unwrap();
1572        let mut hasher = Koopman16::with_modulus(alt_modulus);
1573        hasher.update(data);
1574        let streaming = hasher.finalize();
1575
1576        // Should produce a valid result (just verify it's deterministic)
1577        let mut hasher2 = Koopman16::with_modulus(alt_modulus);
1578        hasher2.update(data);
1579        assert_eq!(streaming, hasher2.finalize());
1580    }
1581}