1use crate::error::{Error, Result};
15use crate::io::WriteHandle;
16use std::path::{Path, PathBuf};
17
18pub const STATE_SUFFIX: &str = ".rst-state";
19const STATE_MAGIC: &[u8; 4] = b"RSTS";
20const STATE_VERSION: u16 = 1;
21const STATE_HEADER_LEN: usize = 4 + 2 + 4 + 8 + 8 + 32;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ChunkBitmap {
26 bits: Vec<u8>,
27 len: u64,
28 set_count: u64,
29}
30
31impl ChunkBitmap {
32 pub fn new(len: u64) -> Self {
33 Self {
34 bits: vec![0u8; len.div_ceil(8) as usize],
35 len,
36 set_count: 0,
37 }
38 }
39
40 pub fn from_bytes(bits: Vec<u8>, len: u64) -> Result<Self> {
42 let expect = len.div_ceil(8) as usize;
43 if bits.len() != expect {
44 return Err(Error::protocol(format!(
45 "bitmap is {} bytes, expected {expect} for {len} chunks",
46 bits.len()
47 )));
48 }
49 if len % 8 != 0 {
52 if let Some(last) = bits.last() {
53 let valid = (len % 8) as u32;
54 if last >> valid != 0 {
55 return Err(Error::protocol("bitmap has bits set past the chunk count"));
56 }
57 }
58 }
59 let set_count = bits.iter().map(|b| b.count_ones() as u64).sum();
60 Ok(Self {
61 bits,
62 len,
63 set_count,
64 })
65 }
66
67 pub fn as_bytes(&self) -> &[u8] {
68 &self.bits
69 }
70
71 pub fn len(&self) -> u64 {
72 self.len
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.len == 0
77 }
78
79 #[inline]
80 pub fn get(&self, i: u64) -> bool {
81 if i >= self.len {
82 return false;
83 }
84 self.bits[(i / 8) as usize] & (1 << (i % 8)) != 0
85 }
86
87 #[inline]
89 pub fn set(&mut self, i: u64) -> bool {
90 if i >= self.len {
91 return false;
92 }
93 let byte = &mut self.bits[(i / 8) as usize];
94 let mask = 1u8 << (i % 8);
95 if *byte & mask != 0 {
96 return false;
97 }
98 *byte |= mask;
99 self.set_count += 1;
100 true
101 }
102
103 pub fn count(&self) -> u64 {
104 self.set_count
105 }
106
107 pub fn is_complete(&self) -> bool {
108 self.set_count == self.len
109 }
110
111 pub fn missing(&self) -> impl Iterator<Item = u64> + '_ {
113 (0..self.len).filter(move |&i| !self.get(i))
114 }
115
116 pub fn fill(&mut self) {
117 for b in self.bits.iter_mut() {
118 *b = 0xFF;
119 }
120 if self.len % 8 != 0 {
121 let valid = (self.len % 8) as u32;
122 if let Some(last) = self.bits.last_mut() {
123 *last = (1u8 << valid) - 1;
124 }
125 }
126 self.set_count = self.len;
127 }
128}
129
130pub struct ResumeState {
132 path: PathBuf,
133 bitmap: ChunkBitmap,
134 size: u64,
135 chunk_size: u32,
136 dirty: u64,
138}
139
140impl ResumeState {
141 pub fn state_path(final_path: &Path) -> PathBuf {
142 let mut s = final_path.as_os_str().to_os_string();
143 s.push(STATE_SUFFIX);
144 PathBuf::from(s)
145 }
146
147 pub fn load_or_new(final_path: &Path, size: u64, chunk_size: u32) -> Self {
153 let path = Self::state_path(final_path);
154 let chunks = if chunk_size == 0 {
155 0
156 } else {
157 size.div_ceil(chunk_size as u64)
158 };
159
160 if let Some(bm) = Self::try_load(&path, size, chunk_size, chunks) {
161 return Self {
162 path,
163 bitmap: bm,
164 size,
165 chunk_size,
166 dirty: 0,
167 };
168 }
169 Self {
170 path,
171 bitmap: ChunkBitmap::new(chunks),
172 size,
173 chunk_size,
174 dirty: 0,
175 }
176 }
177
178 fn try_load(path: &Path, size: u64, chunk_size: u32, chunks: u64) -> Option<ChunkBitmap> {
179 let raw = std::fs::read(path).ok()?;
180 if raw.len() < STATE_HEADER_LEN || &raw[0..4] != STATE_MAGIC {
181 return None;
182 }
183 if u16::from_le_bytes(raw[4..6].try_into().ok()?) != STATE_VERSION {
184 return None;
185 }
186 if u32::from_le_bytes(raw[6..10].try_into().ok()?) != chunk_size {
187 return None;
188 }
189 if u64::from_le_bytes(raw[10..18].try_into().ok()?) != size {
190 return None;
191 }
192 let n = u64::from_le_bytes(raw[18..26].try_into().ok()?);
193 if n != chunks {
194 return None;
195 }
196 let stored_hash = &raw[26..58];
197 let body = &raw[STATE_HEADER_LEN..];
198 if blake3::hash(body).as_bytes() != stored_hash {
200 tracing::warn!(path = %path.display(), "resume state failed its checksum; starting over");
201 return None;
202 }
203 ChunkBitmap::from_bytes(body.to_vec(), chunks).ok()
204 }
205
206 pub fn bitmap(&self) -> &ChunkBitmap {
207 &self.bitmap
208 }
209
210 pub fn has(&self, chunk: u64) -> bool {
211 self.bitmap.get(chunk)
212 }
213
214 pub fn record(&mut self, chunk: u64) {
216 if self.bitmap.set(chunk) {
217 self.dirty += 1;
218 }
219 }
220
221 pub fn pending(&self) -> u64 {
222 self.dirty
223 }
224
225 pub fn is_complete(&self) -> bool {
226 self.bitmap.is_complete()
227 }
228
229 pub fn checkpoint(&mut self, handle: &WriteHandle) -> Result<()> {
231 if self.dirty == 0 {
232 return Ok(());
233 }
234 handle.sync()?;
235 self.persist()?;
236 self.dirty = 0;
237 Ok(())
238 }
239
240 fn persist(&self) -> Result<()> {
241 let body = self.bitmap.as_bytes();
242 let mut out = Vec::with_capacity(STATE_HEADER_LEN + body.len());
243 out.extend_from_slice(STATE_MAGIC);
244 out.extend_from_slice(&STATE_VERSION.to_le_bytes());
245 out.extend_from_slice(&self.chunk_size.to_le_bytes());
246 out.extend_from_slice(&self.size.to_le_bytes());
247 out.extend_from_slice(&self.bitmap.len().to_le_bytes());
248 out.extend_from_slice(blake3::hash(body).as_bytes());
249 out.extend_from_slice(body);
250
251 let tmp = self.path.with_extension("tmp");
254 std::fs::write(&tmp, &out)?;
255 std::fs::rename(&tmp, &self.path)?;
256 Ok(())
257 }
258
259 pub fn clear(&self) {
261 let _ = std::fs::remove_file(&self.path);
262 let _ = std::fs::remove_file(self.path.with_extension("tmp"));
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn bitmap_set_get_count() {
272 let mut b = ChunkBitmap::new(20);
273 assert_eq!(b.count(), 0);
274 assert!(!b.is_complete());
275 assert!(b.set(0));
276 assert!(!b.set(0), "setting twice must not double-count");
277 assert!(b.set(19));
278 assert!(b.get(0) && b.get(19));
279 assert!(!b.get(5));
280 assert_eq!(b.count(), 2);
281 assert!(!b.set(20), "out of range set is a no-op");
282 assert!(!b.get(20));
283 assert_eq!(b.missing().count(), 18);
284 b.fill();
285 assert!(b.is_complete());
286 assert_eq!(b.count(), 20);
287 assert_eq!(b.missing().count(), 0);
288 }
289
290 #[test]
291 fn bitmap_roundtrips_and_rejects_bad_input() {
292 let mut b = ChunkBitmap::new(100);
293 for i in (0..100).step_by(3) {
294 b.set(i);
295 }
296 let restored = ChunkBitmap::from_bytes(b.as_bytes().to_vec(), 100).unwrap();
297 assert_eq!(restored, b);
298 assert_eq!(restored.count(), b.count());
299
300 assert!(ChunkBitmap::from_bytes(vec![0u8; 3], 100).is_err());
301 assert!(ChunkBitmap::from_bytes(vec![0xFF; 13], 100).is_err());
303 }
304
305 #[test]
306 fn bitmap_scales_to_a_100gb_file() {
307 let chunks = 100 * 1024u64;
309 let mut b = ChunkBitmap::new(chunks);
310 assert_eq!(b.as_bytes().len(), 12_800, "12.8 KB of state for 100 GiB");
311 for i in 0..chunks {
312 b.set(i);
313 }
314 assert!(b.is_complete());
315 }
316
317 #[test]
318 fn state_survives_a_reload() {
319 let tmp = tempfile::tempdir().unwrap();
320 let dest = tmp.path().join("f.bin");
321 let h = WriteHandle::open(&dest, 10_000, false).unwrap();
322
323 let mut s = ResumeState::load_or_new(&dest, 10_000, 1000);
324 assert_eq!(s.bitmap().len(), 10);
325 for c in [0u64, 1, 2, 7] {
326 h.write_at(c * 1000, &[1u8; 1000]).unwrap();
327 s.record(c);
328 }
329 assert_eq!(s.pending(), 4);
330 s.checkpoint(&h).unwrap();
331 assert_eq!(s.pending(), 0);
332
333 let s2 = ResumeState::load_or_new(&dest, 10_000, 1000);
334 assert_eq!(s2.bitmap().count(), 4);
335 for c in [0u64, 1, 2, 7] {
336 assert!(s2.has(c));
337 }
338 assert!(!s2.has(3));
339 }
340
341 #[test]
342 fn state_is_discarded_when_the_file_changed() {
343 let tmp = tempfile::tempdir().unwrap();
344 let dest = tmp.path().join("f.bin");
345 let h = WriteHandle::open(&dest, 10_000, false).unwrap();
346 let mut s = ResumeState::load_or_new(&dest, 10_000, 1000);
347 s.record(0);
348 s.checkpoint(&h).unwrap();
349
350 let s2 = ResumeState::load_or_new(&dest, 20_000, 1000);
352 assert_eq!(s2.bitmap().count(), 0);
353 let s3 = ResumeState::load_or_new(&dest, 10_000, 4096);
355 assert_eq!(s3.bitmap().count(), 0);
356 }
357
358 #[test]
359 fn corrupt_state_falls_back_to_a_full_transfer() {
360 let tmp = tempfile::tempdir().unwrap();
361 let dest = tmp.path().join("f.bin");
362 let h = WriteHandle::open(&dest, 10_000, false).unwrap();
363 let mut s = ResumeState::load_or_new(&dest, 10_000, 1000);
364 s.record(0);
365 s.record(1);
366 s.checkpoint(&h).unwrap();
367
368 let p = ResumeState::state_path(&dest);
370 let mut raw = std::fs::read(&p).unwrap();
371 let last = raw.len() - 1;
372 raw[last] ^= 0xFF;
373 std::fs::write(&p, &raw).unwrap();
374
375 let s2 = ResumeState::load_or_new(&dest, 10_000, 1000);
376 assert_eq!(s2.bitmap().count(), 0, "corruption must not be trusted");
377 }
378
379 #[test]
380 fn clear_removes_the_sidecar() {
381 let tmp = tempfile::tempdir().unwrap();
382 let dest = tmp.path().join("f.bin");
383 let h = WriteHandle::open(&dest, 1000, false).unwrap();
384 let mut s = ResumeState::load_or_new(&dest, 1000, 1000);
385 s.record(0);
386 s.checkpoint(&h).unwrap();
387 assert!(ResumeState::state_path(&dest).exists());
388 s.clear();
389 assert!(!ResumeState::state_path(&dest).exists());
390 }
391}