1pub const SEED: u64 = 0x4D4B_4954_4643_4443;
24
25pub const MIN_SIZE: usize = 0x4000;
28pub const AVG_SIZE: usize = 0x10000;
31pub const MAX_SIZE: usize = 0x40000;
33
34pub const MASK_S: u64 = 0x0001_FFFF;
37pub const MASK_L: u64 = 0x0000_7FFF;
40
41fn gear_table() -> &'static [u64; 256] {
45 use std::sync::OnceLock;
46 static TABLE: OnceLock<[u64; 256]> = OnceLock::new();
47 TABLE.get_or_init(build_gear_table)
48}
49
50fn build_gear_table() -> [u64; 256] {
51 let mut state: u64 = SEED;
52 let mut table = [0u64; 256];
53 for entry in &mut table {
54 state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
56 let mut z = state;
57 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
58 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
59 z ^= z >> 31;
60 *entry = z;
61 }
62 table
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct ChunkBoundary {
68 pub offset: usize,
69 pub length: usize,
70}
71
72#[derive(Debug, Clone, Copy)]
76pub struct FastCdc {
77 min_size: usize,
78 avg_size: usize,
79 max_size: usize,
80 mask_s: u64,
81 mask_l: u64,
82}
83
84impl FastCdc {
85 #[must_use]
88 pub const fn v1() -> Self {
89 Self {
90 min_size: MIN_SIZE,
91 avg_size: AVG_SIZE,
92 max_size: MAX_SIZE,
93 mask_s: MASK_S,
94 mask_l: MASK_L,
95 }
96 }
97
98 #[must_use]
111 pub fn custom(min: usize, avg: usize, max: usize) -> Self {
112 assert!(min < avg && avg < max, "FastCdc: require min<avg<max");
113 assert!(avg.is_power_of_two(), "FastCdc: avg must be a power of 2");
114 let bits = avg.trailing_zeros();
115 let mask: u64 = (1u64 << bits) - 1;
116 Self {
117 min_size: min,
118 avg_size: avg,
119 max_size: max,
120 mask_s: mask | (mask << 1),
121 mask_l: mask >> 1,
122 }
123 }
124
125 #[must_use]
126 pub const fn min_size(&self) -> usize {
127 self.min_size
128 }
129 #[must_use]
130 pub const fn avg_size(&self) -> usize {
131 self.avg_size
132 }
133 #[must_use]
134 pub const fn max_size(&self) -> usize {
135 self.max_size
136 }
137
138 #[must_use]
146 pub fn cut(&self, data: &[u8]) -> usize {
147 if data.len() <= self.min_size {
148 return data.len();
149 }
150 let table = gear_table();
151 let mut hash: u64 = 0;
152 let avg_end = self.avg_size.min(data.len());
153 let mut i = self.min_size;
154 while i < avg_end {
155 hash = (hash << 1).wrapping_add(table[data[i] as usize]);
156 if (hash & self.mask_s) == 0 {
157 return i;
158 }
159 i += 1;
160 }
161 let max_end = self.max_size.min(data.len());
162 while i < max_end {
163 hash = (hash << 1).wrapping_add(table[data[i] as usize]);
164 if (hash & self.mask_l) == 0 {
165 return i;
166 }
167 i += 1;
168 }
169 max_end
170 }
171}
172
173#[derive(Debug)]
175pub struct ChunkIterator<'a> {
176 cdc: FastCdc,
177 data: &'a [u8],
178 offset: usize,
179}
180
181impl<'a> ChunkIterator<'a> {
182 #[must_use]
183 pub fn new(cdc: FastCdc, data: &'a [u8]) -> Self {
184 Self {
185 cdc,
186 data,
187 offset: 0,
188 }
189 }
190}
191
192impl Iterator for ChunkIterator<'_> {
193 type Item = ChunkBoundary;
194 fn next(&mut self) -> Option<Self::Item> {
195 if self.offset >= self.data.len() {
196 return None;
197 }
198 let remaining = &self.data[self.offset..];
199 let length = self.cdc.cut(remaining);
200 let boundary = ChunkBoundary {
201 offset: self.offset,
202 length,
203 };
204 self.offset += length;
205 Some(boundary)
206 }
207}
208
209const READ_SCRATCH_SIZE: usize = 64 * 1024;
225
226#[derive(Debug)]
227pub struct ChunkReader<R> {
228 cdc: FastCdc,
229 reader: R,
230 buf: Vec<u8>,
231 scratch: Vec<u8>,
232 eof: bool,
233}
234
235impl<R: std::io::Read> ChunkReader<R> {
236 #[must_use]
237 pub fn new(cdc: FastCdc, reader: R) -> Self {
238 Self {
239 cdc,
240 reader,
241 buf: Vec::new(),
242 scratch: vec![0u8; READ_SCRATCH_SIZE],
243 eof: false,
244 }
245 }
246
247 fn fill(&mut self) -> std::io::Result<()> {
252 let target = self.cdc.max_size();
253 while !self.eof && self.buf.len() < target {
254 let n = self.reader.read(&mut self.scratch)?;
255 if n == 0 {
256 self.eof = true;
257 } else {
258 self.buf.extend_from_slice(&self.scratch[..n]);
259 }
260 }
261 Ok(())
262 }
263
264 pub fn next_chunk(&mut self) -> std::io::Result<Option<Vec<u8>>> {
271 self.fill()?;
272 if self.buf.is_empty() {
273 return Ok(None);
274 }
275 let length = self.cdc.cut(&self.buf);
276 let rest = self.buf.split_off(length);
277 let chunk = std::mem::replace(&mut self.buf, rest);
278 Ok(Some(chunk))
279 }
280}
281
282#[must_use]
288pub fn chunk_boundaries(data: &[u8]) -> Vec<usize> {
289 let cdc = FastCdc::v1();
290 let mut out = Vec::new();
291 let mut offset = 0usize;
292 while offset < data.len() {
293 let len = cdc.cut(&data[offset..]);
294 offset += len;
295 out.push(offset);
296 }
297 out
298}
299
300#[must_use]
304pub fn gear_table_digest() -> [u8; 32] {
305 let table = gear_table();
306 let mut bytes = [0u8; 256 * 8];
307 for (i, v) in table.iter().enumerate() {
308 bytes[i * 8..i * 8 + 8].copy_from_slice(&v.to_le_bytes());
309 }
310 crate::hash::hash(&bytes)
311}
312
313#[cfg(test)]
318mod tests {
319 use super::*;
320
321 struct Prng(u64);
325 impl Prng {
326 fn new(seed: u64) -> Self {
327 Self(seed.max(1))
328 }
329 fn next_u64(&mut self) -> u64 {
330 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
333 let mut z = self.0;
334 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
335 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
336 z ^ (z >> 31)
337 }
338 fn fill(&mut self, dst: &mut [u8]) {
339 for chunk in dst.chunks_mut(8) {
340 let bytes = self.next_u64().to_le_bytes();
341 let n = chunk.len();
342 chunk.copy_from_slice(&bytes[..n]);
343 }
344 }
345 }
346
347 #[test]
348 fn gear_table_is_unique_and_nonzero() {
349 let table = gear_table();
350 let mut seen = std::collections::HashSet::new();
351 for &v in table {
352 assert_ne!(v, 0, "gear table entry is zero");
353 assert!(seen.insert(v), "duplicate gear table entry");
354 }
355 assert_eq!(seen.len(), 256);
356 }
357
358 #[test]
359 fn min_max_size_constraints() {
360 let cdc = FastCdc::v1();
361 let mut data = vec![0u8; 512 * 1024];
362 Prng::new(0xCAFE_BABE).fill(&mut data);
363
364 let mut total = 0usize;
365 let mut count = 0usize;
366 for b in ChunkIterator::new(cdc, &data) {
367 assert!(b.length <= MAX_SIZE);
368 if b.offset + b.length < data.len() {
370 assert!(b.length >= MIN_SIZE, "chunk under MIN_SIZE: {}", b.length);
371 }
372 total += b.length;
373 count += 1;
374 }
375 assert_eq!(total, data.len());
376 assert!(count > 1);
377 }
378
379 #[test]
380 fn small_input_is_single_chunk() {
381 let cdc = FastCdc::v1();
382 let small = b"hello, this is a tiny file";
383 assert_eq!(cdc.cut(small), small.len());
384 let boundaries: Vec<_> = ChunkIterator::new(cdc, small).collect();
385 assert_eq!(boundaries.len(), 1);
386 assert_eq!(boundaries[0].offset, 0);
387 assert_eq!(boundaries[0].length, small.len());
388 }
389
390 #[test]
391 fn empty_input_iterator_is_empty() {
392 let cdc = FastCdc::v1();
393 assert_eq!(cdc.cut(b""), 0);
394 let none: Vec<_> = ChunkIterator::new(cdc, b"").collect();
395 assert!(none.is_empty());
396 }
397
398 #[test]
399 fn cut_at_exactly_min_size_returns_full() {
400 let cdc = FastCdc::custom(1024, 4096, 16384);
401 let mut data = vec![0u8; 1024];
402 Prng::new(99).fill(&mut data);
403 assert_eq!(cdc.cut(&data), data.len());
404 }
405
406 #[test]
407 fn cut_forces_boundary_at_max_size() {
408 let cdc = FastCdc::custom(4, 8, 16);
411 let data = [0u8; 64];
412 let len = cdc.cut(&data);
413 assert!(len <= 16, "cut returned {len} > max=16");
414 }
415
416 #[test]
417 fn boundary_stability_single_byte_insert() {
418 let cdc = FastCdc::v1();
420 let mut original = vec![0u8; 64 * 1024];
421 Prng::new(0xBEEF).fill(&mut original);
422 let insert_point = 32 * 1024;
423 let mut modified = Vec::with_capacity(original.len() + 1);
424 modified.extend_from_slice(&original[..insert_point]);
425 modified.push(0xFF);
426 modified.extend_from_slice(&original[insert_point..]);
427
428 let orig_chunks: Vec<_> = ChunkIterator::new(cdc, &original).collect();
429 let mod_chunks: Vec<_> = ChunkIterator::new(cdc, &modified).collect();
430
431 let max_chunks = orig_chunks.len().max(mod_chunks.len());
432 let mut differing = 0usize;
433 for i in 0..max_chunks {
434 match (orig_chunks.get(i), mod_chunks.get(i)) {
435 (Some(o), Some(m)) => {
436 let os = &original[o.offset..o.offset + o.length];
437 let ms = &modified[m.offset..m.offset + m.length];
438 if os != ms {
439 differing += 1;
440 }
441 }
442 _ => differing += 1,
443 }
444 }
445 assert!(
446 differing <= 3,
447 "expected <=3 differing chunks, got {differing}"
448 );
449 }
450
451 #[test]
452 fn different_avg_size_yields_different_boundaries() {
453 let mut data = vec![0u8; 100 * 1024];
454 Prng::new(0xABCD).fill(&mut data);
455 let small = FastCdc::custom(8 * 1024, 32 * 1024, 128 * 1024);
456 let large = FastCdc::v1();
457
458 let s: Vec<_> = ChunkIterator::new(small, &data)
459 .map(|b| b.offset + b.length)
460 .collect();
461 let l: Vec<_> = ChunkIterator::new(large, &data)
462 .map(|b| b.offset + b.length)
463 .collect();
464 assert!(
465 s != l,
466 "expected different boundaries for different avg_size"
467 );
468 }
469
470 #[test]
471 fn chunk_boundaries_helper_matches_iterator() {
472 let mut data = vec![0u8; 200 * 1024];
473 Prng::new(0xFEED_FACE).fill(&mut data);
474 let from_helper = chunk_boundaries(&data);
475 let from_iter: Vec<usize> = ChunkIterator::new(FastCdc::v1(), &data)
476 .map(|b| b.offset + b.length)
477 .collect();
478 assert_eq!(from_helper, from_iter);
479 }
480
481 #[test]
482 fn streaming_matches_in_memory_iterator() {
483 let mut data = vec![0u8; 500 * 1024];
484 Prng::new(0x5713_EA31).fill(&mut data);
485
486 let in_memory: Vec<Vec<u8>> = ChunkIterator::new(FastCdc::v1(), &data)
487 .map(|b| data[b.offset..b.offset + b.length].to_vec())
488 .collect();
489
490 let mut reader = ChunkReader::new(FastCdc::v1(), std::io::Cursor::new(&data));
491 let mut streamed = Vec::new();
492 while let Some(chunk) = reader.next_chunk().unwrap() {
493 streamed.push(chunk);
494 }
495
496 assert_eq!(in_memory, streamed);
497 }
498
499 #[test]
500 fn streaming_empty_input_yields_no_chunks() {
501 let mut reader = ChunkReader::new(FastCdc::v1(), std::io::Cursor::new(&[] as &[u8]));
502 assert!(reader.next_chunk().unwrap().is_none());
503 }
504
505 #[test]
506 fn streaming_small_input_is_single_chunk() {
507 let small = b"hello, this is a tiny file";
508 let mut reader = ChunkReader::new(FastCdc::v1(), std::io::Cursor::new(&small[..]));
509 let chunk = reader.next_chunk().unwrap().expect("one chunk");
510 assert_eq!(chunk, small);
511 assert!(reader.next_chunk().unwrap().is_none());
512 }
513
514 struct StingyReader<'a> {
518 data: &'a [u8],
519 pos: usize,
520 }
521 impl std::io::Read for StingyReader<'_> {
522 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
523 let n = (self.data.len() - self.pos).min(buf.len()).min(7);
524 buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
525 self.pos += n;
526 Ok(n)
527 }
528 }
529
530 #[test]
531 fn streaming_reader_buffer_stays_bounded_regardless_of_input_size() {
532 let mut data = vec![0u8; 8 * 1024 * 1024];
538 Prng::new(0x8000_0008).fill(&mut data);
539 let mut reader = ChunkReader::new(FastCdc::v1(), std::io::Cursor::new(&data));
540 let mut chunk_count = 0usize;
541 while let Some(_chunk) = reader.next_chunk().unwrap() {
542 assert!(
543 reader.buf.len() <= MAX_SIZE + READ_SCRATCH_SIZE,
544 "internal buffer grew to {} bytes, expected <= {}",
545 reader.buf.len(),
546 MAX_SIZE + READ_SCRATCH_SIZE
547 );
548 chunk_count += 1;
549 }
550 assert!(chunk_count > 1, "expected multiple chunks from 8 MiB input");
551 }
552
553 #[test]
554 fn streaming_matches_in_memory_iterator_under_short_reads() {
555 let mut data = vec![0u8; 300 * 1024];
556 Prng::new(0x5713_EA32).fill(&mut data);
557
558 let in_memory: Vec<Vec<u8>> = ChunkIterator::new(FastCdc::v1(), &data)
559 .map(|b| data[b.offset..b.offset + b.length].to_vec())
560 .collect();
561
562 let mut reader = ChunkReader::new(
563 FastCdc::v1(),
564 StingyReader {
565 data: &data,
566 pos: 0,
567 },
568 );
569 let mut streamed = Vec::new();
570 while let Some(chunk) = reader.next_chunk().unwrap() {
571 streamed.push(chunk);
572 }
573
574 assert_eq!(in_memory, streamed);
575 }
576
577 const EXPECTED_GEAR_DIGEST_HEX: &str =
580 "7b238963a8bb10c4dea1bf678aa07d8c3ce94284209c440ca971ff3a97ee5ad4";
581
582 #[test]
583 fn gear_table_digest_is_stable() {
584 let hex = crate::hash::to_hex(&gear_table_digest());
587 assert_eq!(
588 hex, EXPECTED_GEAR_DIGEST_HEX,
589 "gear table digest changed; refuse to drift silently"
590 );
591 }
592
593 proptest::proptest! {
600 #[test]
605 fn proptest_determinism(data in proptest::collection::vec(proptest::num::u8::ANY, 0..256 * 1024)) {
606 let cdc = FastCdc::v1();
607 let pass1: Vec<_> = ChunkIterator::new(cdc, &data).collect();
608 let pass2: Vec<_> = ChunkIterator::new(cdc, &data).collect();
609 proptest::prop_assert_eq!(pass1, pass2);
610 }
611
612 #[test]
615 fn proptest_boundaries_partition_input(
616 data in proptest::collection::vec(proptest::num::u8::ANY, 0..256 * 1024),
617 ) {
618 let cdc = FastCdc::v1();
619 let boundaries: Vec<_> = ChunkIterator::new(cdc, &data).collect();
620 let mut expected_offset = 0usize;
621 for b in &boundaries {
622 proptest::prop_assert_eq!(b.offset, expected_offset);
623 expected_offset += b.length;
624 }
625 proptest::prop_assert_eq!(expected_offset, data.len());
626 }
627
628 #[test]
634 fn proptest_streaming_matches_in_memory(
635 data in proptest::collection::vec(proptest::num::u8::ANY, 0..256 * 1024),
636 ) {
637 let cdc = FastCdc::v1();
638 let in_memory: Vec<Vec<u8>> = ChunkIterator::new(cdc, &data)
639 .map(|b| data[b.offset..b.offset + b.length].to_vec())
640 .collect();
641
642 let mut reader = ChunkReader::new(cdc, std::io::Cursor::new(&data));
643 let mut streamed = Vec::new();
644 while let Some(chunk) = reader.next_chunk().unwrap() {
645 streamed.push(chunk);
646 }
647 proptest::prop_assert_eq!(in_memory, streamed);
648 }
649 }
650}