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