Skip to main content

safari_binarycookies/
model.rs

1//! Decoded data model: [`BinaryCookies`], [`Page`], [`Cookie`], [`Flags`].
2
3use core::fmt;
4
5use time::OffsetDateTime;
6
7/// The [`Iterator`] over every cookie in a [`BinaryCookies`], in file order,
8/// returned by [`BinaryCookies::iter`] and `&BinaryCookies`'s [`IntoIterator`].
9pub type CookieIter<'a> = core::iter::FlatMap<
10    core::slice::Iter<'a, Page>,
11    &'a Vec<Cookie>,
12    fn(&'a Page) -> &'a Vec<Cookie>,
13>;
14
15/// A fully decoded `.binarycookies` file.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17// The serde impls are derive-generated, so rustdoc cannot attach a
18// `doc(cfg(feature = "serde"))` badge to them on docs.rs; the feature gating is
19// real regardless, and hand-writing the impls just to badge them is not worth it.
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[non_exhaustive]
22pub struct BinaryCookies {
23    /// The pages in file order, including empty ones.
24    pub pages: Vec<Page>,
25    /// The trailing 8-byte checksum, stored as-is and never verified, matching
26    /// the Go reference implementation.
27    pub checksum: [u8; 8],
28}
29
30impl BinaryCookies {
31    /// Iterates over every cookie across all pages, in file order.
32    ///
33    /// The borrowing counterpart of the lazy [`cookies`](crate::cookies)
34    /// stream, for when the file is already fully decoded.
35    pub fn cookies(&self) -> impl Iterator<Item = &Cookie> {
36        self.iter()
37    }
38
39    /// Borrowing iterator over every cookie across all pages, in file order.
40    ///
41    /// The same sequence as [`cookies`](Self::cookies) but with the nameable
42    /// [`CookieIter`] return type; `&BinaryCookies` implements
43    /// [`IntoIterator`] too, so `for cookie in &jar` works.
44    pub fn iter(&self) -> CookieIter<'_> {
45        self.pages.iter().flat_map(|page| &page.cookies)
46    }
47}
48
49impl<'a> IntoIterator for &'a BinaryCookies {
50    type Item = &'a Cookie;
51    type IntoIter = CookieIter<'a>;
52
53    fn into_iter(self) -> Self::IntoIter {
54        self.iter()
55    }
56}
57
58/// A single page, grouping the cookies of one domain.
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[non_exhaustive]
62pub struct Page {
63    /// The cookies in record order.
64    pub cookies: Vec<Cookie>,
65    /// The page's raw cookie-offset table, kept for forensic verification.
66    /// Decoding never depends on it: cookies are consumed sequentially.
67    pub offsets: Vec<u32>,
68}
69
70impl Page {
71    /// Borrowing iterator over this page's cookies, in record order.
72    ///
73    /// `&Page` implements [`IntoIterator`] too, so `for cookie in &page` works.
74    pub fn iter(&self) -> core::slice::Iter<'_, Cookie> {
75        self.cookies.iter()
76    }
77}
78
79impl<'a> IntoIterator for &'a Page {
80    type Item = &'a Cookie;
81    type IntoIter = core::slice::Iter<'a, Cookie>;
82
83    fn into_iter(self) -> Self::IntoIter {
84        self.cookies.iter()
85    }
86}
87
88/// A single decoded HTTP cookie.
89///
90/// With the `serde` feature, timestamps serialize as RFC 3339, which cannot
91/// represent negative years: serializing a cookie whose crafted on-disk
92/// timestamp decoded (clamped) to a year below 0000 returns a serialization
93/// error — never a panic — matching Go's `time.Time` JSON marshaling limits.
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96#[non_exhaustive]
97pub struct Cookie {
98    /// The cookie domain, with its single trailing NUL terminator removed.
99    pub domain: String,
100    /// The cookie name, with its single trailing NUL terminator removed.
101    pub name: String,
102    /// The cookie path, with its single trailing NUL terminator removed.
103    pub path: String,
104    /// The cookie value, truncated at the first NUL byte.
105    pub value: String,
106    /// The optional cookie comment, stored verbatim (including any trailing
107    /// NUL). `None` when the record's comment offset is zero.
108    pub comment: Option<String>,
109    /// The raw flag bits.
110    pub flags: Flags,
111    /// Expiration time, decoded from the on-disk Mac-epoch seconds.
112    #[cfg_attr(feature = "serde", serde(with = "time::serde::rfc3339"))]
113    pub expires: OffsetDateTime,
114    /// Creation time, decoded from the on-disk Mac-epoch seconds.
115    #[cfg_attr(feature = "serde", serde(with = "time::serde::rfc3339"))]
116    pub creation: OffsetDateTime,
117}
118
119impl Cookie {
120    /// Whether the `Secure` flag bit (`0x1`) is set.
121    #[must_use]
122    pub const fn is_secure(&self) -> bool {
123        self.flags.is_secure()
124    }
125
126    /// Whether the `HttpOnly` flag bit (`0x4`) is set.
127    #[must_use]
128    pub const fn is_http_only(&self) -> bool {
129        self.flags.is_http_only()
130    }
131
132    /// Expiration time as seconds since the Unix epoch.
133    ///
134    /// Out-of-range on-disk timestamps are clamped by the decoder into the
135    /// range supported by [`time`] (years ±9999), so this returns the clamped
136    /// value; a NaN timestamp decodes to `0`.
137    #[must_use]
138    pub const fn expires_unix(&self) -> i64 {
139        self.expires.unix_timestamp()
140    }
141
142    /// Creation time as seconds since the Unix epoch.
143    ///
144    /// Clamped exactly like [`Cookie::expires_unix`].
145    #[must_use]
146    pub const fn creation_unix(&self) -> i64 {
147        self.creation.unix_timestamp()
148    }
149}
150
151/// Raw cookie flag bits with typed accessors.
152///
153/// Real Safari files use `0x0`, `0x1` (`Secure`), `0x4` (`HttpOnly`) or `0x5`;
154/// unknown bits are preserved and readable through [`Flags::bits`].
155#[derive(Clone, Copy, PartialEq, Eq, Hash)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157#[non_exhaustive]
158pub struct Flags(u32);
159
160impl Flags {
161    /// The `Secure` bit.
162    pub const SECURE: u32 = 0x1;
163    /// The `HttpOnly` bit.
164    pub const HTTP_ONLY: u32 = 0x4;
165
166    pub(crate) const fn new(bits: u32) -> Self {
167        Self(bits)
168    }
169
170    /// The raw flag bits as stored on disk.
171    #[must_use]
172    pub const fn bits(self) -> u32 {
173        self.0
174    }
175
176    /// Whether `bit` is fully set.
177    #[must_use]
178    pub const fn contains(self, bit: u32) -> bool {
179        self.0 & bit == bit
180    }
181
182    /// Whether the [`Secure`](Self::SECURE) bit is set.
183    #[must_use]
184    pub const fn is_secure(self) -> bool {
185        self.contains(Self::SECURE)
186    }
187
188    /// Whether the [`HttpOnly`](Self::HTTP_ONLY) bit is set.
189    #[must_use]
190    pub const fn is_http_only(self) -> bool {
191        self.contains(Self::HTTP_ONLY)
192    }
193}
194
195impl fmt::Debug for Flags {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        f.debug_struct("Flags")
198            .field("bits", &format_args!("{:#x}", self.0))
199            .field("secure", &self.is_secure())
200            .field("http_only", &self.is_http_only())
201            .finish()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    fn assert_send_sync<T: Send + Sync>() {}
210
211    #[test]
212    fn public_types_are_send_and_sync() {
213        assert_send_sync::<BinaryCookies>();
214        assert_send_sync::<Page>();
215        assert_send_sync::<Cookie>();
216        assert_send_sync::<Flags>();
217        assert_send_sync::<crate::Error>();
218        assert_send_sync::<crate::Cookies<'static>>();
219    }
220
221    #[test]
222    fn flags_read_bits_via_bitmask() {
223        assert!(!Flags::new(0x0).is_secure() && !Flags::new(0x0).is_http_only());
224        assert!(Flags::new(0x1).is_secure() && !Flags::new(0x1).is_http_only());
225        assert!(!Flags::new(0x4).is_secure() && Flags::new(0x4).is_http_only());
226        assert!(Flags::new(0x5).is_secure() && Flags::new(0x5).is_http_only());
227        assert_eq!(Flags::new(0x2).bits(), 0x2);
228    }
229}