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#[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"; 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 pub fn iter_cookies(&self) -> impl Iterator<Item = &Cookie> {
121 self.iter_pages()
122 .flat_map(Page::iter_cookies)
123 }
124
125 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 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 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 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 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 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 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 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#[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 version: u32, pub flags: u32, #[cfg(test)]
365 pub domain_offset: u32, #[cfg(test)]
367 pub name_offset: u32, #[cfg(test)]
369 pub path_offset: u32, #[cfg(test)]
371 pub value_offset: u32, #[cfg(test)]
373 pub comment_offset: u32, #[cfg(test)]
377 pub raw_expires: f64, #[cfg(test)]
379 pub raw_creation: f64, pub port: Option<u16>, pub comment: Option<BString>, pub domain: BString, pub name: BString, pub path: BString, pub value: BString, 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 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())) .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 const fn prefix_offset(&self) -> u32 {
593 4 * 10 + 8 * 2 + if self.has_port() { 2 } else { 0 }
594 }
595 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 pub fn name_offset(&self) -> u32 {
605 self.domain_offset() + self.domain.len() as u32 + 1
606 }
607 pub fn path_offset(&self) -> u32 {
609 self.name_offset() + self.name.len() as u32 + 1
610 }
611 pub fn value_offset(&self) -> u32 {
613 self.path_offset() + self.path.len() as u32 + 1
614 }
615 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}