1mod parallel;
33
34pub use parallel::ParallelFastCDC;
35
36use std::io::Read;
37
38pub const DEFAULT_MIN_SIZE: usize = 64 * 1024;
40pub const DEFAULT_AVG_SIZE: usize = 256 * 1024;
42pub const DEFAULT_MAX_SIZE: usize = 1024 * 1024;
44
45const READ_BUFFER_SIZE: usize = 64 * 1024;
47
48pub trait Chunker: Send + Sync {
58 fn chunk_slice<'a>(&self, data: &'a [u8]) -> Vec<&'a [u8]>;
61
62 fn chunk_reader(&self, reader: &mut dyn Read) -> std::io::Result<Vec<Vec<u8>>>;
74
75 fn avg_chunk_size(&self) -> usize;
77}
78
79#[derive(Clone, Debug)]
89pub struct FastCDC {
90 min_size: usize,
91 avg_size: usize,
92 max_size: usize,
93 mask1: u64,
94 mask2: u64,
95 gear: GearTable,
96}
97
98impl Default for FastCDC {
99 fn default() -> Self {
100 Self::new(DEFAULT_MIN_SIZE, DEFAULT_AVG_SIZE, DEFAULT_MAX_SIZE)
101 .expect("default sizes are valid")
102 }
103}
104
105impl FastCDC {
106 pub fn new(min_size: usize, avg_size: usize, max_size: usize) -> Result<Self, &'static str> {
115 if min_size == 0 {
116 return Err("min_size must be > 0");
117 }
118 if min_size >= avg_size {
119 return Err("min_size must be < avg_size");
120 }
121 if avg_size >= max_size {
122 return Err("avg_size must be < max_size");
123 }
124 let mask1 = mask_for(avg_size - min_size);
125 let mask2 = mask_for(max_size - avg_size);
126 Ok(Self {
127 min_size,
128 avg_size,
129 max_size,
130 mask1,
131 mask2,
132 gear: GearTable::default(),
133 })
134 }
135
136 #[must_use]
138 pub const fn min_size(&self) -> usize {
139 self.min_size
140 }
141
142 #[must_use]
144 pub const fn avg_size(&self) -> usize {
145 self.avg_size
146 }
147
148 #[must_use]
150 pub const fn max_size(&self) -> usize {
151 self.max_size
152 }
153
154 #[must_use]
161 pub fn chunk_slice<'a>(&self, data: &'a [u8]) -> Vec<&'a [u8]> {
162 let mut chunks = Vec::new();
163 let mut start = 0;
164 while start < data.len() {
165 let end = self.find_boundary(data, start);
166 chunks.push(&data[start..end]);
167 start = end;
168 }
169 chunks
170 }
171
172 pub fn chunk_reader<R: Read>(&self, mut reader: R) -> std::io::Result<Vec<Vec<u8>>> {
180 let mut buffer: Vec<u8> = Vec::with_capacity(self.max_size + READ_BUFFER_SIZE);
181 let mut read_buf = vec![0u8; READ_BUFFER_SIZE];
182 let mut fp: u64 = 0;
183 let mut chunk_start: usize = 0;
184 let mut i: usize = 0;
185 let mut chunks: Vec<Vec<u8>> = Vec::new();
186
187 loop {
188 let n = reader.read(&mut read_buf)?;
189 if n == 0 {
190 break;
191 }
192 buffer.extend_from_slice(&read_buf[..n]);
193
194 while i < buffer.len() {
195 let pos_in_chunk = i - chunk_start;
196 if pos_in_chunk >= self.max_size {
197 chunks.push(buffer[chunk_start..i].to_vec());
198 chunk_start = i;
199 fp = 0;
200 continue;
201 }
202 if pos_in_chunk < self.min_size {
203 i += 1;
204 continue;
205 }
206 fp = (fp << 1).wrapping_add(self.gear.bytes[usize::from(buffer[i])]);
207 let mask = if pos_in_chunk < self.avg_size {
208 self.mask1
209 } else {
210 self.mask2
211 };
212 if fp & mask == 0 {
213 i += 1;
214 chunks.push(buffer[chunk_start..i].to_vec());
215 chunk_start = i;
216 fp = 0;
217 continue;
218 }
219 i += 1;
220 }
221 }
222
223 if chunk_start < buffer.len() {
224 chunks.push(buffer[chunk_start..].to_vec());
225 }
226 Ok(chunks)
227 }
228
229 fn find_boundary(&self, data: &[u8], start: usize) -> usize {
232 let data_len = data.len();
233 if data_len <= start {
234 return start;
235 }
236 let max_end = (start + self.max_size).min(data_len);
237 if max_end - start <= self.min_size {
238 return max_end;
239 }
240
241 let mut fp: u64 = 0;
242 let avg_end = (start + self.avg_size).min(max_end);
243 let gear = &self.gear.bytes;
244
245 let mut i = start + self.min_size;
254 let unroll_end1 = i + ((avg_end - i) / 4) * 4;
255 while i < unroll_end1 {
256 let b0 = usize::from(data[i]);
257 let b1 = usize::from(data[i + 1]);
258 let b2 = usize::from(data[i + 2]);
259 let b3 = usize::from(data[i + 3]);
260 let g0 = gear[b0];
261 let g1 = gear[b1];
262 let g2 = gear[b2];
263 let g3 = gear[b3];
264 fp = (fp << 1).wrapping_add(g0);
265 if fp & self.mask1 == 0 {
266 return i + 1;
267 }
268 fp = (fp << 1).wrapping_add(g1);
269 if fp & self.mask1 == 0 {
270 return i + 2;
271 }
272 fp = (fp << 1).wrapping_add(g2);
273 if fp & self.mask1 == 0 {
274 return i + 3;
275 }
276 fp = (fp << 1).wrapping_add(g3);
277 if fp & self.mask1 == 0 {
278 return i + 4;
279 }
280 i += 4;
281 }
282 while i < avg_end {
283 fp = (fp << 1).wrapping_add(gear[usize::from(data[i])]);
284 if fp & self.mask1 == 0 {
285 return i + 1;
286 }
287 i += 1;
288 }
289
290 let unroll_end2 = i + ((max_end - i) / 4) * 4;
292 while i < unroll_end2 {
293 let b0 = usize::from(data[i]);
294 let b1 = usize::from(data[i + 1]);
295 let b2 = usize::from(data[i + 2]);
296 let b3 = usize::from(data[i + 3]);
297 let g0 = gear[b0];
298 let g1 = gear[b1];
299 let g2 = gear[b2];
300 let g3 = gear[b3];
301 fp = (fp << 1).wrapping_add(g0);
302 if fp & self.mask2 == 0 {
303 return i + 1;
304 }
305 fp = (fp << 1).wrapping_add(g1);
306 if fp & self.mask2 == 0 {
307 return i + 2;
308 }
309 fp = (fp << 1).wrapping_add(g2);
310 if fp & self.mask2 == 0 {
311 return i + 3;
312 }
313 fp = (fp << 1).wrapping_add(g3);
314 if fp & self.mask2 == 0 {
315 return i + 4;
316 }
317 i += 4;
318 }
319 while i < max_end {
320 fp = (fp << 1).wrapping_add(gear[usize::from(data[i])]);
321 if fp & self.mask2 == 0 {
322 return i + 1;
323 }
324 i += 1;
325 }
326 max_end
327 }
328}
329
330impl Chunker for FastCDC {
331 fn chunk_slice<'a>(&self, data: &'a [u8]) -> Vec<&'a [u8]> {
332 FastCDC::chunk_slice(self, data)
333 }
334
335 fn chunk_reader(&self, reader: &mut dyn Read) -> std::io::Result<Vec<Vec<u8>>> {
336 FastCDC::chunk_reader(self, reader)
337 }
338
339 fn avg_chunk_size(&self) -> usize {
340 self.avg_size
341 }
342}
343
344fn mask_for(range: usize) -> u64 {
350 if range == 0 {
351 return 0;
352 }
353 let bits = (64 - range.leading_zeros()).saturating_sub(1).max(1);
354 (1u64 << bits) - 1
355}
356
357#[derive(Clone, Debug)]
360struct GearTable {
361 bytes: [u64; 256],
362}
363
364impl Default for GearTable {
365 fn default() -> Self {
366 let mut table = [0u64; 256];
367 let mut state: u64 = 0x0123_4567_89AB_CDEF; for entry in &mut table {
369 state = splitmix64(state);
370 *entry = state;
371 }
372 Self { bytes: table }
373 }
374}
375
376fn splitmix64(mut state: u64) -> u64 {
378 state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
379 let mut z = state;
380 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
381 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
382 z ^ (z >> 31)
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use std::collections::HashSet;
389 use std::io::Cursor;
390
391 fn chunk_sizes(chunks: &[&[u8]]) -> Vec<usize> {
392 chunks.iter().map(|c| c.len()).collect()
393 }
394
395 fn pseudo_random_bytes(seed: u64, count: usize) -> Vec<u8> {
398 let mut state = seed;
399 let mut out = Vec::with_capacity(count);
400 for _ in 0..count {
401 state = state
402 .wrapping_mul(6_364_136_223_846_793_005)
403 .wrapping_add(1_442_695_040_888_963_407);
404 out.push(u8::try_from(state >> 56).expect("fits u8"));
405 }
406 out
407 }
408
409 #[test]
410 fn short_input_produces_one_chunk() {
411 let chunker = FastCDC::new(64, 256, 1024).expect("valid sizes");
412 let data = vec![0xAA; 50];
413 let chunks = chunker.chunk_slice(&data);
414 assert_eq!(chunks.len(), 1);
415 assert_eq!(chunks[0].len(), 50);
416 }
417
418 #[test]
419 fn chunks_cover_input_exactly() {
420 let chunker = FastCDC::new(64, 256, 1024).expect("valid sizes");
421 let data: Vec<u8> = (0..5000u32)
422 .map(|i| u8::try_from(i & 0xFF).expect("fits"))
423 .collect();
424 let chunks = chunker.chunk_slice(&data);
425 let total: usize = chunks.iter().map(|c| c.len()).sum();
426 assert_eq!(total, data.len());
427 }
428
429 #[test]
430 fn chunks_respect_min_and_max() {
431 let chunker = FastCDC::new(64, 256, 1024).expect("valid sizes");
432 let data = pseudo_random_bytes(1, 10_000);
433 let chunks = chunker.chunk_slice(&data);
434 for (i, chunk) in chunks.iter().enumerate() {
435 if i + 1 == chunks.len() {
436 continue; }
438 assert!(chunk.len() >= 64, "chunk {i} size {} < min 64", chunk.len());
439 assert!(
440 chunk.len() <= 1024,
441 "chunk {i} size {} > max 1024",
442 chunk.len()
443 );
444 }
445 }
446
447 #[test]
448 fn boundary_shift_one_byte_insert_affects_few_chunks() {
449 let chunker = FastCDC::new(64, 256, 1024).expect("valid sizes");
452 let base = pseudo_random_bytes(7, 10_000);
453 let mut shifted = Vec::with_capacity(base.len() + 1);
454 shifted.push(0xFF);
455 shifted.extend_from_slice(&base);
456
457 let base_chunks = chunker.chunk_slice(&base);
458 let shifted_chunks = chunker.chunk_slice(&shifted);
459
460 let base_starts: HashSet<usize> = std::iter::once(0)
461 .chain(base_chunks.iter().scan(0, |acc, c| {
462 *acc += c.len();
463 Some(*acc)
464 }))
465 .collect();
466 let shifted_starts: HashSet<usize> = std::iter::once(0)
467 .chain(shifted_chunks.iter().scan(0, |acc, c| {
468 *acc += c.len();
469 Some(*acc)
470 }))
471 .collect();
472
473 let shifted_count = shifted_starts
474 .iter()
475 .filter(|&&s| {
476 !base_starts.contains(&s) && !base_starts.contains(&(s.saturating_sub(1)))
477 })
478 .count();
479 let max_shifted_boundaries = 3;
480 assert!(
481 shifted_count <= max_shifted_boundaries,
482 "1-byte insert shifted {shifted_count} boundaries (expected ≤ {max_shifted_boundaries})"
483 );
484 }
485
486 #[test]
487 fn chunk_reader_matches_chunk_slice() {
488 let chunker = FastCDC::new(64, 256, 1024).expect("valid sizes");
489 let data = pseudo_random_bytes(99, 5000);
490 let slice_chunks: Vec<Vec<u8>> = chunker
491 .chunk_slice(&data)
492 .into_iter()
493 .map(Vec::from)
494 .collect();
495 let mut cursor = Cursor::new(&data);
496 let reader_chunks = chunker.chunk_reader(&mut cursor).expect("read succeeds");
497 assert_eq!(slice_chunks, reader_chunks);
498 }
499
500 #[test]
501 fn deterministic_across_instances() {
502 let chunker_a = FastCDC::new(64, 256, 1024).expect("valid");
503 let chunker_b = FastCDC::new(64, 256, 1024).expect("valid");
504 let data: Vec<u8> = (0..1000u32)
505 .map(|i| u8::try_from(i & 0xFF).expect("fits"))
506 .collect();
507 assert_eq!(
508 chunk_sizes(&chunker_a.chunk_slice(&data)),
509 chunk_sizes(&chunker_b.chunk_slice(&data))
510 );
511 }
512
513 #[test]
514 fn rejects_invalid_sizes() {
515 assert!(FastCDC::new(0, 256, 1024).is_err());
516 assert!(FastCDC::new(256, 256, 1024).is_err());
517 assert!(FastCDC::new(64, 64, 64).is_err());
518 assert!(FastCDC::new(64, 256, 100).is_err());
519 }
520
521 #[test]
522 fn default_sizes_match_spec() {
523 let chunker = FastCDC::default();
524 assert_eq!(chunker.min_size(), 64 * 1024);
525 assert_eq!(chunker.avg_size(), 256 * 1024);
526 assert_eq!(chunker.max_size(), 1024 * 1024);
527 }
528
529 #[test]
530 fn identical_substrings_produce_identical_chunks() {
531 let chunker = FastCDC::new(64, 256, 1024).expect("valid");
534 let shared: Vec<u8> = (0..2000u32)
535 .map(|i| u8::try_from(i & 0xFF).expect("fits"))
536 .collect();
537
538 let mut a = Vec::new();
539 a.extend_from_slice(&[0xAA; 100]);
540 a.extend_from_slice(&shared);
541 a.extend_from_slice(&[0xBB; 100]);
542
543 let mut b = Vec::new();
544 b.extend_from_slice(&[0xCC; 50]);
545 b.extend_from_slice(&shared);
546 b.extend_from_slice(&[0xDD; 200]);
547
548 let a_chunks = chunker.chunk_slice(&a);
549 let b_chunks = chunker.chunk_slice(&b);
550
551 let a_ids: HashSet<&[u8]> = a_chunks.iter().copied().collect();
552 let b_ids: HashSet<&[u8]> = b_chunks.iter().copied().collect();
553 let shared_chunks: Vec<&&[u8]> = a_ids.intersection(&b_ids).collect();
554 assert!(
555 !shared_chunks.is_empty(),
556 "expected at least one shared chunk between the two inputs"
557 );
558 }
559
560 #[test]
561 fn mask_for_handles_small_values() {
562 assert_eq!(mask_for(0), 0);
563 assert!(mask_for(1) > 0);
564 assert!(mask_for(256) > mask_for(64));
565 }
566
567 #[test]
568 fn empty_input_yields_no_chunks() {
569 let chunker = FastCDC::default();
570 let data: Vec<u8> = Vec::new();
571 let chunks = chunker.chunk_slice(&data);
572 assert!(chunks.is_empty());
573
574 let mut cursor = Cursor::new(&data);
575 let reader_chunks = chunker.chunk_reader(&mut cursor).expect("read succeeds");
576 assert!(reader_chunks.is_empty());
577 }
578}