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#[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, 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 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#[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 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())) .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 const fn prefix_offset(&self) -> u32 {
614 4 * 10 + 8 * 2 + if self.has_port() { 2 } else { 0 }
615 }
616 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 pub fn name_offset(&self) -> u32 {
626 self.domain_offset() + self.domain.len() as u32 + 1
627 }
628 pub fn path_offset(&self) -> u32 {
630 self.name_offset() + self.name.len() as u32 + 1
631 }
632 pub fn value_offset(&self) -> u32 {
634 self.path_offset() + self.path.len() as u32 + 1
635 }
636 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}