OXYGENHUB
# 201f900d7ece oxygen / crates / oxy-core / src / chunk.rs
5.1 KiB · Rust · last changed FastCDC chunking with a pinned gear table; oxy checkout with checkout-safety rules 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().update(GEAR_SEED).finalize_xof().fill(&mut out);
25
        let mut g = [0u64; 256];
26
        for (i, v) in g.iter_mut().enumerate() {
27
            *v = u64::from_le_bytes(out[i * 8..i * 8 + 8].try_into().unwrap());
28
        }
29
        g
30
    })
31
}
32
33
/// A mask with `ones` one-bits at bit positions `63 - floor(i * 64 / ones)`.
34
pub const fn spread_mask(ones: u32) -> u64 {
35
    let mut m = 0u64;
36
    let mut i = 0;
37
    while i < ones {
38
        m |= 1u64 << (63 - (i * 64 / ones));
39
        i += 1;
40
    }
41
    m
42
}
43
44
pub const MASK_S: u64 = spread_mask(AVG_BITS + NORMALIZATION);
45
pub const MASK_L: u64 = spread_mask(AVG_BITS - NORMALIZATION);
46
47
/// Length of the first chunk of `data`.
48
pub fn cut_point(data: &[u8]) -> usize {
49
    let g = gear();
50
    let mut n = data.len();
51
    if n <= MIN_SIZE {
52
        return n;
53
    }
54
    let mut normal = AVG_SIZE;
55
    if n >= MAX_SIZE {
56
        n = MAX_SIZE;
57
    } else if n <= normal {
58
        normal = n;
59
    }
60
    let mut hash: u64 = 0;
61
    let mut i = MIN_SIZE;
62
    while i < normal {
63
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
64
        if hash & MASK_S == 0 {
65
            return i;
66
        }
67
        i += 1;
68
    }
69
    while i < n {
70
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
71
        if hash & MASK_L == 0 {
72
            return i;
73
        }
74
        i += 1;
75
    }
76
    n
77
}
78
79
/// Split `data` into chunks; the concatenation of the result is `data`.
80
pub fn chunks(data: &[u8]) -> Vec<&[u8]> {
81
    let mut out = Vec::new();
82
    let mut rest = data;
83
    while !rest.is_empty() {
84
        let n = cut_point(rest);
85
        out.push(&rest[..n]);
86
        rest = &rest[n..];
87
    }
88
    out
89
}
90
91
#[cfg(test)]
92
mod tests {
93
    use super::*;
94
    use rand::{RngCore, SeedableRng};
95
96
    fn random(len: usize, seed: u64) -> Vec<u8> {
97
        let mut v = vec![0u8; len];
98
        rand::rngs::StdRng::seed_from_u64(seed).fill_bytes(&mut v);
99
        v
100
    }
101
102
    #[test]
103
    fn masks_and_gear_are_pinned() {
104
        assert_eq!(MASK_S.count_ones(), 18);
105
        assert_eq!(MASK_L.count_ones(), 14);
106
        assert_eq!(MASK_S & (1 << 63), 1 << 63);
107
        // Pin the first gear entry so the table cannot drift silently.
108
        let mut out = [0u8; 8];
109
        blake3::Hasher::new().update(GEAR_SEED).finalize_xof().fill(&mut out);
110
        assert_eq!(gear()[0], u64::from_le_bytes(out));
111
        assert_ne!(gear()[1], gear()[2]);
112
    }
113
114
    #[test]
115
    fn small_inputs_are_one_chunk() {
116
        assert!(chunks(b"").is_empty());
117
        assert_eq!(chunks(b"abc"), vec![&b"abc"[..]]);
118
        let d = random(MIN_SIZE, 1);
119
        assert_eq!(chunks(&d).len(), 1);
120
    }
121
122
    #[test]
123
    fn bounds_and_reassembly() {
124
        let d = random(3 * 1024 * 1024, 2);
125
        let cs = chunks(&d);
126
        assert!(cs.len() > 10, "{}", cs.len());
127
        let total: usize = cs.iter().map(|c| c.len()).sum();
128
        assert_eq!(total, d.len());
129
        assert_eq!(cs.concat(), d);
130
        for (i, c) in cs.iter().enumerate() {
131
            assert!(c.len() <= MAX_SIZE);
132
            if i + 1 < cs.len() {
133
                assert!(c.len() >= MIN_SIZE, "chunk {i} is {} bytes", c.len());
134
            }
135
        }
136
        let avg = total / cs.len();
137
        assert!(avg > MIN_SIZE && avg < MAX_SIZE, "average {avg}");
138
        assert_eq!(chunks(&d), cs, "deterministic");
139
    }
140
141
    #[test]
142
    fn insertion_only_disturbs_local_chunks() {
143
        let d = random(2 * 1024 * 1024, 3);
144
        let mut e = d.clone();
145
        e.splice(1_000_000..1_000_000, random(100, 4));
146
        let a: std::collections::HashSet<[u8; 32]> = chunks(&d).iter().map(|c| *blake3::hash(c).as_bytes()).collect();
147
        let b: Vec<[u8; 32]> = chunks(&e).iter().map(|c| *blake3::hash(c).as_bytes()).collect();
148
        let shared = b.iter().filter(|h| a.contains(*h)).count();
149
        assert!(shared * 10 >= b.len() * 8, "only {shared} of {} chunks shared", b.len());
150
    }
151
152
    #[test]
153
    fn adversarial_constant_input_hits_bounds() {
154
        let zeros = vec![0u8; 1024 * 1024];
155
        let cs = chunks(&zeros);
156
        for c in &cs[..cs.len() - 1] {
157
            assert!(c.len() >= MIN_SIZE && c.len() <= MAX_SIZE);
158
        }
159
        assert!(cs.len() >= 1024 * 1024 / MAX_SIZE);
160
        assert!(cs.len() <= 1024 * 1024 / MIN_SIZE + 1);
161
    }
162
}