decdn_cli/commands/
chunker.rs1use anyhow::{Result, bail};
7use decdn_protocol::client::MB_BYTES;
8use fastcdc::v2020;
9
10pub(crate) struct ChunkSizes {
13 pub(crate) min: u32,
15 pub(crate) avg: u32,
17 pub(crate) max: u32,
19}
20
21impl ChunkSizes {
22 pub(crate) fn resolve(avg: u64, min: Option<u64>, max: Option<u64>) -> Result<Self> {
29 if !avg.is_power_of_two() {
30 bail!("--chunk-avg {avg} must be a power of two");
31 }
32 let min = min.unwrap_or_else(|| (avg / 4).max(MB_BYTES));
35 let max = max.unwrap_or_else(|| avg.saturating_mul(2));
36
37 if min < MB_BYTES {
38 bail!("--chunk-min {min} is below the 1 MiB payment interval (MB_BYTES)");
39 }
40 if min > avg || avg > max {
41 bail!("--chunk sizes must satisfy min <= avg <= max (min={min}, avg={avg}, max={max})");
42 }
43 let minimum_min = u64::try_from(v2020::MINIMUM_MIN).unwrap_or(u64::MAX);
50 let minimum_max = u64::try_from(v2020::MINIMUM_MAX).unwrap_or(u64::MAX);
51 let average_min = u64::try_from(v2020::AVERAGE_MIN).unwrap_or(u64::MAX);
52 let average_max = u64::try_from(v2020::AVERAGE_MAX).unwrap_or(u64::MAX);
53 let maximum_min = u64::try_from(v2020::MAXIMUM_MIN).unwrap_or(u64::MAX);
54 let maximum_max = u64::try_from(v2020::MAXIMUM_MAX).unwrap_or(u64::MAX);
55
56 if !(minimum_min..=minimum_max).contains(&min) {
57 bail!("--chunk-min {min} out of fastcdc range [{minimum_min}, {minimum_max}]");
58 }
59 if !(average_min..=average_max).contains(&avg) {
60 bail!("--chunk-avg {avg} out of fastcdc range [{average_min}, {average_max}]");
61 }
62 if !(maximum_min..=maximum_max).contains(&max) {
63 bail!("--chunk-max {max} out of fastcdc range [{maximum_min}, {maximum_max}]");
64 }
65 Ok(Self {
66 min: u32::try_from(min)
67 .map_err(|_| anyhow::anyhow!("--chunk-min {min} exceeds u32"))?,
68 avg: u32::try_from(avg)
69 .map_err(|_| anyhow::anyhow!("--chunk-avg {avg} exceeds u32"))?,
70 max: u32::try_from(max)
71 .map_err(|_| anyhow::anyhow!("--chunk-max {max} exceeds u32"))?,
72 })
73 }
74}
75
76pub(crate) struct ChunkedFile {
78 pub(crate) whole_hash: blake3::Hash,
80 pub(crate) total_size: u64,
82 pub(crate) chunks: Vec<crate::commands::manifest::Chunk>,
85}
86
87pub(crate) fn chunk_file<R, F>(source: R, sizes: &ChunkSizes, mut sink: F) -> Result<ChunkedFile>
95where
96 R: std::io::Read,
97 F: FnMut(blake3::Hash, &[u8]) -> Result<()>,
98{
99 use crate::commands::manifest::{Chunk, b3_hex_str};
100
101 let mut whole = blake3::Hasher::new();
102 let mut total: u64 = 0;
103 let mut chunks: Vec<Chunk> = Vec::new();
104
105 let min = usize::try_from(sizes.min).map_err(|_| anyhow::anyhow!("chunk min exceeds usize"))?;
109 let avg = usize::try_from(sizes.avg).map_err(|_| anyhow::anyhow!("chunk avg exceeds usize"))?;
110 let max = usize::try_from(sizes.max).map_err(|_| anyhow::anyhow!("chunk max exceeds usize"))?;
111 let chunker = v2020::StreamCDC::new(source, min, avg, max);
112 for result in chunker {
113 let chunk = result.map_err(|e| anyhow::anyhow!("fastcdc streaming error: {e}"))?;
114 let data = chunk.data.as_slice();
115 whole.update(data);
116 let size = u64::try_from(chunk.length)
117 .map_err(|_| anyhow::anyhow!("chunk length {} exceeds u64", chunk.length))?;
118 total = total
119 .checked_add(size)
120 .ok_or_else(|| anyhow::anyhow!("total size overflow"))?;
121 let chash = blake3::hash(data);
122 sink(chash, data)?;
123 chunks.push(Chunk {
124 hash: b3_hex_str(chash),
125 size,
126 });
127 }
128 Ok(ChunkedFile {
129 whole_hash: whole.finalize(),
130 total_size: total,
131 chunks,
132 })
133}
134
135#[cfg(test)]
136#[allow(
137 clippy::unwrap_used,
138 clippy::expect_used,
139 clippy::indexing_slicing,
140 clippy::panic
141)]
142mod tests;