zipatch_rs/apply/checkpoint.rs
1//! Apply-time progress checkpoints — the data the library emits while a patch
2//! is being written so a consumer can persist enough state to resume a
3//! crashed/interrupted apply later.
4//!
5//! # Shape
6//!
7//! Every checkpoint is a small, owned, serialisable value. The library hands
8//! one to the consumer-installed [`CheckpointSink`] at each natural recovery
9//! boundary the apply driver walks past:
10//!
11//! - **Sequential apply** ([`crate::ZiPatchReader::apply_to`]) — one
12//! [`Checkpoint::Sequential`] per top-level chunk, plus one per DEFLATE
13//! block inside the [`crate::chunk::sqpk::SqpkFile`] `AddFile` loop
14//! (the only chunk type that can carry hundreds of MB of payload). The
15//! in-flight `AddFile` state rides inside the same record so a resume can
16//! pick up the file mid-stream rather than restarting the chunk.
17//! - **Indexed apply** ([`crate::index::IndexApplier`]) — one
18//! [`Checkpoint::Indexed`] per target boundary, plus an interior poll every
19//! 64 regions inside a long target. Same cadence as the existing
20//! cancellation poll, so the cost of opting in is one extra struct
21//! construction per 64 regions and one [`CheckpointSink::record`] call.
22//!
23//! Nothing here consumes a checkpoint — the read side is part of the resume
24//! work that follows. This module is the emit side only: the library writes
25//! checkpoints, the consumer persists them, and a later resume entry point
26//! will load the most recent one and pick up from there.
27//!
28//! # Sink ownership
29//!
30//! [`ApplyContext::with_checkpoint_sink`](crate::ApplyContext::with_checkpoint_sink)
31//! installs a [`CheckpointSink`] for the sequential driver; the indexed
32//! driver picks it up off the same [`crate::ApplyContext`]. The default is
33//! [`NoopCheckpointSink`] — consumers that never opt in pay nothing.
34//!
35//! # Serde
36//!
37//! Every type here derives `serde::Serialize` / `serde::Deserialize` under
38//! the existing `serde` feature so consumers can persist checkpoints
39//! alongside the rest of the indexed-apply data model. The format on disk is
40//! the consumer's choice; the crate does not pin one.
41//!
42//! [`ApplyContext`]: crate::ApplyContext
43
44use std::io;
45use std::path::PathBuf;
46
47/// Persistence policy a [`CheckpointSink`] requests from the apply driver.
48///
49/// After a checkpoint is recorded, the apply driver inspects this value to
50/// decide how aggressively to push pending writes through the operating
51/// system. Cheap sinks (in-memory test capture) ask for [`Self::Flush`];
52/// durability-sensitive sinks (persist-to-disk so a crash recovers cleanly)
53/// ask for [`Self::Fsync`] or [`Self::FsyncEveryN`].
54///
55/// The driver calls [`crate::ApplyContext::sync_all`] when the policy
56/// demands it, which both flushes every cached `BufWriter` and calls
57/// `File::sync_all` on the underlying handle. Honouring this on every
58/// record would gut throughput on patches with millions of regions — hence
59/// [`Self::FsyncEveryN`] for the typical "fsync every N records" cadence
60/// downstream consumers want.
61///
62/// **Mid-block checkpoints** — the per-DEFLATE-block emissions inside
63/// [`crate::chunk::sqpk::SqpkFile`] `AddFile` — never flush and never
64/// fsync regardless of policy. Those emissions fire often enough on a
65/// multi-GB file that interleaving a sync syscall would gut throughput.
66/// The driver guarantees the next chunk-boundary checkpoint flushes the
67/// bytes the mid-block run accumulated in its `BufWriter`, so a resume
68/// from an in-flight checkpoint can never miss data that a later
69/// chunk-boundary checkpoint already covered.
70#[non_exhaustive]
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub enum CheckpointPolicy {
74 /// Flush `BufWriter` buffers to the OS only; no `fsync`. Survives a
75 /// process crash but not an OS crash or power loss between checkpoint
76 /// and recovery.
77 Flush,
78 /// Flush and `fsync` every cached file handle on every recorded
79 /// checkpoint. Strongest durability; pay the syscall cost on every
80 /// record.
81 Fsync,
82 /// Flush every record; `fsync` once every `N` records. `N == 0` is
83 /// rejected at sink-installation time
84 /// ([`crate::ApplyContext::with_checkpoint_sink`] and
85 /// [`crate::IndexApplier::with_checkpoint_sink`] both panic) — use
86 /// [`Self::Fsync`] for "fsync every record" instead.
87 ///
88 /// In-flight mid-block checkpoints (the per-DEFLATE-block emissions
89 /// inside [`crate::chunk::sqpk::SqpkFile`] `AddFile`) **never** fsync
90 /// regardless of policy: those emissions are too frequent to interleave
91 /// with a sync syscall, and the apply driver guarantees that a resume
92 /// from an in-flight checkpoint can never miss data that a later
93 /// chunk-boundary checkpoint already covered.
94 FsyncEveryN(u32),
95}
96
97/// Sink for apply-time checkpoints — installed via
98/// [`crate::ApplyContext::with_checkpoint_sink`] on the sequential driver and
99/// inherited by the indexed driver.
100///
101/// Implement on a struct that owns whatever persistence handle the consumer
102/// uses (a file, a channel, a key-value store, an in-memory `Vec` for tests).
103/// A blanket impl is provided for any
104/// `FnMut(&Checkpoint) -> io::Result<()>` closure for the ad-hoc case.
105///
106/// # Recording semantics
107///
108/// [`Self::record`] runs synchronously inline with the apply loop. The
109/// returned `io::Result` is propagated as a [`crate::ZiPatchError::Io`]; a
110/// failing sink aborts the apply at the boundary just past the chunk or
111/// region that produced the checkpoint. The library does not retry. Sinks
112/// should be cheap — buffering through to an `O_APPEND` write on a small
113/// log file is typical; the [`Self::policy`] return then decides whether the
114/// driver also issues a flush/fsync.
115pub trait CheckpointSink {
116 /// Persist or otherwise record `checkpoint`. Called inline with the
117 /// apply loop at each emission site.
118 fn record(&mut self, checkpoint: &Checkpoint) -> io::Result<()>;
119
120 /// Policy the driver should honour after each successful [`Self::record`].
121 /// Default is [`CheckpointPolicy::Flush`] — strongest available without
122 /// the per-record `fsync` cost.
123 fn policy(&self) -> CheckpointPolicy {
124 CheckpointPolicy::Flush
125 }
126}
127
128/// No-op sink used when no consumer is installed.
129///
130/// Public because [`ApplyContext::with_checkpoint_sink`](crate::ApplyContext::with_checkpoint_sink)
131/// is generic and callers occasionally need to name the default.
132#[derive(Debug, Default, Clone, Copy)]
133pub struct NoopCheckpointSink;
134
135impl CheckpointSink for NoopCheckpointSink {
136 fn record(&mut self, _checkpoint: &Checkpoint) -> io::Result<()> {
137 Ok(())
138 }
139}
140
141impl<F> CheckpointSink for F
142where
143 F: FnMut(&Checkpoint) -> io::Result<()>,
144{
145 fn record(&mut self, checkpoint: &Checkpoint) -> io::Result<()> {
146 self(checkpoint)
147 }
148}
149
150/// One apply-time checkpoint, emitted at a natural recovery boundary.
151///
152/// Two variants, one per apply driver. The `schema_version` field on each
153/// inner struct lets a future resume reader refuse to consume a checkpoint
154/// from an incompatible build rather than silently misinterpret its fields.
155#[non_exhaustive]
156#[derive(Debug, Clone, PartialEq, Eq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub enum Checkpoint {
159 /// Sequential apply driver checkpoint. Emitted per top-level chunk and
160 /// per DEFLATE block inside a long `SqpkFile::AddFile`.
161 Sequential(SequentialCheckpoint),
162 /// Indexed apply driver checkpoint. Emitted per target and every 64
163 /// regions inside a long target.
164 Indexed(IndexedCheckpoint),
165}
166
167/// Sequential-apply checkpoint payload.
168///
169/// Captures "how far into the patch stream the driver has gotten" using two
170/// independent measures so a resume can pick the right one:
171///
172/// - `next_chunk_index` — the zero-based index of the **next** chunk the
173/// driver is about to apply. Equal to the count of chunks that have been
174/// fully applied.
175/// - `bytes_read` — the cumulative byte offset within the patch stream the
176/// driver has read up to. Equivalent to the [`crate::ChunkEvent::bytes_read`]
177/// field at the same emission point.
178///
179/// `in_flight` is `Some` only between DEFLATE block boundaries inside an
180/// `SqpkFile::AddFile`; per-chunk emissions carry `None`. Resuming a sequential
181/// apply that crashed mid-AddFile picks up at `in_flight.block_idx`, seeking
182/// to `in_flight.bytes_into_target` within the target file before resuming the
183/// chunk's block loop.
184#[non_exhaustive]
185#[derive(Debug, Clone, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187pub struct SequentialCheckpoint {
188 /// Layout version of this checkpoint struct. See [`Self::CURRENT_SCHEMA_VERSION`].
189 pub schema_version: u32,
190 /// Index of the next chunk to apply. Equal to the number of chunks that
191 /// have been fully applied as of this checkpoint.
192 pub next_chunk_index: u64,
193 /// Cumulative byte offset within the patch stream the driver has read.
194 ///
195 /// Informational metadata. Resume is positional on `next_chunk_index`,
196 /// not on this counter: the fast-forward re-parses `next_chunk_index`
197 /// chunks and surfaces a `warn!` (but does not error) if the resulting
198 /// `bytes_read` differs from the value recorded here.
199 pub bytes_read: u64,
200 /// Identifier the driver was told to associate with the patch source —
201 /// typically the patch filename. `None` when no identifier was supplied
202 /// via [`crate::ZiPatchReader::with_patch_name`]. Used at resume time
203 /// to detect a checkpoint that was persisted for a different patch.
204 pub patch_name: Option<String>,
205 /// Total byte length of the patch stream, when the driver could measure
206 /// it (i.e. the underlying reader is [`std::io::Seek`]). `None` for the
207 /// sequential `apply_to` path which only requires
208 /// [`std::io::Read`]; populated by `resume_apply_to`. Used together with
209 /// `patch_name` to detect a checkpoint persisted for a patch that has
210 /// since been replaced.
211 ///
212 /// `None` means the recording driver did not know the size (e.g.
213 /// checkpoints captured via [`crate::ZiPatchReader::apply_to`] with a
214 /// `Read`-only source); the resume path will not use it for stale
215 /// detection in that case — the `patch_name` check alone governs.
216 /// Stale-detection mismatch only fires when both the checkpoint and the
217 /// resume side carry a `Some` and the two values disagree.
218 pub patch_size: Option<u64>,
219 /// Mid-chunk state for an in-flight [`crate::chunk::sqpk::SqpkFile`]
220 /// `AddFile`. Present only at per-block emissions; `None` at per-chunk
221 /// emissions.
222 pub in_flight: Option<InFlightAddFile>,
223}
224
225impl SequentialCheckpoint {
226 /// Current schema-version constant for [`SequentialCheckpoint`].
227 pub const CURRENT_SCHEMA_VERSION: u32 = 1;
228
229 /// Construct a [`SequentialCheckpoint`] with the given fields.
230 ///
231 /// Exists because the struct is `#[non_exhaustive]`, which forbids
232 /// external code from using the struct-literal syntax. Consumers
233 /// hand-rolling a synthetic checkpoint (typically in tests, or when
234 /// migrating a persisted checkpoint from an older schema) construct
235 /// one via this method instead.
236 ///
237 /// # Notes
238 ///
239 /// The constructor is intentionally permissive: it accepts any
240 /// combination of fields, including pairings the apply driver itself
241 /// would never emit. Resume re-validates against the patch stream and
242 /// the on-disk state before honouring a checkpoint, so contradictory
243 /// inputs are detected at resume time and either trigger a
244 /// warn-and-restart (stale-detection paths) or surface as a typed
245 /// error — never silent corruption.
246 #[must_use]
247 pub fn new(
248 next_chunk_index: u64,
249 bytes_read: u64,
250 patch_name: Option<String>,
251 patch_size: Option<u64>,
252 in_flight: Option<InFlightAddFile>,
253 ) -> Self {
254 Self {
255 schema_version: Self::CURRENT_SCHEMA_VERSION,
256 next_chunk_index,
257 bytes_read,
258 patch_name,
259 patch_size,
260 in_flight,
261 }
262 }
263
264 /// Return a clone of `self` with `in_flight` overwritten.
265 ///
266 /// Convenience for resume tests / persistence-layer code that needs
267 /// to splice an in-flight payload into an otherwise-untouched
268 /// chunk-boundary checkpoint without re-naming every field.
269 ///
270 /// # Notes
271 ///
272 /// Like [`Self::new`], this is permissive — any
273 /// [`InFlightAddFile`] payload is accepted regardless of whether it
274 /// pairs sensibly with the chunk index or byte offset on `self`. Resume
275 /// re-validates before acting on the in-flight state, so a mismatch
276 /// surfaces as a warn-and-restart rather than silent corruption.
277 #[must_use]
278 pub fn with_in_flight(mut self, in_flight: Option<InFlightAddFile>) -> Self {
279 self.in_flight = in_flight;
280 self
281 }
282}
283
284/// Mid-AddFile state — the DEFLATE block boundary the driver is between.
285///
286/// Resume reads this back, opens `target_path`, seeks to
287/// `file_offset + bytes_into_target`, and re-feeds the chunk's remaining
288/// blocks starting from `block_idx`. The chunk's `path` and `file_offset`
289/// are echoed in full to make the resume self-contained — callers shouldn't
290/// have to cross-reference the original patch stream to interpret the
291/// checkpoint.
292#[non_exhaustive]
293#[derive(Debug, Clone, PartialEq, Eq)]
294#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
295pub struct InFlightAddFile {
296 /// Absolute filesystem path of the target file the `AddFile` writes into.
297 pub target_path: PathBuf,
298 /// The chunk's wire-format `file_offset` — the byte offset within the
299 /// target file at which block 0 starts.
300 pub file_offset: u64,
301 /// Zero-based index of the **next** block to write. Equal to the count
302 /// of blocks already written for this `AddFile`.
303 pub block_idx: u32,
304 /// Total decompressed bytes written into the target file so far for
305 /// this `AddFile`. The current writer position is
306 /// `file_offset + bytes_into_target`.
307 pub bytes_into_target: u64,
308}
309
310impl InFlightAddFile {
311 /// Construct an [`InFlightAddFile`] with the given fields.
312 ///
313 /// Exists because the struct is `#[non_exhaustive]`, which forbids
314 /// external code from using the struct-literal syntax.
315 ///
316 /// # Notes
317 ///
318 /// Permissive by design: any combination of fields is accepted,
319 /// including ones the apply driver would never produce (e.g.
320 /// `block_idx = u32::MAX`, or a `bytes_into_target` that does not
321 /// correspond to any real block boundary). Resume re-validates the
322 /// in-flight state against the patch stream and the on-disk file
323 /// before acting on it; contradictory inputs trigger a warn-and-restart
324 /// path rather than silent corruption.
325 #[must_use]
326 pub fn new(
327 target_path: PathBuf,
328 file_offset: u64,
329 block_idx: u32,
330 bytes_into_target: u64,
331 ) -> Self {
332 Self {
333 target_path,
334 file_offset,
335 block_idx,
336 bytes_into_target,
337 }
338 }
339}
340
341/// Indexed-apply checkpoint payload.
342///
343/// Emitted by the [`crate::index::IndexApplier::execute`] driver at the same
344/// per-target / per-64-regions cadence as the existing cancellation poll.
345///
346/// - `plan_crc32` — identity of the [`crate::index::Plan`] the checkpoint was
347/// produced against, computed via [`crate::index::Plan::crc32`].
348/// [`crate::index::IndexApplier::resume_execute`] re-computes the CRC at
349/// resume time and warns-and-restarts on mismatch (same precedent as the
350/// sequential resume's `patch_name` / `patch_size` check).
351/// - `next_target_idx` — zero-based index of the **next** target the driver
352/// will write into. Equal to the count of fully-written targets.
353/// - `next_region_idx` — zero-based index of the **next** region within
354/// `next_target_idx`'s timeline. Zero at the target boundary; advances by
355/// 64 for each mid-target poll.
356/// - `bytes_written` — cumulative bytes written across all targets and
357/// regions completed so far. Mirrors the same counter the indexed driver
358/// carries internally.
359/// - `fs_ops_done` — `true` once every [`crate::index::FilesystemOp`] in
360/// [`crate::index::Plan::fs_ops`] has been applied. A resume that sees
361/// this `true` skips the `fs_ops` pass; otherwise it re-runs every op
362/// (each op is idempotent w.r.t. the install state the prior partial run
363/// would have left behind).
364#[non_exhaustive]
365#[derive(Debug, Clone, PartialEq, Eq)]
366#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
367pub struct IndexedCheckpoint {
368 /// Layout version of this checkpoint struct. See [`Self::CURRENT_SCHEMA_VERSION`].
369 pub schema_version: u32,
370 /// CRC32 of the [`crate::index::Plan`] this checkpoint was produced
371 /// against. See [`crate::index::Plan::crc32`].
372 ///
373 /// `0` is a legitimate CRC32 output — the hash space is uniform over
374 /// `u32` and a real plan can hash to zero. Consumers must represent
375 /// "no checkpoint yet" via `Option<IndexedCheckpoint>` (i.e. pass
376 /// `None` to [`crate::index::IndexApplier::resume_execute`]); a
377 /// sentinel `plan_crc32: 0` would collide with that legitimate output
378 /// and either trigger a spurious warn-and-restart against a plan that
379 /// happens to hash to zero, or silently accept a stale checkpoint
380 /// from a plan that does.
381 pub plan_crc32: u32,
382 /// `true` once every [`crate::index::FilesystemOp`] in
383 /// [`crate::index::Plan::fs_ops`] has been applied.
384 pub fs_ops_done: bool,
385 /// Index of the next target to apply. Equal to the number of targets
386 /// fully written at this checkpoint.
387 pub next_target_idx: u64,
388 /// Index of the next region within `next_target_idx`'s timeline.
389 pub next_region_idx: u64,
390 /// Cumulative bytes written across all targets and regions completed.
391 pub bytes_written: u64,
392}
393
394impl IndexedCheckpoint {
395 /// Current schema-version constant for [`IndexedCheckpoint`].
396 ///
397 /// Bumped to `2` alongside the addition of `plan_crc32` and
398 /// `fs_ops_done`; a `1`-vintage persisted checkpoint will fail
399 /// `check_schema_version`-style validation at resume time.
400 pub const CURRENT_SCHEMA_VERSION: u32 = 2;
401
402 /// Construct an [`IndexedCheckpoint`] with the given fields.
403 ///
404 /// Exists because the struct is `#[non_exhaustive]`, which forbids
405 /// external code from using the struct-literal syntax. Consumers
406 /// hand-rolling a synthetic checkpoint (typically in tests, or when
407 /// migrating a persisted checkpoint from an older schema) construct
408 /// one via this method instead.
409 ///
410 /// # Notes
411 ///
412 /// Permissive by design: any combination of fields is accepted,
413 /// including pairings the indexed driver would never emit (e.g.
414 /// `fs_ops_done: false` with `next_target_idx > 0`).
415 /// [`crate::index::IndexApplier::resume_execute`] re-validates the
416 /// checkpoint's `plan_crc32` against the plan it is handed, and
417 /// re-runs every step that has not been confirmed durable; a
418 /// contradictory checkpoint either matches a re-derivable resume
419 /// state or triggers the warn-and-restart path on CRC mismatch.
420 #[must_use]
421 pub fn new(
422 plan_crc32: u32,
423 fs_ops_done: bool,
424 next_target_idx: u64,
425 next_region_idx: u64,
426 bytes_written: u64,
427 ) -> Self {
428 Self {
429 schema_version: Self::CURRENT_SCHEMA_VERSION,
430 plan_crc32,
431 fs_ops_done,
432 next_target_idx,
433 next_region_idx,
434 bytes_written,
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use std::sync::Arc;
443 use std::sync::Mutex;
444
445 // --- NoopCheckpointSink ---
446
447 #[test]
448 fn noop_sink_records_succeed_for_every_variant() {
449 let mut sink = NoopCheckpointSink;
450 let seq = Checkpoint::Sequential(SequentialCheckpoint {
451 schema_version: SequentialCheckpoint::CURRENT_SCHEMA_VERSION,
452 next_chunk_index: 3,
453 bytes_read: 1024,
454 patch_name: None,
455 patch_size: None,
456 in_flight: None,
457 });
458 let indexed = Checkpoint::Indexed(IndexedCheckpoint {
459 schema_version: IndexedCheckpoint::CURRENT_SCHEMA_VERSION,
460 plan_crc32: 0xDEAD_BEEF,
461 fs_ops_done: true,
462 next_target_idx: 7,
463 next_region_idx: 128,
464 bytes_written: 65536,
465 });
466
467 sink.record(&seq).expect("Noop must succeed");
468 sink.record(&indexed).expect("Noop must succeed");
469 }
470
471 #[test]
472 fn noop_sink_default_policy_is_flush() {
473 let sink = NoopCheckpointSink;
474 assert_eq!(sink.policy(), CheckpointPolicy::Flush);
475 }
476
477 // --- Closure blanket impl ---
478
479 #[test]
480 fn closure_sink_captures_records_in_order() {
481 let captured: Arc<Mutex<Vec<Checkpoint>>> = Arc::new(Mutex::new(Vec::new()));
482 let captured_clone = captured.clone();
483 let mut sink = move |c: &Checkpoint| -> io::Result<()> {
484 captured_clone.lock().unwrap().push(c.clone());
485 Ok(())
486 };
487
488 let a = Checkpoint::Sequential(SequentialCheckpoint {
489 schema_version: SequentialCheckpoint::CURRENT_SCHEMA_VERSION,
490 next_chunk_index: 1,
491 bytes_read: 32,
492 patch_name: None,
493 patch_size: None,
494 in_flight: None,
495 });
496 let b = Checkpoint::Sequential(SequentialCheckpoint {
497 schema_version: SequentialCheckpoint::CURRENT_SCHEMA_VERSION,
498 next_chunk_index: 2,
499 bytes_read: 64,
500 patch_name: None,
501 patch_size: None,
502 in_flight: None,
503 });
504
505 sink.record(&a).unwrap();
506 sink.record(&b).unwrap();
507
508 let got = captured.lock().unwrap();
509 assert_eq!(got.len(), 2);
510 assert_eq!(got[0], a);
511 assert_eq!(got[1], b);
512 }
513
514 #[test]
515 fn closure_sink_propagates_io_error_unchanged() {
516 let mut sink = |_: &Checkpoint| -> io::Result<()> { Err(io::Error::other("synthetic")) };
517 let c = Checkpoint::Indexed(IndexedCheckpoint {
518 schema_version: IndexedCheckpoint::CURRENT_SCHEMA_VERSION,
519 plan_crc32: 0,
520 fs_ops_done: false,
521 next_target_idx: 0,
522 next_region_idx: 0,
523 bytes_written: 0,
524 });
525 let err = sink.record(&c).unwrap_err();
526 assert_eq!(err.kind(), io::ErrorKind::Other);
527 assert_eq!(err.to_string(), "synthetic");
528 }
529
530 // --- Schema-version constants ---
531
532 #[test]
533 fn schema_version_constants_have_expected_values() {
534 assert_eq!(SequentialCheckpoint::CURRENT_SCHEMA_VERSION, 1);
535 assert_eq!(IndexedCheckpoint::CURRENT_SCHEMA_VERSION, 2);
536 }
537
538 // --- Serde round-trip (only meaningful under the serde feature) ---
539
540 #[cfg(feature = "serde")]
541 #[test]
542 fn bincode_round_trips_sequential_checkpoint_without_in_flight() {
543 let cp = Checkpoint::Sequential(SequentialCheckpoint {
544 schema_version: SequentialCheckpoint::CURRENT_SCHEMA_VERSION,
545 next_chunk_index: 42,
546 bytes_read: 0x1_0000_0000,
547 patch_name: Some("H2017.07.11.0000.0000a.patch".into()),
548 patch_size: Some(0x2_0000_0000),
549 in_flight: None,
550 });
551
552 let cfg = bincode::config::standard();
553 let bytes = bincode::serde::encode_to_vec(&cp, cfg).unwrap();
554 let (decoded, _): (Checkpoint, _) = bincode::serde::decode_from_slice(&bytes, cfg).unwrap();
555 assert_eq!(cp, decoded);
556 }
557
558 #[cfg(feature = "serde")]
559 #[test]
560 fn bincode_round_trips_sequential_checkpoint_with_in_flight() {
561 let cp = Checkpoint::Sequential(SequentialCheckpoint {
562 schema_version: SequentialCheckpoint::CURRENT_SCHEMA_VERSION,
563 next_chunk_index: 9,
564 bytes_read: 1_048_576,
565 patch_name: Some("patch.patch".into()),
566 patch_size: Some(1_048_576 * 4),
567 in_flight: Some(InFlightAddFile {
568 target_path: PathBuf::from("/install/sqpack/ffxiv/000000.win32.dat0"),
569 file_offset: 0,
570 block_idx: 17,
571 bytes_into_target: 17 * 16_384,
572 }),
573 });
574
575 let cfg = bincode::config::standard();
576 let bytes = bincode::serde::encode_to_vec(&cp, cfg).unwrap();
577 let (decoded, _): (Checkpoint, _) = bincode::serde::decode_from_slice(&bytes, cfg).unwrap();
578 assert_eq!(cp, decoded);
579 }
580
581 #[cfg(feature = "serde")]
582 #[test]
583 fn bincode_round_trips_indexed_checkpoint() {
584 let cp = Checkpoint::Indexed(IndexedCheckpoint {
585 schema_version: IndexedCheckpoint::CURRENT_SCHEMA_VERSION,
586 plan_crc32: 0xA5A5_5A5A,
587 fs_ops_done: true,
588 next_target_idx: 12,
589 next_region_idx: 64,
590 bytes_written: 12345,
591 });
592
593 let cfg = bincode::config::standard();
594 let bytes = bincode::serde::encode_to_vec(&cp, cfg).unwrap();
595 let (decoded, _): (Checkpoint, _) = bincode::serde::decode_from_slice(&bytes, cfg).unwrap();
596 assert_eq!(cp, decoded);
597 }
598
599 #[cfg(feature = "serde")]
600 #[test]
601 fn bincode_round_trips_checkpoint_policy_variants() {
602 for policy in [
603 CheckpointPolicy::Flush,
604 CheckpointPolicy::Fsync,
605 CheckpointPolicy::FsyncEveryN(64),
606 CheckpointPolicy::FsyncEveryN(1),
607 ] {
608 let cfg = bincode::config::standard();
609 let bytes = bincode::serde::encode_to_vec(&policy, cfg).unwrap();
610 let (decoded, _): (CheckpointPolicy, _) =
611 bincode::serde::decode_from_slice(&bytes, cfg).unwrap();
612 assert_eq!(policy, decoded);
613 }
614 }
615}