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