1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16use sha2::{Digest, Sha256};
17
18use super::error::{SyncError, SyncResult};
19
20pub const MAX_SYNC_CHUNK_BYTES: usize = 64 * 1024;
22pub const MAX_SYNC_CHUNK_TOTAL_BYTES: usize = 16 * 1024 * 1024;
24pub const MAX_SYNC_CHUNKS: usize = 4_096;
26pub const MAX_SYNC_TRANSFER_ID_BYTES: usize = 128;
28
29#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct SyncPayloadChunk {
32 pub transfer_id: String,
34 pub offset: u64,
36 pub total_bytes: u64,
38 pub payload_sha256: String,
40 pub chunk_sha256: String,
42 pub payload: Vec<u8>,
44}
45
46impl std::fmt::Debug for SyncPayloadChunk {
47 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 formatter
49 .debug_struct("SyncPayloadChunk")
50 .field("transfer_id", &self.transfer_id)
51 .field("offset", &self.offset)
52 .field("total_bytes", &self.total_bytes)
53 .field("payload_sha256", &self.payload_sha256)
54 .field("chunk_sha256", &self.chunk_sha256)
55 .field("payload_bytes", &self.payload.len())
56 .finish()
57 }
58}
59
60impl SyncPayloadChunk {
61 pub fn validate(&self) -> SyncResult<()> {
63 validate_transfer_id(&self.transfer_id)?;
64 validate_hash(&self.payload_sha256, "payload_sha256")?;
65 validate_hash(&self.chunk_sha256, "chunk_sha256")?;
66 if self.total_bytes == 0 || self.total_bytes > MAX_SYNC_CHUNK_TOTAL_BYTES as u64 {
67 return Err(SyncError::InvalidChunk("invalid total_bytes".to_owned()));
68 }
69 if self.payload.is_empty() || self.payload.len() > MAX_SYNC_CHUNK_BYTES {
70 return Err(SyncError::InvalidChunk("invalid chunk size".to_owned()));
71 }
72 let end = self
73 .offset
74 .checked_add(self.payload.len() as u64)
75 .ok_or_else(|| SyncError::InvalidChunk("chunk range overflow".to_owned()))?;
76 if end > self.total_bytes {
77 return Err(SyncError::InvalidChunk(
78 "chunk exceeds total_bytes".to_owned(),
79 ));
80 }
81 if sha256_hex(&self.payload) != self.chunk_sha256 {
82 return Err(SyncError::InvalidChunk("chunk digest mismatch".to_owned()));
83 }
84 Ok(())
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub struct SyncChunkRange {
91 pub offset: u64,
93 pub len: u64,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SyncChunkProgress {
100 pub transfer_id: String,
102 pub total_bytes: u64,
104 pub payload_sha256: String,
106 pub received_bytes: u64,
108 pub missing: Vec<SyncChunkRange>,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SyncChunkInsert {
115 Accepted,
117 Duplicate,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct SyncChunkAssembler {
124 transfer_id: String,
125 total_bytes: u64,
126 payload_sha256: String,
127 received_bytes: u64,
128 chunks: BTreeMap<u64, Vec<u8>>,
129}
130
131impl SyncChunkAssembler {
132 pub fn new(
134 transfer_id: impl Into<String>,
135 total_bytes: u64,
136 payload_sha256: impl Into<String>,
137 ) -> SyncResult<Self> {
138 let transfer_id = transfer_id.into();
139 let payload_sha256 = payload_sha256.into();
140 validate_transfer_id(&transfer_id)?;
141 validate_hash(&payload_sha256, "payload_sha256")?;
142 if total_bytes == 0 || total_bytes > MAX_SYNC_CHUNK_TOTAL_BYTES as u64 {
143 return Err(SyncError::InvalidChunk("invalid total_bytes".to_owned()));
144 }
145 Ok(Self {
146 transfer_id,
147 total_bytes,
148 payload_sha256,
149 received_bytes: 0,
150 chunks: BTreeMap::new(),
151 })
152 }
153
154 pub fn insert(&mut self, chunk: SyncPayloadChunk) -> SyncResult<SyncChunkInsert> {
156 chunk.validate()?;
157 if chunk.transfer_id != self.transfer_id
158 || chunk.total_bytes != self.total_bytes
159 || chunk.payload_sha256 != self.payload_sha256
160 {
161 return Err(SyncError::InvalidChunk(
162 "transfer metadata mismatch".to_owned(),
163 ));
164 }
165 if let Some(existing) = self.chunks.get(&chunk.offset) {
166 return if existing == &chunk.payload {
167 Ok(SyncChunkInsert::Duplicate)
168 } else {
169 Err(SyncError::ChunkConflict(chunk.offset))
170 };
171 }
172 if self.chunks.len() >= MAX_SYNC_CHUNKS {
173 return Err(SyncError::InvalidChunk("too many chunks".to_owned()));
174 }
175 let end = chunk.offset + chunk.payload.len() as u64;
176 if self.overlaps_existing(chunk.offset, end) {
177 return Err(SyncError::ChunkConflict(chunk.offset));
178 }
179 self.received_bytes += chunk.payload.len() as u64;
180 self.chunks.insert(chunk.offset, chunk.payload);
181 Ok(SyncChunkInsert::Accepted)
182 }
183
184 pub fn progress(&self) -> SyncChunkProgress {
186 SyncChunkProgress {
187 transfer_id: self.transfer_id.clone(),
188 total_bytes: self.total_bytes,
189 payload_sha256: self.payload_sha256.clone(),
190 received_bytes: self.received_bytes,
191 missing: self.missing_ranges(),
192 }
193 }
194
195 pub fn assemble(&self) -> SyncResult<Vec<u8>> {
197 if self.received_bytes != self.total_bytes || !self.missing_ranges().is_empty() {
198 return Err(SyncError::ChunkIncomplete);
199 }
200 let mut payload = Vec::with_capacity(self.total_bytes as usize);
201 for bytes in self.chunks.values() {
202 payload.extend_from_slice(bytes);
203 }
204 if sha256_hex(&payload) != self.payload_sha256 {
205 return Err(SyncError::InvalidChunk(
206 "payload digest mismatch".to_owned(),
207 ));
208 }
209 Ok(payload)
210 }
211
212 fn overlaps_existing(&self, offset: u64, end: u64) -> bool {
213 self.chunks.iter().any(|(existing_offset, bytes)| {
214 let existing_end = *existing_offset + bytes.len() as u64;
215 offset < existing_end && *existing_offset < end
216 })
217 }
218
219 fn missing_ranges(&self) -> Vec<SyncChunkRange> {
220 let mut missing = Vec::new();
221 let mut cursor = 0;
222 for (offset, bytes) in &self.chunks {
223 if cursor < *offset {
224 missing.push(SyncChunkRange {
225 offset: cursor,
226 len: *offset - cursor,
227 });
228 }
229 cursor = (*offset + bytes.len() as u64).max(cursor);
230 }
231 if cursor < self.total_bytes {
232 missing.push(SyncChunkRange {
233 offset: cursor,
234 len: self.total_bytes - cursor,
235 });
236 }
237 missing
238 }
239}
240
241pub fn split_sync_payload(
243 transfer_id: impl Into<String>,
244 payload: &[u8],
245 chunk_bytes: usize,
246) -> SyncResult<Vec<SyncPayloadChunk>> {
247 let transfer_id = transfer_id.into();
248 validate_transfer_id(&transfer_id)?;
249 if payload.is_empty() || payload.len() > MAX_SYNC_CHUNK_TOTAL_BYTES {
250 return Err(SyncError::InvalidChunk("invalid payload size".to_owned()));
251 }
252 if chunk_bytes == 0 || chunk_bytes > MAX_SYNC_CHUNK_BYTES {
253 return Err(SyncError::InvalidChunk("invalid chunk_bytes".to_owned()));
254 }
255 let payload_sha256 = sha256_hex(payload);
256 payload
257 .chunks(chunk_bytes)
258 .enumerate()
259 .map(|(index, bytes)| SyncPayloadChunk {
260 transfer_id: transfer_id.clone(),
261 offset: (index * chunk_bytes) as u64,
262 total_bytes: payload.len() as u64,
263 payload_sha256: payload_sha256.clone(),
264 chunk_sha256: sha256_hex(bytes),
265 payload: bytes.to_vec(),
266 })
267 .collect::<Vec<_>>()
268 .into_iter()
269 .map(|chunk| {
270 chunk.validate()?;
271 Ok(chunk)
272 })
273 .collect()
274}
275
276fn validate_transfer_id(transfer_id: &str) -> SyncResult<()> {
277 if transfer_id.is_empty()
278 || transfer_id.len() > MAX_SYNC_TRANSFER_ID_BYTES
279 || transfer_id.chars().any(char::is_control)
280 {
281 return Err(SyncError::InvalidChunk("invalid transfer_id".to_owned()));
282 }
283 Ok(())
284}
285
286fn validate_hash(value: &str, name: &str) -> SyncResult<()> {
287 if value.len() != 64
288 || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
289 || value != value.to_ascii_lowercase()
290 {
291 return Err(SyncError::InvalidChunk(format!("invalid {name}")));
292 }
293 Ok(())
294}
295
296fn sha256_hex(bytes: &[u8]) -> String {
297 let digest = Sha256::digest(bytes);
298 digest.iter().map(|byte| format!("{byte:02x}")).collect()
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn splits_and_resumes_out_of_order() {
307 let payload = b"abcdefghij";
308 let chunks = split_sync_payload("transfer-1", payload, 4).unwrap();
309 let mut assembler = SyncChunkAssembler::new(
310 "transfer-1",
311 payload.len() as u64,
312 chunks[0].payload_sha256.clone(),
313 )
314 .unwrap();
315 assert_eq!(
316 assembler.insert(chunks[2].clone()).unwrap(),
317 SyncChunkInsert::Accepted
318 );
319 assert_eq!(
320 assembler.progress().missing[0],
321 SyncChunkRange { offset: 0, len: 8 }
322 );
323 assembler.insert(chunks[0].clone()).unwrap();
324 assembler.insert(chunks[1].clone()).unwrap();
325 assert_eq!(assembler.assemble().unwrap(), payload);
326 }
327
328 #[test]
329 fn duplicate_is_idempotent_and_overlap_is_rejected() {
330 let chunks = split_sync_payload("transfer-2", b"abcdefgh", 4).unwrap();
331 let mut assembler =
332 SyncChunkAssembler::new("transfer-2", 8, chunks[0].payload_sha256.clone()).unwrap();
333 assert_eq!(
334 assembler.insert(chunks[0].clone()).unwrap(),
335 SyncChunkInsert::Accepted
336 );
337 assert_eq!(
338 assembler.insert(chunks[0].clone()).unwrap(),
339 SyncChunkInsert::Duplicate
340 );
341 let mut overlap = chunks[1].clone();
342 overlap.offset = 3;
343 overlap.chunk_sha256 = sha256_hex(&overlap.payload);
344 assert!(matches!(
345 assembler.insert(overlap),
346 Err(SyncError::ChunkConflict(3))
347 ));
348 }
349}