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::{BadKeySnafu, ExpectErr, MagicSnafu, NotDictSnafu, OneByteIntSnafu},
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, MagicSnafu.build());
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, NotDictSnafu.build());
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, BadKeySnafu.build());
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, OneByteIntSnafu.build());
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#[cfg(feature = "csv")]
396impl Cookie {
397    pub fn csv_header<D: std::fmt::Display>(sep: D) -> String {
398        format!("domain{sep}name{sep}path{sep}value{sep}creation{sep}expires{sep}is_secure{sep}is_http_only")
399    }
400
401    pub fn to_csv<D: std::fmt::Display>(&self, sep: D) -> String {
402        format!(
403            "{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}{sep}{}",
404            self.domain,
405            self.name,
406            self.path,
407            self.value,
408            self.creation.unwrap_or_default(),
409            self.expires.unwrap_or_default(),
410            self.is_secure,
411            self.is_http_only,
412        )
413    }
414}
415
416#[rustfmt::skip]
417impl Cookie {
418    pub const IS_SECURE:     u32 = 0b000001;
419    pub const IS_HTTP_ONLY:  u32 = 0b000100;
420    pub const SAME_SITE_BIT: u32 = 0b111000;
421    pub const SS_STRICT:     u32 = 0b111000;
422    pub const SS_LAX:        u32 = 0b101000;
423    pub const SS_NONE:       u32 = 0b100000;
424}
425
426impl Cookie {
427    pub(crate) const fn same_site(flags: u32) -> SameSite {
428        #[expect(clippy::wildcard_in_or_patterns, reason = "this is more clear")]
429        match flags & Self::SAME_SITE_BIT {
430            Self::SS_STRICT => SameSite::Strict,
431            Self::SS_LAX => SameSite::Lax,
432            Self::SS_NONE | _ => SameSite::None,
433        }
434    }
435
436    pub(crate) const fn is_secure(flags: u32) -> bool {
437        flags & Self::IS_SECURE == Self::IS_SECURE
438    }
439
440    pub(crate) const fn is_http_only(flags: u32) -> bool {
441        flags & Self::IS_HTTP_ONLY == Self::IS_HTTP_ONLY
442    }
443
444    pub(crate) fn decode(input: &mut StreamIn) -> ModalResult<Self> {
445        let cookie_size = le_u32(input)?;
446
447        let need_size = cookie_size as usize - 4;
448        if input.len() < need_size {
449            return Err(ErrMode::Incomplete(winnow::error::Needed::Size(unsafe {
450                NonZeroUsize::new_unchecked(need_size - input.len())
451            })));
452        }
453
454        // NOTE: No accurate explanation of `version` was found
455        let (
456            version,
457            flags,
458            has_port,
459            domain_offset,
460            name_offset,
461            path_offset,
462            value_offset,
463            comment_offset,
464        ) = (
465            le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32,
466        )
467            .parse_next(input)?;
468
469        let end_header = take(4_usize).parse_next(input)?;
470        if end_header != Self::END_HEADER {
471            #[expect(clippy::unwrap_used, reason = "end_header len is 4")]
472            let arr: [u8; 4] = end_header.try_into().unwrap();
473            let mut context_error =
474                ContextError::from_external_error(input, ExpectErr::EndHeader(arr));
475            context_error.extend([
476                StrContext::Label("Cookies end header broken"),
477                StrContext::Expected(StrContextValue::Description("Expected end header: `0000`")),
478            ]);
479            return Err(ErrMode::Cut(context_error));
480        }
481
482        let (raw_expires, raw_creation) = (le_f64, le_f64).parse_next(input)?;
483        let (expires, creation) = ((raw_expires).to_utc(), (raw_creation).to_utc());
484        let port = if has_port > 0 {
485            let port = le_u16(input)?;
486            Some(port)
487        }
488        else {
489            None
490        };
491
492        let comment = if comment_offset > 0 {
493            let comment = Self::get_string(input, (domain_offset - comment_offset) as usize)?;
494            Some(comment)
495        }
496        else {
497            None
498        };
499
500        let domain = Self::get_string(input, (name_offset - domain_offset) as usize)?;
501        let name = Self::get_string(input, (path_offset - name_offset) as usize)?;
502        let path = Self::get_string(input, (value_offset - path_offset) as usize)?;
503        let value = Self::get_string(input, (cookie_size - value_offset) as usize)?;
504
505        let same_site = Self::same_site(flags);
506
507        let is_secure = Self::is_secure(flags);
508        let is_http_only = Self::is_http_only(flags);
509
510        Ok(Self {
511            version,
512            flags,
513            #[cfg(test)]
514            domain_offset,
515            #[cfg(test)]
516            name_offset,
517            #[cfg(test)]
518            path_offset,
519            #[cfg(test)]
520            value_offset,
521            #[cfg(test)]
522            comment_offset,
523            #[cfg(test)]
524            raw_expires,
525            #[cfg(test)]
526            raw_creation,
527            port,
528            comment,
529            domain,
530            name,
531            path,
532            value,
533            expires,
534            creation,
535            same_site,
536            is_secure,
537            is_http_only,
538        })
539    }
540
541    #[inline(always)]
542    fn get_string(input: &mut StreamIn, len: usize) -> ModalResult<bstr::BString> {
543        let str = take(len)
544            .map(|c: &[u8]| bstr::BString::new(c[..len - 1].to_vec())) // c-string, end with 0
545            .parse_next(input)?;
546        Ok(str)
547    }
548}
549
550impl Cookie {
551    pub const fn flags(&self) -> u32 {
552        let mut flags = self.flags;
553
554        if self.is_secure {
555            flags |= Self::IS_SECURE;
556        }
557
558        if self.is_http_only {
559            flags |= Self::IS_HTTP_ONLY;
560        }
561
562        match self.same_site {
563            SameSite::None => {},
564            SameSite::Lax => flags |= Self::SS_LAX,
565            SameSite::Strict => flags |= Self::SS_STRICT,
566        }
567
568        flags
569    }
570
571    pub(crate) fn time_to_f64(time: DateTime<Utc>) -> f64 {
572        let timestamp = time
573            - Utc
574                .with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
575                .unwrap();
576        timestamp.num_seconds() as f64
577    }
578
579    pub fn encode(&self) -> Vec<u8> {
580        let size = self.size();
581        let mut raw = Vec::with_capacity(size as usize);
582        raw.extend_from_slice(&size.to_le_bytes());
583        raw.extend_from_slice(&self.version.to_le_bytes());
584        raw.extend_from_slice(&self.flags().to_le_bytes());
585        raw.extend_from_slice(&(self.has_port() as u32).to_le_bytes());
586        raw.extend_from_slice(&self.domain_offset().to_le_bytes());
587        raw.extend_from_slice(&self.name_offset().to_le_bytes());
588        raw.extend_from_slice(&self.path_offset().to_le_bytes());
589        raw.extend_from_slice(&self.value_offset().to_le_bytes());
590        raw.extend_from_slice(&self.comment_offset().to_le_bytes());
591        raw.extend_from_slice(&Self::END_HEADER);
592        raw.extend_from_slice(&Self::time_to_f64(self.expires.unwrap_or_default()).to_le_bytes());
593        raw.extend_from_slice(&Self::time_to_f64(self.creation.unwrap_or_default()).to_le_bytes());
594        if let Some(port) = self.port {
595            raw.extend_from_slice(&port.to_le_bytes());
596        }
597        if let Some(s) = &self.comment {
598            Self::encode_string(&mut raw, s.as_bstr());
599        }
600        Self::encode_string(&mut raw, self.domain.as_bstr());
601        Self::encode_string(&mut raw, self.name.as_bstr());
602        Self::encode_string(&mut raw, self.path.as_bstr());
603        Self::encode_string(&mut raw, self.value.as_bstr());
604
605        raw
606    }
607
608    pub const fn has_port(&self) -> bool {
609        self.port.is_some()
610    }
611
612    /// Dynamic calculation
613    const fn prefix_offset(&self) -> u32 {
614        4 * 10 + 8 * 2 + if self.has_port() { 2 } else { 0 }
615    }
616    /// Dynamic calculation
617    pub fn domain_offset(&self) -> u32 {
618        self.prefix_offset()
619            + self
620                .comment
621                .as_ref()
622                .map_or(0, |v| v.len() as u32 + 1)
623    }
624    /// Dynamic calculation
625    pub fn name_offset(&self) -> u32 {
626        self.domain_offset() + self.domain.len() as u32 + 1
627    }
628    /// Dynamic calculation
629    pub fn path_offset(&self) -> u32 {
630        self.name_offset() + self.name.len() as u32 + 1
631    }
632    /// Dynamic calculation
633    pub fn value_offset(&self) -> u32 {
634        self.path_offset() + self.path.len() as u32 + 1
635    }
636    /// Dynamic calculation
637    pub const fn comment_offset(&self) -> u32 {
638        if self.comment.is_none() {
639            0
640        }
641        else {
642            self.prefix_offset()
643        }
644    }
645
646    pub fn size(&self) -> u32 {
647        4 * 10
648            + 8 * 2
649            + self.port.map_or(0, |_| 2)
650            + self
651                .comment
652                .as_ref()
653                .map_or(0, |v| v.len() as u32 + 1)
654            + (self.domain.len() as u32 + 1)
655            + (self.name.len() as u32 + 1)
656            + (self.path.len() as u32 + 1)
657            + (self.value.len() as u32 + 1)
658    }
659
660    fn encode_string(raw: &mut Vec<u8>, s: &bstr::BStr) {
661        raw.extend(s.bytes());
662        raw.push(0);
663    }
664
665    pub const END_HEADER: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
666}
667
668#[derive(Clone, Copy)]
669#[derive(Debug)]
670#[derive(Default)]
671#[derive(PartialEq, Eq, PartialOrd, Ord)]
672#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
673pub enum SameSite {
674    #[default]
675    None,
676    Lax,
677    Strict,
678}
679
680impl From<i32> for SameSite {
681    fn from(value: i32) -> Self {
682        #[expect(clippy::wildcard_in_or_patterns, reason = "this is more clear")]
683        match value {
684            1 => Self::Lax,
685            2 => Self::Strict,
686            0 | _ => Self::None,
687        }
688    }
689}
690
691impl From<Option<i32>> for SameSite {
692    fn from(value: Option<i32>) -> Self {
693        value.unwrap_or_default().into()
694    }
695}
696
697impl std::fmt::Display for SameSite {
698    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699        match self {
700            Self::None => "None",
701            Self::Lax => "Lax",
702            Self::Strict => "Strict",
703        }
704        .fmt(f)
705    }
706}
707
708#[test]
709fn test_encode_metadata() {
710    let meta = Metadata { nshttp_cookie_accept_policy: 1 };
711    let mut res = vec![];
712    plist::to_writer_binary(&mut res, &meta).unwrap();
713    assert_eq!(&meta.encode(), res.as_slice());
714}