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#[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"; 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 pub fn iter_cookies(&self) -> impl Iterator<Item = &Cookie> {
120 self.iter_pages()
121 .flat_map(Page::iter_cookies)
122 }
123
124 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 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 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 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 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 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 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 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#[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 version: u32, pub flags: u32, #[cfg(test)]
359 pub domain_offset: u32, #[cfg(test)]
361 pub name_offset: u32, #[cfg(test)]
363 pub path_offset: u32, #[cfg(test)]
365 pub value_offset: u32, #[cfg(test)]
367 pub comment_offset: u32, #[cfg(test)]
371 pub raw_expires: f64, #[cfg(test)]
373 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>>,
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 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())) .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 const fn prefix_offset(&self) -> u32 {
587 4 * 10 + 8 * 2 + if self.has_port() { 2 } else { 0 }
588 }
589 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 pub fn name_offset(&self) -> u32 {
599 self.domain_offset() + self.domain.len() as u32 + 1
600 }
601 pub fn path_offset(&self) -> u32 {
603 self.name_offset() + self.name.len() as u32 + 1
604 }
605 pub fn value_offset(&self) -> u32 {
607 self.path_offset() + self.path.len() as u32 + 1
608 }
609 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}