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