OXYGENHUB

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

120 of 130 lines run, 18 of 20 functions · run #427 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
201328×
fn gear() -> &'static [u64; 256] {
21
    static GEAR: OnceLock<[u64; 256]> = OnceLock::new();
221328×
    GEAR.get_or_init(|| {
2374×
        let mut out = [0u8; 256 * 8];
2474×
        blake3::Hasher::new()
2574×
            .update(GEAR_SEED)
2674×
            .finalize_xof()
2774×
            .fill(&mut out);
2874×
        let mut g = [0u64; 256];
2918944×
        for (i, v) in g.iter_mut().enumerate() {
3018944×
            *v = u64::from_le_bytes(out[i * 8..i * 8 + 8].try_into().unwrap());
3118944×
        }
3274×
        g
3374×
    })
341328×
}
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`.
511325×
pub fn cut_point(data: &[u8]) -> usize {
521325×
    let g = gear();
531325×
    let mut n = data.len();
541325×
    if n <= MIN_SIZE {
55662×
        return n;
56663×
    }
57663×
    let mut normal = AVG_SIZE;
58663×
    if n >= MAX_SIZE {
59381×
        n = MAX_SIZE;
60381×
    } else if n <= normal {
61183×
        normal = n;
62183×
    }
63663×
    let mut hash: u64 = 0;
64663×
    let mut i = MIN_SIZE;
6516870282×
    while i < normal {
6616869639×
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
6716869639×
        if hash & MASK_S == 0 {
6820×
            return i;
6916869619×
        }
7016869619×
        i += 1;
71
    }
7231435958×
    while i < n {
7331435617×
        hash = (hash << 1).wrapping_add(g[data[i] as usize]);
7431435617×
        if hash & MASK_L == 0 {
75302×
            return i;
7631435315×
        }
7731435315×
        i += 1;
78
    }
79341×
    n
801325×
}
81
82
/// Split `data` into chunks; the concatenation of the result is `data`.
83873×
pub fn chunks(data: &[u8]) -> Vec<&[u8]> {
84873×
    let mut out = Vec::new();
85873×
    let mut rest = data;
862198×
    while !rest.is_empty() {
871325×
        let n = cut_point(rest);
881325×
        out.push(&rest[..n]);
891325×
        rest = &rest[n..];
901325×
    }
91873×
    out
92873×
}
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
}