binary_cookies/
cookie.rs

1use std::num::NonZeroUsize;
2
3use bstr::{BString, ByteSlice as _};
4use chrono::{DateTime, TimeZone as _, Utc};
5use winnow::{
6    binary::{be_u32, be_u64, be_u8, le_f64, le_u16, le_u32},
7    combinator::repeat,
8    error::{ContextError, ErrMode, FromExternalError, Needed, StrContext, StrContextValue},
9    token::take,
10    ModalResult, Parser,
11};
12
13use crate::{
14    decode::{
15        binary_cookies::Offsets, cookies::CookiesOffsetInPage, F64ToSafariTime as _, StreamIn,
16    },
17    error::{BplistErr, ExpectErr},
18};
19
20/// raw file information, with pages
21#[derive(Clone)]
22#[derive(Debug)]
23#[derive(Default)]
24#[derive(PartialEq)]
25#[cfg_attr(not(test), derive(Eq))]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[expect(
28    clippy::exhaustive_structs,
29    reason = "Breaking change with Binarycookies format"
30)]
31pub struct BinaryCookies {
32    pub pages: Vec<Page>,
33    pub metadata: Option<Metadata>,
34}
35
36pub type Checksum = u32;
37
38impl BinaryCookies {
39    pub(crate) fn decode_head(input: &mut StreamIn) -> ModalResult<Offsets> {
40        if input.len() < 8 {
41            return Err(ErrMode::Incomplete(Needed::Size(unsafe {
42                NonZeroUsize::new_unchecked(8 - input.len())
43            })));
44        }
45        let magic = take(4_usize).parse_next(input)?;
46        if magic != Self::MAGIC {
47            #[expect(clippy::unwrap_used, reason = "magic len is 4")]
48            let arr: [u8; 4] = magic.try_into().unwrap();
49            let mut context_error = ContextError::from_external_error(input, ExpectErr::Magic(arr));
50            context_error.extend([
51                StrContext::Label("BinaryCookies magic broken"),
52                StrContext::Expected(StrContextValue::Description(r#"Expected magic: `b"cook"`"#)),
53            ]);
54            return Err(ErrMode::Cut(context_error));
55        }
56        let num_pages = be_u32(input)? as usize;
57        let pages_size = num_pages * 4;
58
59        if input.len() < pages_size {
60            let size = unsafe { NonZeroUsize::new_unchecked(pages_size - input.len()) };
61            return Err(ErrMode::Incomplete(Needed::Size(size)));
62        }
63
64        let page_sizes: Vec<u32> = repeat(num_pages..num_pages + 1, be_u32).parse_next(input)?;
65
66        let tail_offset = 4
67            + 4
68            + num_pages as u64 * 4
69            + page_sizes
70                .iter()
71                .map(|&v| v as u64)
72                .sum::<u64>();
73        Ok(Offsets { page_sizes, tail_offset })
74    }
75
76    pub(crate) fn decode_tail(input: &mut StreamIn) -> ModalResult<(Checksum, Option<Metadata>)> {
77        if input.len() < 4 + 8 {
78            return Err(ErrMode::Incomplete(Needed::Size(unsafe {
79                NonZeroUsize::new_unchecked(4 + 8 - input.len())
80            })));
81        }
82        let checksum = be_u32(input)?;
83        let footer = be_u64(input)?;
84        if footer != Self::FOOTER {
85            let mut ctx_err = ContextError::from_external_error(input, ExpectErr::U64(footer));
86            ctx_err.extend([
87                StrContext::Label("BinaryCookies footer broken"),
88                StrContext::Expected(StrContextValue::Description(
89                    r#"Expected big endian: `0x071720050000004b_u64`"#,
90                )),
91            ]);
92            return Err(ErrMode::Cut(ctx_err));
93        }
94
95        let metadata = Metadata::decode(input).ok();
96        Ok((checksum, metadata))
97    }
98}
99
100impl BinaryCookies {
101    pub const MAGIC: &'static [u8] = b"cook"; // 0 offset, 4 size
102    pub const FOOTER: u64 = 0x071720050000004B;
103
104    pub fn push(&mut self, page: Page) {
105        self.pages.push(page);
106    }
107
108    pub fn page_sizes(&self) -> Vec<u32> {
109        self.pages
110            .iter()
111            .map(Page::size)
112            .collect()
113    }
114
115    pub fn iter_pages(&self) -> impl Iterator<Item = &Page> {
116        self.pages.iter()
117    }
118
119    /// iter all pages cookies
120    pub fn iter_cookies(&self) -> impl Iterator<Item = &Cookie> {
121        self.iter_pages()
122            .flat_map(Page::iter_cookies)
123    }
124
125    /// FIXME: checksum impl not correct
126    pub fn checksum(&self) -> u32 {
127        self.pages
128            .iter()
129            .fold(0_u32, |i, v| v.encode().1.wrapping_add(i))
130    }
131    pub fn encode(&self) -> Vec<u8> {
132        let mut raw = Self::MAGIC.to_vec();
133        raw.extend_from_slice(&(self.pages.len() as u32).to_be_bytes());
134        for ele in self.iter_pages() {
135            raw.extend_from_slice(&ele.size().to_be_bytes());
136        }
137
138        // FIXME: checksum impl not correct
139        let checksum = self
140            .pages
141            .iter()
142            .fold(0_u32, |i, v| {
143                let (data, sum) = v.encode();
144                raw.extend_from_slice(&data);
145                i.wrapping_add(sum)
146            });
147
148        raw.extend_from_slice(&checksum.to_be_bytes());
149        raw.extend_from_slice(&Self::FOOTER.to_be_bytes());
150        if let Some(meta) = &self.metadata {
151            raw.extend_from_slice(&meta.encode());
152        }
153        raw
154    }
155}
156
157#[derive(Clone)]
158#[derive(Debug)]
159#[derive(Default)]
160#[derive(PartialEq, Eq)]
161#[derive(PartialOrd, Ord)]
162#[cfg_attr(
163    any(test, feature = "serde"),
164    derive(serde::Serialize, serde::Deserialize)
165)]
166#[expect(
167    clippy::exhaustive_structs,
168    reason = "Breaking change with Binarycookies format"
169)]
170pub struct Metadata {
171    #[cfg_attr(test, serde(rename = "NSHTTPCookieAcceptPolicy"))]
172    pub nshttp_cookie_accept_policy: u8,
173}
174
175impl Metadata {
176    #[rustfmt::skip]
177    // This is a very specialized decoder that needs to be updated with the BinaryCookies format
178    pub const fn encode(&self) -> [u8; 75] {
179        [
180            98, 112, 108, 105, 115, 116, 48, 48, 209, 1, 2, 95, 16, 24, 78, 83, 72, 84, 84, 80, 67,
181            111, 111, 107, 105, 101, 65, 99, 99, 101, 112, 116, 80, 111, 108, 105, 99, 121, 16,
182            self.nshttp_cookie_accept_policy,
183            8, 11, 38, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 3,
184            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40,
185        ]
186    }
187    // See apple opensource CFBinaryPList.c
188    // This is a very specialized decoder that needs to be updated with the BinaryCookies format
189    pub(crate) fn decode(input: &mut StreamIn) -> Result<Self, ErrMode<ContextError>> {
190        if input.len() < 75 {
191            return Err(ErrMode::Incomplete(Needed::Size(unsafe {
192                NonZeroUsize::new_unchecked(75 - input.len())
193            })));
194        }
195        let bplist = take(8_usize).parse_next(input)?;
196        if bplist != b"bplist00" {
197            let ctx_err = ContextError::from_external_error(input, BplistErr::Magic);
198            return Err(ErrMode::Cut(ctx_err));
199        }
200        let dict = be_u8(input)?;
201        if dict != 0xD1 {
202            let ctx_err = ContextError::from_external_error(input, BplistErr::NotDict);
203            return Err(ErrMode::Cut(ctx_err));
204        }
205        let _length = take(5_usize).parse_next(input)?;
206        let key = take(24_usize).parse_next(input)?;
207        if b"NSHTTPCookieAcceptPolicy" != key {
208            let ctx_err = ContextError::from_external_error(input, BplistErr::BadKey);
209            return Err(ErrMode::Cut(ctx_err));
210        }
211        let int_flags = be_u8(input)?;
212        if int_flags != 0x10 {
213            let ctx_err = ContextError::from_external_error(input, BplistErr::OneByteInt);
214            return Err(ErrMode::Cut(ctx_err));
215        }
216        let int_val = be_u8(input)?;
217        take(32 + 3_usize).parse_next(input)?;
218        let metadata = Self {
219            nshttp_cookie_accept_policy: int_val,
220        };
221        Ok(metadata)
222    }
223}
224
225#[derive(Clone)]
226#[derive(Debug)]
227#[derive(Default)]
228#[derive(PartialEq)]
229#[cfg_attr(not(test), derive(Eq))]
230#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
231#[expect(
232    clippy::exhaustive_structs,
233    reason = "Breaking change with Binarycookies format"
234)]
235pub struct Page {
236    pub cookies: Vec<Cookie>,
237}
238
239impl Page {
240    /// Return cookie offsets
241    pub(crate) fn decode_head(input: &mut StreamIn) -> ModalResult<CookiesOffsetInPage> {
242        if input.len() < 8 {
243            return Err(ErrMode::Incomplete(Needed::Size(unsafe {
244                NonZeroUsize::new_unchecked(8 - input.len())
245            })));
246        }
247        let header = be_u32(input)?;
248        if Self::HEADER != header {
249            let mut context_error =
250                ContextError::from_external_error(input, ExpectErr::U32(header));
251            context_error.extend([
252                StrContext::Label("Page header broken"),
253                StrContext::Expected(StrContextValue::Description(
254                    "Expected page start header: `0x0010`",
255                )),
256            ]);
257            return Err(ErrMode::Cut(context_error));
258        }
259
260        let num_cookies = le_u32(input)? as usize;
261        // cookies size and footer
262        let need_size = num_cookies * 4 + 4;
263        if input.len() < need_size {
264            return Err(ErrMode::Incomplete(Needed::Size(unsafe {
265                NonZeroUsize::new_unchecked(need_size - input.len())
266            })));
267        }
268        let cookie_offsets: Vec<u32> =
269            repeat(num_cookies..num_cookies + 1, le_u32).parse_next(input)?;
270
271        let footer = be_u32(input)?;
272        if footer != Self::FOOTER {
273            let mut context_error =
274                ContextError::from_external_error(input, ExpectErr::U32(footer));
275            context_error.extend([
276                StrContext::Label("Page page footer broken"),
277                StrContext::Expected(StrContextValue::Description(
278                    "Expected page footer: `0x0000`",
279                )),
280            ]);
281            return Err(ErrMode::Cut(context_error));
282        }
283
284        Ok(CookiesOffsetInPage(cookie_offsets))
285    }
286}
287
288impl Page {
289    pub const HEADER: u32 = 0x00000100;
290    pub const FOOTER: u32 = 0x00000000;
291
292    pub fn push(&mut self, cookie: Cookie) {
293        self.cookies.push(cookie);
294    }
295
296    /// Dynamic calculation offset in the page
297    pub fn cookie_offsets(&self) -> Vec<u32> {
298        let mut offset = 4 + 4 + 4 * self.cookies.len() as u32 + 4;
299        let mut offsets = Vec::with_capacity(self.cookies.len());
300        for ele in &self.cookies {
301            offsets.push(offset);
302            offset += ele.size();
303        }
304        offsets
305    }
306
307    pub fn size(&self) -> u32 {
308        4 * 3
309            + self.cookies.len() as u32 * 4
310            + self
311                .cookies
312                .iter()
313                .map(Cookie::size)
314                .sum::<u32>()
315    }
316
317    pub fn encode(&self) -> (Vec<u8>, u32) {
318        let data = self._encode();
319        // FIXME: checksum impl not correct
320        let checksum = data
321            .iter()
322            .step_by(4)
323            .fold(0_u32, |i, &v| i.wrapping_add(v as u32));
324
325        (data, checksum)
326    }
327
328    fn _encode(&self) -> Vec<u8> {
329        let mut raw = Vec::new();
330        raw.extend_from_slice(&Self::HEADER.to_be_bytes());
331        raw.extend_from_slice(&(self.cookies.len() as u32).to_le_bytes());
332        for ele in self.cookie_offsets() {
333            raw.extend_from_slice(&ele.to_le_bytes());
334        }
335        raw.extend_from_slice(&Self::FOOTER.to_be_bytes());
336        for ele in &self.cookies {
337            raw.extend_from_slice(&ele.encode());
338        }
339
340        raw
341    }
342
343    pub fn iter_cookies(&self) -> impl Iterator<Item = &Cookie> {
344        self.cookies.iter()
345    }
346}
347
348/// alone cookies
349#[derive(Clone)]
350#[derive(Debug)]
351#[derive(PartialEq)]
352#[cfg_attr(not(test), derive(Eq))]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[expect(
355    clippy::exhaustive_structs,
356    reason = "Breaking change with Binarycookies format"
357)]
358pub struct Cookie {
359    // pub cookie_size: u32, // LE Cookie size. Number of bytes associated to the cookie
360    pub version: u32, // LE Unknown field possibly related to the cookie flags
361
362    pub flags: u32, // LE 0x0:None , 0x1:Secure , 0x4:HttpOnly , 0x5:Secure+HttpOnly
363    // pub has_port: u32,       // LE 0 or 1
364    #[cfg(test)]
365    pub domain_offset: u32, // LE Cookie domain offset in the cookie
366    #[cfg(test)]
367    pub name_offset: u32, // LE Cookie name offset in the cookie
368    #[cfg(test)]
369    pub path_offset: u32, // LE Cookie path offset in the cookie
370    #[cfg(test)]
371    pub value_offset: u32, // LE Cookie value offset in the cookie
372    #[cfg(test)]
373    pub comment_offset: u32, // LE Cookie comment offset in the cookie
374
375    // pub end_header: [u8; 4], /* 4    byte    Marks the end of a header. Must be equal to []byte{0x00000000} */
376    #[cfg(test)]
377    pub raw_expires: f64, /* f64    Cookie expiration time in Mac epoch time. Add 978307200 to turn into Unix */
378    #[cfg(test)]
379    pub raw_creation: f64, /* f64    Cookie creation time in Mac epoch time. Add 978307200 to turn into Unix */
380
381    pub port: Option<u16>, // LE  Only present if the "Has port" field is 1
382    pub comment: Option<BString>, /* Cookie comment string. N = `self.domain_offset` - `self.comment_offset` when `comment_offset` > 0 */
383    pub domain: BString, // Cookie domain string. N = `self.name_offset` - `self.domain_offset`
384    pub name: BString,   // Cookie name string. N = `self.path_offset` - `self.name_offset`
385    pub path: BString,   // Cookie path string. N = `self.value_offset` - `self.path_offset`
386    pub value: BString,  // Cookie value string. N = `self.cookie_size` - `self.value_offset`
387
388    pub expires: Option<DateTime<Utc>>,
389    pub creation: Option<DateTime<Utc>>,
390    pub same_site: SameSite,
391    pub is_secure: bool,
392    pub is_http_only: bool,
393}
394
395#[rustfmt::skip]
396impl Cookie {
397    pub const IS_SECURE:     u32 = 0b000001;
398    pub const IS_HTTP_ONLY:  u32 = 0b000100;
399    pub const SAME_SITE_BIT: u32 = 0b111000;
400    pub const SS_STRICT:     u32 = 0b111000;
401    pub const SS_LAX:        u32 = 0b101000;
402    pub const SS_NONE:       u32 = 0b100000;
403}
404
405impl Cookie {
406    pub(crate) const fn same_site(flags: u32) -> SameSite {
407        #[expect(clippy::wildcard_in_or_patterns, reason = "this is more clear")]
408        match flags & Self::SAME_SITE_BIT {
409            Self::SS_STRICT => SameSite::Strict,
410            Self::SS_LAX => SameSite::Lax,
411            Self::SS_NONE | _ => SameSite::None,
412        }
413    }
414
415    pub(crate) const fn is_secure(flags: u32) -> bool {
416        flags & Self::IS_SECURE == Self::IS_SECURE
417    }
418
419    pub(crate) const fn is_http_only(flags: u32) -> bool {
420        flags & Self::IS_HTTP_ONLY == Self::IS_HTTP_ONLY
421    }
422
423    pub(crate) fn decode(input: &mut StreamIn) -> ModalResult<Self> {
424        let cookie_size = le_u32(input)?;
425
426        let need_size = cookie_size as usize - 4;
427        if input.len() < need_size {
428            return Err(ErrMode::Incomplete(winnow::error::Needed::Size(unsafe {
429                NonZeroUsize::new_unchecked(need_size - input.len())
430            })));
431        }
432
433        // NOTE: No accurate explanation of `version` was found
434        let (
435            version,
436            flags,
437            has_port,
438            domain_offset,
439            name_offset,
440            path_offset,
441            value_offset,
442            comment_offset,
443        ) = (
444            le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32,
445        )
446            .parse_next(input)?;
447
448        let end_header = take(4_usize).parse_next(input)?;
449        if end_header != Self::END_HEADER {
450            #[expect(clippy::unwrap_used, reason = "end_header len is 4")]
451            let arr: [u8; 4] = end_header.try_into().unwrap();
452            let mut context_error =
453                ContextError::from_external_error(input, ExpectErr::EndHeader(arr));
454            context_error.extend([
455                StrContext::Label("Cookies end header broken"),
456                StrContext::Expected(StrContextValue::Description("Expected end header: `0000`")),
457            ]);
458            return Err(ErrMode::Cut(context_error));
459        }
460
461        let (raw_expires, raw_creation) = (le_f64, le_f64).parse_next(input)?;
462        let (expires, creation) = ((raw_expires).to_utc(), (raw_creation).to_utc());
463        let port = if has_port > 0 {
464            let port = le_u16(input)?;
465            Some(port)
466        }
467        else {
468            None
469        };
470
471        let comment = if comment_offset > 0 {
472            let comment = Self::get_string(input, (domain_offset - comment_offset) as usize)?;
473            Some(comment)
474        }
475        else {
476            None
477        };
478
479        let domain = Self::get_string(input, (name_offset - domain_offset) as usize)?;
480        let name = Self::get_string(input, (path_offset - name_offset) as usize)?;
481        let path = Self::get_string(input, (value_offset - path_offset) as usize)?;
482        let value = Self::get_string(input, (cookie_size - value_offset) as usize)?;
483
484        let same_site = Self::same_site(flags);
485
486        let is_secure = Self::is_secure(flags);
487        let is_http_only = Self::is_http_only(flags);
488
489        Ok(Self {
490            version,
491            flags,
492            #[cfg(test)]
493            domain_offset,
494            #[cfg(test)]
495            name_offset,
496            #[cfg(test)]
497            path_offset,
498            #[cfg(test)]
499            value_offset,
500            #[cfg(test)]
501            comment_offset,
502            #[cfg(test)]
503            raw_expires,
504            #[cfg(test)]
505            raw_creation,
506            port,
507            comment,
508            domain,
509            name,
510            path,
511            value,
512            expires,
513            creation,
514            same_site,
515            is_secure,
516            is_http_only,
517        })
518    }
519
520    #[inline(always)]
521    fn get_string(input: &mut StreamIn, len: usize) -> ModalResult<bstr::BString> {
522        let str = take(len)
523            .map(|c: &[u8]| bstr::BString::new(c[..len - 1].to_vec())) // c-string, end with 0
524            .parse_next(input)?;
525        Ok(str)
526    }
527}
528
529impl Cookie {
530    pub const fn flags(&self) -> u32 {
531        let mut flags = self.flags;
532
533        if self.is_secure {
534            flags |= Self::IS_SECURE;
535        }
536
537        if self.is_http_only {
538            flags |= Self::IS_HTTP_ONLY;
539        }
540
541        match self.same_site {
542            SameSite::None => {},
543            SameSite::Lax => flags |= Self::SS_LAX,
544            SameSite::Strict => flags |= Self::SS_STRICT,
545        }
546
547        flags
548    }
549
550    pub(crate) fn time_to_f64(time: DateTime<Utc>) -> f64 {
551        let timestamp = time
552            - Utc
553                .with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
554                .unwrap();
555        timestamp.num_seconds() as f64
556    }
557
558    pub fn encode(&self) -> Vec<u8> {
559        let size = self.size();
560        let mut raw = Vec::with_capacity(size as usize);
561        raw.extend_from_slice(&size.to_le_bytes());
562        raw.extend_from_slice(&self.version.to_le_bytes());
563        raw.extend_from_slice(&self.flags().to_le_bytes());
564        raw.extend_from_slice(&(self.has_port() as u32).to_le_bytes());
565        raw.extend_from_slice(&self.domain_offset().to_le_bytes());
566        raw.extend_from_slice(&self.name_offset().to_le_bytes());
567        raw.extend_from_slice(&self.path_offset().to_le_bytes());
568        raw.extend_from_slice(&self.value_offset().to_le_bytes());
569        raw.extend_from_slice(&self.comment_offset().to_le_bytes());
570        raw.extend_from_slice(&Self::END_HEADER);
571        raw.extend_from_slice(&Self::time_to_f64(self.expires.unwrap_or_default()).to_le_bytes());
572        raw.extend_from_slice(&Self::time_to_f64(self.creation.unwrap_or_default()).to_le_bytes());
573        if let Some(port) = self.port {
574            raw.extend_from_slice(&port.to_le_bytes());
575        }
576        if let Some(s) = &self.comment {
577            Self::encode_string(&mut raw, s.as_bstr());
578        }
579        Self::encode_string(&mut raw, self.domain.as_bstr());
580        Self::encode_string(&mut raw, self.name.as_bstr());
581        Self::encode_string(&mut raw, self.path.as_bstr());
582        Self::encode_string(&mut raw, self.value.as_bstr());
583
584        raw
585    }
586
587    pub const fn has_port(&self) -> bool {
588        self.port.is_some()
589    }
590
591    /// Dynamic calculation
592    const fn prefix_offset(&self) -> u32 {
593        4 * 10 + 8 * 2 + if self.has_port() { 2 } else { 0 }
594    }
595    /// Dynamic calculation
596    pub fn domain_offset(&self) -> u32 {
597        self.prefix_offset()
598            + self
599                .comment
600                .as_ref()
601                .map_or(0, |v| v.len() as u32 + 1)
602    }
603    /// Dynamic calculation
604    pub fn name_offset(&self) -> u32 {
605        self.domain_offset() + self.domain.len() as u32 + 1
606    }
607    /// Dynamic calculation
608    pub fn path_offset(&self) -> u32 {
609        self.name_offset() + self.name.len() as u32 + 1
610    }
611    /// Dynamic calculation
612    pub fn value_offset(&self) -> u32 {
613        self.path_offset() + self.path.len() as u32 + 1
614    }
615    /// Dynamic calculation
616    pub const fn comment_offset(&self) -> u32 {
617        if self.comment.is_none() {
618            0
619        }
620        else {
621            self.prefix_offset()
622        }
623    }
624
625    pub fn size(&self) -> u32 {
626        4 * 10
627            + 8 * 2
628            + self.port.map_or(0, |_| 2)
629            + self
630                .comment
631                .as_ref()
632                .map_or(0, |v| v.len() as u32 + 1)
633            + (self.domain.len() as u32 + 1)
634            + (self.name.len() as u32 + 1)
635            + (self.path.len() as u32 + 1)
636            + (self.value.len() as u32 + 1)
637    }
638
639    fn encode_string(raw: &mut Vec<u8>, s: &bstr::BStr) {
640        raw.extend(s.bytes());
641        raw.push(0);
642    }
643
644    pub const END_HEADER: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
645}
646
647#[derive(Clone, Copy)]
648#[derive(Debug)]
649#[derive(Default)]
650#[derive(PartialEq, Eq, PartialOrd, Ord)]
651#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
652pub enum SameSite {
653    #[default]
654    None,
655    Lax,
656    Strict,
657}
658
659impl From<i32> for SameSite {
660    fn from(value: i32) -> Self {
661        #[expect(clippy::wildcard_in_or_patterns, reason = "this is more clear")]
662        match value {
663            1 => Self::Lax,
664            2 => Self::Strict,
665            0 | _ => Self::None,
666        }
667    }
668}
669
670impl From<Option<i32>> for SameSite {
671    fn from(value: Option<i32>) -> Self {
672        value.unwrap_or_default().into()
673    }
674}
675
676impl std::fmt::Display for SameSite {
677    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678        match self {
679            Self::None => "None",
680            Self::Lax => "Lax",
681            Self::Strict => "Strict",
682        }
683        .fmt(f)
684    }
685}
686
687#[test]
688fn test_encode_metadata() {
689    let meta = Metadata { nshttp_cookie_accept_policy: 1 };
690    let mut res = vec![];
691    plist::to_writer_binary(&mut res, &meta).unwrap();
692    assert_eq!(&meta.encode(), res.as_slice());
693}