OXYGENHUB

Coverage / crates/oxy-core/src/chunk.rs 92.3%

120 of 130 lines run, 18 of 20 functions · run #314 on main · run missed

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
201032×
fn gear() -> &'static [u64; 256] {
21
    static GEAR: OnceLock<[u64; 256]> = OnceLock::new();
221032×
    GEAR.get_or_init(|| {
232×
        let mut out = [0u8; 256 * 8];
242×
        blake3::Hasher::new()
252×
            .update(GEAR_SEED)
262×
            .finalize_xof()
272×
            .fill(&mut out);
282×
        let mut g = [0u64; 256];
29512×
        for (i, v) in g.iter_mut().enumerate() {
30512×
            *v = u64::from_le_bytes(out[i * 8..i * 8 + 8].try_into().unwrap());
31512×
        }
322×
        g
332×
    })
341032×
}
35
36
/// A mask with `ones` one-bits at bit positions `63 - floor(i * 64 / ones)`.
370×
pub const fn spread_mask(ones: u32) -> u64 {
380×
    let mut m = 0u64;
390×
    let mut i = 0;
400×
    while i < ones {
410×
        m |= 1u64 << (63 - (i * 64 / ones));
420×
        i += 1;
430×
    }
440×
    m
450×
}
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`.
511029×
pub fn cut_point(data: &[u8]) -> usize {
521029×
    let g = gear();
531029×
    let mut n = data.len();
541029×
    if n <= MIN_SIZE {
55517×
        return n;
56512×
    }
57512×
    let mut normal = AVG_SIZE;
58512×
    if n >= MAX_SIZE {
59282×
        n = MAX_SIZE;
60282×
    } else if n <= normal {
61137×
        normal = n;
62137×
    }
63512×
    let mut hash: u64 = 0;
64512×
    let mut i = MIN_SIZE;
6513175817×
    while i < normal {
6613175321×
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
6713175321×
        if hash & MASK_S == 0 {
6816×
            return i;
6913175305×
        }
7013175305×
        i += 1;
71
    }
7224477131×
    while i < n {
7324476861×
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
7424476861×
        if hash & MASK_L == 0 {
75226×
            return i;
7624476635×
        }
7724476635×
        i += 1;
78
    }
79270×
    n
801029×
}
81
82
/// Split `data` into chunks; the concatenation of the result is `data`.
83686×
pub fn chunks(data: &[u8]) -> Vec<&[u8]> {
84686×
    let mut out = Vec::new();
85686×
    let mut rest = data;
861715×
    while !rest.is_empty() {
871029×
        let n = cut_point(rest);
881029×
        out.push(&rest[..n]);
891029×
        rest = &rest[n..];
901029×
    }
91686×
    out
92686×
}
93
94
#[cfg(test)]
95
mod tests {
96
    use super::*;
97
    use rand::{RngCore, SeedableRng};
98
994×
    fn random(len: usize, seed: u64) -> Vec<u8> {
1004×
        let mut v = vec![0u8; len];
1014×
        rand::rngs::StdRng::seed_from_u64(seed).fill_bytes(&mut v);
1024×
        v
1034×
    }
104
105
    #[test]
1061×
    fn masks_and_gear_are_pinned() {
1071×
        assert_eq!(MASK_S.count_ones(), 18);
1081×
        assert_eq!(MASK_L.count_ones(), 14);
1091×
        assert_eq!(MASK_S & (1 << 63), 1 << 63);
110
        // Pin the first gear entry so the table cannot drift silently.
1111×
        let mut out = [0u8; 8];
1121×
        blake3::Hasher::new()
1131×
            .update(GEAR_SEED)
1141×
            .finalize_xof()
1151×
            .fill(&mut out);
1161×
        assert_eq!(gear()[0], u64::from_le_bytes(out));
1171×
        assert_ne!(gear()[1], gear()[2]);
1181×
    }
119
120
    #[test]
1211×
    fn small_inputs_are_one_chunk() {
1221×
        assert!(chunks(b"").is_empty());
1231×
        assert_eq!(chunks(b"abc"), vec![&b"abc"[..]]);
1241×
        let d = random(MIN_SIZE, 1);
1251×
        assert_eq!(chunks(&d).len(), 1);
1261×
    }
127
128
    #[test]
1291×
    fn bounds_and_reassembly() {
1301×
        let d = random(3 * 1024 * 1024, 2);
1311×
        let cs = chunks(&d);
1321×
        assert!(cs.len() > 10, "{}", cs.len());
13342×
        let total: usize = cs.iter().map(|c| c.len()).sum();
1341×
        assert_eq!(total, d.len());
1351×
        assert_eq!(cs.concat(), d);
13642×
        for (i, c) in cs.iter().enumerate() {
13742×
            assert!(c.len() <= MAX_SIZE);
13842×
            if i + 1 < cs.len() {
13941×
                assert!(c.len() >= MIN_SIZE, "chunk {i} is {} bytes", c.len());
1401×
            }
141
        }
1421×
        let avg = total / cs.len();
1431×
        assert!(avg > MIN_SIZE && avg < MAX_SIZE, "average {avg}");
1441×
        assert_eq!(chunks(&d), cs, "deterministic");
1451×
    }
146
147
    #[test]
1481×
    fn insertion_only_disturbs_local_chunks() {
1491×
        let d = random(2 * 1024 * 1024, 3);
1501×
        let mut e = d.clone();
1511×
        e.splice(1_000_000..1_000_000, random(100, 4));
1521×
        let a: std::collections::HashSet<[u8; 32]> = chunks(&d)
1531×
            .iter()
15427×
            .map(|c| *blake3::hash(c).as_bytes())
1551×
            .collect();
1561×
        let b: Vec<[u8; 32]> = chunks(&e)
1571×
            .iter()
15827×
            .map(|c| *blake3::hash(c).as_bytes())
1591×
            .collect();
16027×
        let shared = b.iter().filter(|h| a.contains(*h)).count();
1611×
        assert!(
1621×
            shared * 10 >= b.len() * 8,
163
            "only {shared} of {} chunks shared",
1640×
            b.len()
165
        );
1661×
    }
167
168
    #[test]
1691×
    fn adversarial_constant_input_hits_bounds() {
1701×
        let zeros = vec![0u8; 1024 * 1024];
1711×
        let cs = chunks(&zeros);
1723×
        for c in &cs[..cs.len() - 1] {
1733×
            assert!(c.len() >= MIN_SIZE && c.len() <= MAX_SIZE);
174
        }
1751×
        assert!(cs.len() >= 1024 * 1024 / MAX_SIZE);
1761×
        assert!(cs.len() <= 1024 * 1024 / MIN_SIZE + 1);
1771×
    }
178
}