OXYGENHUB
# b5e98094eb8f oxygen / crates / oxy-core / src / chunk.rs
5.3 KiB · Rust · last changed Unbound hosted repositories adopt the first pusher's id 1 month ago source blame history raw
1
//! FastCDC content-defined chunking (spec §13.2).
2
//!
3
//! Parameters: 32 KiB minimum, 64 KiB target, 256 KiB maximum, normalized
4
//! chunking level 2. The gear table is derived from BLAKE3 so that every
5
//! implementation produces identical chunk boundaries:
6
//! `GEAR[i] = u64::from_le_bytes(XOF("oxy1 fastcdc gear")[8*i .. 8*i+8])`.
7
//! The masks have `16 + 2` and `16 - 2` one-bits spread evenly over 64 bits
8
//! (see `spread_mask`). Cut-point skipping starts hashing at the minimum size,
9
//! as in the FastCDC paper (Xia et al., 2016).
10
11
use std::sync::OnceLock;
12
13
pub const MIN_SIZE: usize = 32 * 1024;
14
pub const AVG_SIZE: usize = 64 * 1024;
15
pub const MAX_SIZE: usize = 256 * 1024;
16
const AVG_BITS: u32 = 16; // log2(AVG_SIZE)
17
const NORMALIZATION: u32 = 2;
18
pub const GEAR_SEED: &[u8] = b"oxy1 fastcdc gear";
19
20
fn gear() -> &'static [u64; 256] {
21
    static GEAR: OnceLock<[u64; 256]> = OnceLock::new();
22
    GEAR.get_or_init(|| {
23
        let mut out = [0u8; 256 * 8];
24
        blake3::Hasher::new()
25
            .update(GEAR_SEED)
26
            .finalize_xof()
27
            .fill(&mut out);
28
        let mut g = [0u64; 256];
29
        for (i, v) in g.iter_mut().enumerate() {
30
            *v = u64::from_le_bytes(out[i * 8..i * 8 + 8].try_into().unwrap());
31
        }
32
        g
33
    })
34
}
35
36
/// A mask with `ones` one-bits at bit positions `63 - floor(i * 64 / ones)`.
37
pub const fn spread_mask(ones: u32) -> u64 {
38
    let mut m = 0u64;
39
    let mut i = 0;
40
    while i < ones {
41
        m |= 1u64 << (63 - (i * 64 / ones));
42
        i += 1;
43
    }
44
    m
45
}
46
47
pub const MASK_S: u64 = spread_mask(AVG_BITS + NORMALIZATION);
48
pub const MASK_L: u64 = spread_mask(AVG_BITS - NORMALIZATION);
49
50
/// Length of the first chunk of `data`.
51
pub fn cut_point(data: &[u8]) -> usize {
52
    let g = gear();
53
    let mut n = data.len();
54
    if n <= MIN_SIZE {
55
        return n;
56
    }
57
    let mut normal = AVG_SIZE;
58
    if n >= MAX_SIZE {
59
        n = MAX_SIZE;
60
    } else if n <= normal {
61
        normal = n;
62
    }
63
    let mut hash: u64 = 0;
64
    let mut i = MIN_SIZE;
65
    while i < normal {
66
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
67
        if hash & MASK_S == 0 {
68
            return i;
69
        }
70
        i += 1;
71
    }
72
    while i < n {
73
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
74
        if hash & MASK_L == 0 {
75
            return i;
76
        }
77
        i += 1;
78
    }
79
    n
80
}
81
82
/// Split `data` into chunks; the concatenation of the result is `data`.
83
pub fn chunks(data: &[u8]) -> Vec<&[u8]> {
84
    let mut out = Vec::new();
85
    let mut rest = data;
86
    while !rest.is_empty() {
87
        let n = cut_point(rest);
88
        out.push(&rest[..n]);
89
        rest = &rest[n..];
90
    }
91
    out
92
}
93
94
#[cfg(test)]
95
mod tests {
96
    use super::*;
97
    use rand::{RngCore, SeedableRng};
98
99
    fn random(len: usize, seed: u64) -> Vec<u8> {
100
        let mut v = vec![0u8; len];
101
        rand::rngs::StdRng::seed_from_u64(seed).fill_bytes(&mut v);
102
        v
103
    }
104
105
    #[test]
106
    fn masks_and_gear_are_pinned() {
107
        assert_eq!(MASK_S.count_ones(), 18);
108
        assert_eq!(MASK_L.count_ones(), 14);
109
        assert_eq!(MASK_S & (1 << 63), 1 << 63);
110
        // Pin the first gear entry so the table cannot drift silently.
111
        let mut out = [0u8; 8];
112
        blake3::Hasher::new()
113
            .update(GEAR_SEED)
114
            .finalize_xof()
115
            .fill(&mut out);
116
        assert_eq!(gear()[0], u64::from_le_bytes(out));
117
        assert_ne!(gear()[1], gear()[2]);
118
    }
119
120
    #[test]
121
    fn small_inputs_are_one_chunk() {
122
        assert!(chunks(b"").is_empty());
123
        assert_eq!(chunks(b"abc"), vec![&b"abc"[..]]);
124
        let d = random(MIN_SIZE, 1);
125
        assert_eq!(chunks(&d).len(), 1);
126
    }
127
128
    #[test]
129
    fn bounds_and_reassembly() {
130
        let d = random(3 * 1024 * 1024, 2);
131
        let cs = chunks(&d);
132
        assert!(cs.len() > 10, "{}", cs.len());
133
        let total: usize = cs.iter().map(|c| c.len()).sum();
134
        assert_eq!(total, d.len());
135
        assert_eq!(cs.concat(), d);
136
        for (i, c) in cs.iter().enumerate() {
137
            assert!(c.len() <= MAX_SIZE);
138
            if i + 1 < cs.len() {
139
                assert!(c.len() >= MIN_SIZE, "chunk {i} is {} bytes", c.len());
140
            }
141
        }
142
        let avg = total / cs.len();
143
        assert!(avg > MIN_SIZE && avg < MAX_SIZE, "average {avg}");
144
        assert_eq!(chunks(&d), cs, "deterministic");
145
    }
146
147
    #[test]
148
    fn insertion_only_disturbs_local_chunks() {
149
        let d = random(2 * 1024 * 1024, 3);
150
        let mut e = d.clone();
151
        e.splice(1_000_000..1_000_000, random(100, 4));
152
        let a: std::collections::HashSet<[u8; 32]> = chunks(&d)
153
            .iter()
154
            .map(|c| *blake3::hash(c).as_bytes())
155
            .collect();
156
        let b: Vec<[u8; 32]> = chunks(&e)
157
            .iter()
158
            .map(|c| *blake3::hash(c).as_bytes())
159
            .collect();
160
        let shared = b.iter().filter(|h| a.contains(*h)).count();
161
        assert!(
162
            shared * 10 >= b.len() * 8,
163
            "only {shared} of {} chunks shared",
164
            b.len()
165
        );
166
    }
167
168
    #[test]
169
    fn adversarial_constant_input_hits_bounds() {
170
        let zeros = vec![0u8; 1024 * 1024];
171
        let cs = chunks(&zeros);
172
        for c in &cs[..cs.len() - 1] {
173
            assert!(c.len() >= MIN_SIZE && c.len() <= MAX_SIZE);
174
        }
175
        assert!(cs.len() >= 1024 * 1024 / MAX_SIZE);
176
        assert!(cs.len() <= 1024 * 1024 / MIN_SIZE + 1);
177
    }
178
}