Skip to main content

zipatch_rs/
newtypes.rs

1//! Strongly-typed wrappers around primitive identifiers that appear in the
2//! public API.
3//!
4//! Each newtype here exists to give a domain concept a single canonical Rust
5//! type, instead of leaking the raw primitive (`u32`, `[u8; 4]`, …) at every
6//! call site. The wrappers are `#[repr(transparent)]` so they carry no runtime
7//! cost compared to the underlying primitive.
8//!
9//! - [`PatchIndex`](crate::newtypes::PatchIndex) — a 0-based index into a
10//!   multi-patch chain.
11//! - [`ChunkTag`](crate::newtypes::ChunkTag) — a 4-byte ASCII chunk tag
12//!   (`FHDR`, `APLY`, `SQPK`, …).
13//! - [`SchemaVersion`](crate::newtypes::SchemaVersion) — a persisted record's
14//!   schema-format version.
15
16use std::fmt;
17
18// ---------------------------------------------------------------------------
19// PatchIndex
20// ---------------------------------------------------------------------------
21
22/// Zero-based index of a patch within a multi-patch chain.
23///
24/// Used by [`crate::index::PatchSource::read`] and surfaced in
25/// [`crate::IndexError::PatchIndexOutOfRange`] to identify which patch in the
26/// chain a [`crate::index::Plan`] is referring to. The first patch added to a
27/// [`crate::index::PlanBuilder`] is `PatchIndex(0)`, the second
28/// `PatchIndex(1)`, and so on.
29#[repr(transparent)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct PatchIndex(u32);
33
34impl PatchIndex {
35    /// Construct a [`PatchIndex`] from a raw `u32`.
36    #[must_use]
37    pub const fn new(idx: u32) -> Self {
38        Self(idx)
39    }
40
41    /// Return the wrapped `u32`.
42    #[must_use]
43    pub const fn get(self) -> u32 {
44        self.0
45    }
46}
47
48impl fmt::Display for PatchIndex {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        self.0.fmt(f)
51    }
52}
53
54impl From<u32> for PatchIndex {
55    fn from(v: u32) -> Self {
56        Self(v)
57    }
58}
59
60impl From<PatchIndex> for u32 {
61    fn from(v: PatchIndex) -> Self {
62        v.0
63    }
64}
65
66// ---------------------------------------------------------------------------
67// ChunkTag
68// ---------------------------------------------------------------------------
69
70/// A 4-byte ASCII chunk tag identifying the wire-format kind of a `ZiPatch`
71/// chunk frame (`FHDR`, `APLY`, `SQPK`, `EOF_`, …).
72///
73/// Construct via [`ChunkTag::new`] / [`ChunkTag::from_bytes`] or one of the
74/// well-known constants (e.g. [`ChunkTag::SQPK`]). Inspect the raw bytes via
75/// [`ChunkTag::as_bytes`].
76///
77/// The [`Display`](std::fmt::Display) impl renders printable ASCII bytes
78/// directly, NUL bytes as `_`, and any other byte as `.` — matching the
79/// `zipatch dump` CLI's chunk-tag rendering.
80#[repr(transparent)]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct ChunkTag([u8; 4]);
83
84impl ChunkTag {
85    /// `FHDR` — the file-header chunk that opens every `ZiPatch` stream.
86    pub const FHDR: ChunkTag = ChunkTag(*b"FHDR");
87    /// `APLY` — sets/clears a boolean apply-time flag.
88    pub const APLY: ChunkTag = ChunkTag(*b"APLY");
89    /// `APFS` — apply free-space book-keeping (no-op at apply time).
90    pub const APFS: ChunkTag = ChunkTag(*b"APFS");
91    /// `ADIR` — create a directory under the install root.
92    pub const ADIR: ChunkTag = ChunkTag(*b"ADIR");
93    /// `DELD` — delete a directory under the install root.
94    pub const DELD: ChunkTag = ChunkTag(*b"DELD");
95    /// `SQPK` — the SQPK workhorse chunk wrapping one of eight sub-commands.
96    pub const SQPK: ChunkTag = ChunkTag(*b"SQPK");
97    /// `EOF_` — end-of-stream terminator.
98    pub const EOF: ChunkTag = ChunkTag(*b"EOF_");
99
100    /// Construct a [`ChunkTag`] from a 4-byte array.
101    #[must_use]
102    pub const fn new(bytes: [u8; 4]) -> Self {
103        Self(bytes)
104    }
105
106    /// Construct a [`ChunkTag`] from a borrowed 4-byte slice.
107    #[must_use]
108    pub const fn from_bytes(bytes: &[u8; 4]) -> Self {
109        Self(*bytes)
110    }
111
112    /// Return the raw 4 bytes of the tag.
113    #[must_use]
114    pub const fn as_bytes(&self) -> &[u8; 4] {
115        &self.0
116    }
117
118    /// Return the tag as a `&str` if every byte is valid UTF-8.
119    ///
120    /// All wire tags defined by the `ZiPatch` format are 4-byte ASCII, so
121    /// this returns `Some(&str)` in practice; the fallible variant exists
122    /// because the underlying field type does not constrain bytes to UTF-8.
123    #[must_use]
124    pub fn as_str(&self) -> Option<&str> {
125        std::str::from_utf8(&self.0).ok()
126    }
127}
128
129impl fmt::Display for ChunkTag {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        let mut buf = [0u8; 4];
132        for (out, b) in buf.iter_mut().zip(self.0.iter()) {
133            *out = if b.is_ascii_graphic() || *b == b' ' {
134                *b
135            } else if *b == 0 {
136                b'_'
137            } else {
138                b'.'
139            };
140        }
141        // SAFETY: every byte written into `buf` is ASCII (graphic, space, `_`,
142        // or `.`), so the slice is valid UTF-8.
143        f.write_str(std::str::from_utf8(&buf).unwrap_or("????"))
144    }
145}
146
147impl From<[u8; 4]> for ChunkTag {
148    fn from(v: [u8; 4]) -> Self {
149        Self(v)
150    }
151}
152
153impl From<ChunkTag> for [u8; 4] {
154    fn from(v: ChunkTag) -> Self {
155        v.0
156    }
157}
158
159impl PartialEq<[u8; 4]> for ChunkTag {
160    fn eq(&self, other: &[u8; 4]) -> bool {
161        &self.0 == other
162    }
163}
164
165impl PartialEq<ChunkTag> for [u8; 4] {
166    fn eq(&self, other: &ChunkTag) -> bool {
167        self == &other.0
168    }
169}
170
171// ---------------------------------------------------------------------------
172// SchemaVersion
173// ---------------------------------------------------------------------------
174
175/// Schema-format version stamped on a persisted record (e.g. a
176/// [`crate::apply::Checkpoint`] or a [`crate::index::Plan`]).
177///
178/// Strict-equality compatibility: a persisted record with a `SchemaVersion`
179/// that does not equal the build's `CURRENT_SCHEMA_VERSION` is refused rather
180/// than silently re-interpreted. Use [`SchemaVersion::compatible_with`] to
181/// perform the comparison.
182#[repr(transparent)]
183#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185pub struct SchemaVersion(u32);
186
187impl SchemaVersion {
188    /// Construct a [`SchemaVersion`] from a raw `u32`.
189    #[must_use]
190    pub const fn new(v: u32) -> Self {
191        Self(v)
192    }
193
194    /// Return the wrapped `u32`.
195    #[must_use]
196    pub const fn get(self) -> u32 {
197        self.0
198    }
199
200    /// Returns `true` if `self` is compatible with `other`.
201    ///
202    /// Compatibility is strict equality: persisted records carrying a
203    /// different version cannot be silently consumed against the current
204    /// schema, because the layout may have shifted in either direction.
205    #[must_use]
206    pub const fn compatible_with(self, other: Self) -> bool {
207        self.0 == other.0
208    }
209
210    /// Return a [`SchemaVersion`] whose inner `u32` is
211    /// `self.get().wrapping_add(rhs)`. Intended for tests that need to
212    /// fabricate a "version off by one" value.
213    #[must_use]
214    pub const fn wrapping_add(self, rhs: u32) -> Self {
215        Self(self.0.wrapping_add(rhs))
216    }
217
218    /// Return a [`SchemaVersion`] whose inner `u32` is
219    /// `self.get().wrapping_sub(rhs)`. Intended for tests that need to
220    /// fabricate a "version off by one" value.
221    #[must_use]
222    pub const fn wrapping_sub(self, rhs: u32) -> Self {
223        Self(self.0.wrapping_sub(rhs))
224    }
225}
226
227impl fmt::Display for SchemaVersion {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        self.0.fmt(f)
230    }
231}
232
233impl From<u32> for SchemaVersion {
234    fn from(v: u32) -> Self {
235        Self(v)
236    }
237}
238
239impl From<SchemaVersion> for u32 {
240    fn from(v: SchemaVersion) -> Self {
241        v.0
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn patch_index_round_trip() {
251        let p = PatchIndex::new(7);
252        assert_eq!(p.get(), 7);
253        assert_eq!(u32::from(p), 7);
254        assert_eq!(PatchIndex::from(7u32), p);
255    }
256
257    #[test]
258    fn chunk_tag_display_printable_ascii() {
259        assert_eq!(format!("{}", ChunkTag::SQPK), "SQPK");
260        assert_eq!(format!("{}", ChunkTag::EOF), "EOF_");
261        assert_eq!(format!("{}", ChunkTag::new([0, b'A', b'B', b'C'])), "_ABC");
262        assert_eq!(
263            format!("{}", ChunkTag::new([0xff, b'A', b'B', b'C'])),
264            ".ABC"
265        );
266    }
267
268    #[test]
269    fn chunk_tag_constants_match_ascii() {
270        assert_eq!(ChunkTag::FHDR.as_bytes(), b"FHDR");
271        assert_eq!(ChunkTag::SQPK.as_bytes(), b"SQPK");
272        assert_eq!(ChunkTag::EOF.as_bytes(), b"EOF_");
273    }
274
275    #[test]
276    fn chunk_tag_eq_array() {
277        let tag = ChunkTag::SQPK;
278        assert!(tag == *b"SQPK");
279        assert!(*b"SQPK" == tag);
280    }
281
282    #[test]
283    fn schema_version_compat_is_strict_equality() {
284        let a = SchemaVersion::new(1);
285        let b = SchemaVersion::new(1);
286        let c = SchemaVersion::new(2);
287        assert!(a.compatible_with(b));
288        assert!(!a.compatible_with(c));
289    }
290}