script/dom/
cookiestore.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::generic_channel::{GenericSend, GenericSender};
10use base::id::CookieStoreId;
11use cookie::{Cookie, SameSite};
12use dom_struct::dom_struct;
13use hyper_serde::Serde;
14use ipc_channel::ipc;
15use ipc_channel::router::ROUTER;
16use itertools::Itertools;
17use js::jsval::NullValue;
18use net_traits::CookieSource::NonHTTP;
19use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
20use script_bindings::codegen::GenericBindings::CookieStoreBinding::CookieSameSite;
21use script_bindings::script_runtime::CanGc;
22use servo_url::ServoUrl;
23use time::OffsetDateTime;
24
25use crate::dom::bindings::cell::DomRefCell;
26use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
27    CookieInit, CookieListItem, CookieStoreDeleteOptions, CookieStoreGetOptions, CookieStoreMethods,
28};
29use crate::dom::bindings::error::Error;
30use crate::dom::bindings::refcounted::Trusted;
31use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::bindings::str::USVString;
34use crate::dom::document::get_registrable_domain_suffix_of_or_is_equal_to;
35use crate::dom::eventtarget::EventTarget;
36use crate::dom::globalscope::GlobalScope;
37use crate::dom::promise::Promise;
38use crate::dom::window::Window;
39use crate::task_source::SendableTaskSource;
40
41#[derive(JSTraceable, MallocSizeOf)]
42struct DroppableCookieStore {
43    // Store an id so that we can send it with requests and the resource thread knows who to respond to
44    #[no_trace]
45    store_id: CookieStoreId,
46    #[no_trace]
47    unregister_channel: GenericSender<CoreResourceMsg>,
48}
49
50impl Drop for DroppableCookieStore {
51    fn drop(&mut self) {
52        let res = self
53            .unregister_channel
54            .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
55        if res.is_err() {
56            error!("Failed to send cookiestore message to resource threads");
57        }
58    }
59}
60
61/// <https://cookiestore.spec.whatwg.org/>
62/// CookieStore provides an async API for pages and service workers to access and modify cookies.
63/// This requires setting up communication with resource thread's cookie storage that allows for
64/// the page to have multiple cookie storage promises in flight at the same time.
65#[dom_struct]
66pub(crate) struct CookieStore {
67    eventtarget: EventTarget,
68    #[conditional_malloc_size_of]
69    in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
70    droppable: DroppableCookieStore,
71}
72
73struct CookieListener {
74    // TODO:(whatwg/cookiestore#239) The spec is missing details for what task source to use
75    task_source: SendableTaskSource,
76    context: Trusted<CookieStore>,
77}
78
79impl CookieListener {
80    pub(crate) fn handle(&self, message: CookieAsyncResponse) {
81        let context = self.context.clone();
82        self.task_source.queue(task!(cookie_message: move || {
83            let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
84                warn!("No promise exists for cookie store response");
85                return;
86            };
87            match message.data {
88                CookieData::Get(cookie) => {
89                    // If list is failure, then reject p with a TypeError and abort these steps.
90                    // (There is currently no way for list to result in failure)
91                    if let Some(cookie) = cookie {
92                        // Otherwise, resolve p with the first item of list.
93                        promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
94                    } else {
95                        // If list is empty, then resolve p with null.
96                        promise.resolve_native(&NullValue(), CanGc::note());
97                    }
98                },
99                CookieData::GetAll(cookies) => {
100                    // If list is failure, then reject p with a TypeError and abort these steps.
101                    promise.resolve_native(
102                        &cookies
103                        .into_iter()
104                        .map(|cookie| cookie_to_list_item(cookie.0))
105                        .collect_vec(),
106                    CanGc::note());
107                },
108                CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
109                    promise.resolve_native(&(), CanGc::note());
110                }
111            }
112        }));
113    }
114}
115
116impl CookieStore {
117    fn new_inherited(unregister_channel: GenericSender<CoreResourceMsg>) -> CookieStore {
118        CookieStore {
119            eventtarget: EventTarget::new_inherited(),
120            in_flight: Default::default(),
121            droppable: DroppableCookieStore {
122                store_id: CookieStoreId::new(),
123                unregister_channel,
124            },
125        }
126    }
127
128    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
129        let store = reflect_dom_object(
130            Box::new(CookieStore::new_inherited(
131                global.resource_threads().core_thread.clone(),
132            )),
133            global,
134            can_gc,
135        );
136        store.setup_route();
137        store
138    }
139
140    fn setup_route(&self) {
141        let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
142
143        let context = Trusted::new(self);
144        let cs_listener = CookieListener {
145            task_source: self
146                .global()
147                .task_manager()
148                .dom_manipulation_task_source()
149                .to_sendable(),
150            context,
151        };
152
153        ROUTER.add_typed_route(
154            cookie_receiver,
155            Box::new(move |message| match message {
156                Ok(msg) => cs_listener.handle(msg),
157                Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
158            }),
159        );
160
161        let res = self
162            .global()
163            .resource_threads()
164            .send(CoreResourceMsg::NewCookieListener(
165                self.droppable.store_id,
166                cookie_sender,
167                self.global().creation_url().clone(),
168            ));
169        if res.is_err() {
170            error!("Failed to send cookiestore message to resource threads");
171        }
172    }
173}
174
175/// <https://cookiestore.spec.whatwg.org/#create-a-cookielistitem>
176fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
177    // TODO: Investigate if we need to explicitly UTF-8 decode without BOM here or if thats
178    // already being done by cookie-rs or implicitly by using rust strings
179    CookieListItem {
180        // Let name be the result of running UTF-8 decode without BOM on cookie’s name.
181        name: Some(cookie.name().to_string().into()),
182
183        // Let value be the result of running UTF-8 decode without BOM on cookie’s value.
184        value: Some(cookie.value().to_string().into()),
185    }
186}
187
188impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
189    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-get>
190    fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
191        // 1. Let settings be this’s relevant settings object.
192        let global = self.global();
193
194        // 2. Let origin be settings’s origin.
195        let origin = global.origin();
196
197        // 5. Let p be a new promise.
198        let p = Promise::new(&global, can_gc);
199
200        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
201        if !origin.is_tuple() {
202            p.reject_error(Error::Security(None), can_gc);
203            return p;
204        }
205
206        // 4. Let url be settings’s creation URL.
207        let creation_url = global.creation_url();
208
209        let name = CookieStore::normalize(&name);
210
211        // 6. Run the following steps in parallel:
212        let res = self
213            .global()
214            .resource_threads()
215            .send(CoreResourceMsg::GetCookieDataForUrlAsync(
216                self.droppable.store_id,
217                creation_url.clone(),
218                Some(name),
219            ));
220        if res.is_err() {
221            error!("Failed to send cookiestore message to resource threads");
222        } else {
223            self.in_flight.borrow_mut().push_back(p.clone());
224        }
225
226        // 7. Return p.
227        p
228    }
229
230    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-get-options>
231    fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
232        // 1. Let settings be this’s relevant settings object.
233        let global = self.global();
234
235        // 2. Let origin be settings’s origin.
236        let origin = global.origin();
237
238        // 7. Let p be a new promise.
239        let p = Promise::new(&global, can_gc);
240
241        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
242        if !origin.is_tuple() {
243            p.reject_error(Error::Security(None), can_gc);
244            return p;
245        }
246
247        // 4. Let url be settings’s creation URL.
248        let creation_url = global.creation_url();
249
250        // 5. If options is empty, then return a promise rejected with a TypeError.
251        // "is empty" is not strictly defined anywhere in the spec but the only value we require here is "url"
252        if options.url.is_none() && options.name.is_none() {
253            p.reject_error(Error::Type(c"Options cannot be empty".to_owned()), can_gc);
254            return p;
255        }
256
257        let mut final_url = creation_url.clone();
258
259        // 6. If options["url"] is present, then run these steps:
260        if let Some(get_url) = &options.url {
261            // 6.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
262            let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
263
264            // 6.2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
265            // then return a promise rejected with a TypeError.
266            if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
267                if parsed_url
268                    .as_ref()
269                    .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
270                {
271                    p.reject_error(
272                        Error::Type(c"URL does not match context".to_owned()),
273                        can_gc,
274                    );
275                    return p;
276                }
277            }
278
279            // 6.3. If parsed’s origin and url’s origin are not the same origin,
280            // then return a promise rejected with a TypeError.
281            if parsed_url
282                .as_ref()
283                .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
284            {
285                p.reject_error(Error::Type(c"Not same origin".to_owned()), can_gc);
286                return p;
287            }
288
289            // 6.4. Set url to parsed.
290            if let Ok(url) = parsed_url {
291                final_url = url;
292            }
293        }
294
295        // 6. Run the following steps in parallel:
296        let res = self
297            .global()
298            .resource_threads()
299            .send(CoreResourceMsg::GetCookieDataForUrlAsync(
300                self.droppable.store_id,
301                final_url.clone(),
302                options.name.clone().map(|val| CookieStore::normalize(&val)),
303            ));
304        if res.is_err() {
305            error!("Failed to send cookiestore message to resource threads");
306        } else {
307            self.in_flight.borrow_mut().push_back(p.clone());
308        }
309
310        p
311    }
312
313    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall>
314    fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
315        // 1. Let settings be this’s relevant settings object.
316        let global = self.global();
317
318        // 2. Let origin be settings’s origin.
319        let origin = global.origin();
320
321        // 5. Let p be a new promise.
322        let p = Promise::new(&global, can_gc);
323
324        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
325        if !origin.is_tuple() {
326            p.reject_error(Error::Security(None), can_gc);
327            return p;
328        }
329        // 4. Let url be settings’s creation URL.
330        let creation_url = global.creation_url();
331
332        // Normalize name here rather than passing the un-nomarlized name around to the resource thread and back
333        let name = CookieStore::normalize(&name);
334
335        // 6. Run the following steps in parallel:
336        let res =
337            self.global()
338                .resource_threads()
339                .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
340                    self.droppable.store_id,
341                    creation_url.clone(),
342                    Some(name),
343                ));
344        if res.is_err() {
345            error!("Failed to send cookiestore message to resource threads");
346        } else {
347            self.in_flight.borrow_mut().push_back(p.clone());
348        }
349
350        // 7. Return p.
351        p
352    }
353
354    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall-options>
355    fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
356        // 1. Let settings be this’s relevant settings object.
357        let global = self.global();
358
359        // 2. Let origin be settings’s origin.
360        let origin = global.origin();
361
362        // 6. Let p be a new promise.
363        let p = Promise::new(&global, can_gc);
364
365        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
366        if !origin.is_tuple() {
367            p.reject_error(Error::Security(None), can_gc);
368            return p;
369        }
370
371        // 4. Let url be settings’s creation URL.
372        let creation_url = global.creation_url();
373
374        let mut final_url = creation_url.clone();
375
376        // 5. If options["url"] is present, then run these steps:
377        if let Some(get_url) = &options.url {
378            // 5.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
379            let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
380
381            // If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
382            // then return a promise rejected with a TypeError.
383            if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
384                if parsed_url
385                    .as_ref()
386                    .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
387                {
388                    p.reject_error(
389                        Error::Type(c"URL does not match context".to_owned()),
390                        can_gc,
391                    );
392                    return p;
393                }
394            }
395
396            // 5.3. If parsed’s origin and url’s origin are not the same origin,
397            // then return a promise rejected with a TypeError.
398            if parsed_url
399                .as_ref()
400                .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
401            {
402                p.reject_error(Error::Type(c"Not same origin".to_owned()), can_gc);
403                return p;
404            }
405
406            // 5.4. Set url to parsed.
407            if let Ok(url) = parsed_url {
408                final_url = url;
409            }
410        }
411
412        // 7. Run the following steps in parallel:
413        let res =
414            self.global()
415                .resource_threads()
416                .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
417                    self.droppable.store_id,
418                    final_url.clone(),
419                    options.name.clone().map(|val| CookieStore::normalize(&val)),
420                ));
421        if res.is_err() {
422            error!("Failed to send cookiestore message to resource threads");
423        } else {
424            self.in_flight.borrow_mut().push_back(p.clone());
425        }
426
427        // 8. Return p
428        p
429    }
430
431    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-set>
432    fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
433        // 1. Let settings be this’s relevant settings object.
434        let global = self.global();
435
436        // 2. Let origin be settings’s origin.
437        let origin = global.origin();
438
439        // 9. Let p be a new promise.
440        let p = Promise::new(&global, can_gc);
441
442        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
443        if !origin.is_tuple() {
444            p.reject_error(Error::Security(None), can_gc);
445            return p;
446        }
447
448        // 6.1 Let r be the result of running set a cookie with url, name, value, null, null, "/", "strict", false, and null.
449        let properties = CookieInit {
450            name,
451            value,
452            expires: None,
453            domain: None,
454            path: USVString(String::from("/")),
455            sameSite: CookieSameSite::Strict,
456            partitioned: false,
457        };
458        let creation_url = global.creation_url();
459        let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
460            // If r is failure, then reject p with a TypeError and abort these steps.
461            p.reject_error(Error::Type(c"Invalid cookie".to_owned()), can_gc);
462            return p;
463        };
464
465        // 6. Run the following steps in parallel:
466        let res = self
467            .global()
468            .resource_threads()
469            .send(CoreResourceMsg::SetCookieForUrlAsync(
470                self.droppable.store_id,
471                creation_url.clone(),
472                Serde(cookie.into_owned()),
473                NonHTTP,
474            ));
475        if res.is_err() {
476            error!("Failed to send cookiestore message to resource threads");
477        } else {
478            self.in_flight.borrow_mut().push_back(p.clone());
479        }
480
481        // 7. Return p.
482        p
483    }
484
485    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-set-options>
486    fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
487        // 1. Let settings be this’s relevant settings object.
488        let global = self.global();
489
490        // 2. Let origin be settings’s origin.
491        let origin = global.origin();
492
493        // 5. Let p be a new promise.
494        let p = Promise::new(&global, can_gc);
495
496        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
497        if !origin.is_tuple() {
498            p.reject_error(Error::Security(None), can_gc);
499            return p;
500        }
501
502        // 4. Let url be settings’s creation URL.
503        let creation_url = global.creation_url();
504
505        // 6.1. Let r be the result of running set a cookie with url, options["name"], options["value"],
506        // options["expires"], options["domain"], options["path"], options["sameSite"], and options["partitioned"].
507        let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
508            p.reject_error(Error::Type(c"Invalid cookie".to_owned()), can_gc);
509            return p;
510        };
511
512        // 6. Run the following steps in parallel:
513        let res = self
514            .global()
515            .resource_threads()
516            .send(CoreResourceMsg::SetCookieForUrlAsync(
517                self.droppable.store_id,
518                creation_url.clone(),
519                Serde(cookie.into_owned()),
520                NonHTTP,
521            ));
522        if res.is_err() {
523            error!("Failed to send cookiestore message to resource threads");
524        } else {
525            self.in_flight.borrow_mut().push_back(p.clone());
526        }
527
528        // 7. Return p
529        p
530    }
531
532    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete>
533    fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
534        // 1. Let settings be this’s relevant settings object.
535        let global = self.global();
536
537        // 2. Let origin be settings’s origin.
538        let origin = global.origin();
539
540        // 5. Let p be a new promise.
541        let p = Promise::new(&global, can_gc);
542
543        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
544        if !origin.is_tuple() {
545            p.reject_error(Error::Security(None), can_gc);
546            return p;
547        }
548
549        // 6. Run the following steps in parallel:
550        // TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
551        let res = global
552            .resource_threads()
553            .send(CoreResourceMsg::DeleteCookieAsync(
554                self.droppable.store_id,
555                global.creation_url().clone(),
556                name.0,
557            ));
558        if res.is_err() {
559            error!("Failed to send cookiestore message to resource threads");
560        } else {
561            self.in_flight.borrow_mut().push_back(p.clone());
562        }
563
564        // 7. Return p.
565        p
566    }
567
568    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete-options>
569    fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
570        // 1. Let settings be this’s relevant settings object.
571        let global = self.global();
572
573        // 2. Let origin be settings’s origin.
574        let origin = global.origin();
575
576        // 5. Let p be a new promise.
577        let p = Promise::new(&global, can_gc);
578
579        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
580        if !origin.is_tuple() {
581            p.reject_error(Error::Security(None), can_gc);
582            return p;
583        }
584
585        // 6. Run the following steps in parallel:
586        // TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
587        let res = global
588            .resource_threads()
589            .send(CoreResourceMsg::DeleteCookieAsync(
590                self.droppable.store_id,
591                global.creation_url().clone(),
592                options.name.to_string(),
593            ));
594        if res.is_err() {
595            error!("Failed to send cookiestore message to resource threads");
596        } else {
597            self.in_flight.borrow_mut().push_back(p.clone());
598        }
599
600        // 7. Return p.
601        p
602    }
603}
604
605impl CookieStore {
606    /// <https://cookiestore.spec.whatwg.org/#normalize-a-cookie-name-or-value>
607    fn normalize(value: &USVString) -> String {
608        value.trim_matches([' ', '\t']).into()
609    }
610
611    /// <https://cookiestore.spec.whatwg.org/#set-cookie-algorithm>
612    fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
613        // 1. Normalize name.
614        let name = CookieStore::normalize(&properties.name);
615        // 2. Normalize value.
616        let value = CookieStore::normalize(&properties.value);
617
618        // 3. If name or value contain U+003B (;), any C0 control character except U+0009 TAB, or U+007F DELETE, then return failure.
619        if CookieStore::contains_control_characters(&name) ||
620            CookieStore::contains_control_characters(&value)
621        {
622            return None;
623        }
624
625        // 4. If name contains U+003D (=), then return failure.
626        if name.contains('=') {
627            return None;
628        }
629
630        // 5. If name’s length is 0:
631        if name.is_empty() {
632            // 5.1 If value contains U+003D (=), then return failure.
633            // 5.2 If value’s length is 0, then return failure.
634            if value.contains('=') || value.is_empty() {
635                return None;
636            }
637            // 5.3 If value, byte-lowercased, starts with `__host-`, `__host-http-`, `__http-`, or `__secure-`, then return failure.
638            let lowercased_value = value.to_ascii_lowercase();
639            if ["__host-", "__host-http-", "__http-", "__secure-"]
640                .iter()
641                .any(|prefix| lowercased_value.starts_with(prefix))
642            {
643                return None;
644            }
645        }
646
647        // 6. If name, byte-lowercased, starts with `__host-http-` or `__http-`, then return failure.
648        let lowercased_name = name.to_ascii_lowercase();
649        if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
650            return None;
651        }
652
653        // 9. If the byte sequence length of encodedName plus the byte sequence length of encodedValue is greater than the maximum name/value pair size, then return failure.
654        if name.len() + value.len() > 4096 {
655            return None;
656        }
657
658        let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
659            // 21. Append ('Secure', '') to attributes
660            .secure(true)
661            // 23. If partitioned is true, Append (`Partitioned`, ``) to attributes.
662            .partitioned(properties.partitioned)
663            // 21. Switch on sameSite:
664            .same_site(match properties.sameSite {
665                CookieSameSite::Lax => SameSite::Lax,
666                CookieSameSite::Strict => SameSite::Strict,
667                CookieSameSite::None => SameSite::None,
668            });
669
670        // 12. If domain is non-null
671        if let Some(domain) = &properties.domain {
672            // 10. Let host be url's host.
673            let host = match url.host() {
674                Some(host) => host.to_owned(),
675                None => return None,
676            };
677            // 12.1 If domain starts with U+002E (.), then return failure
678            if domain.starts_with('.') {
679                return None;
680            }
681            // 12.2 If name, byte-lowercased, starts with `__host-`, then return failure.
682            if lowercased_name.starts_with("__host-") {
683                return None;
684            }
685
686            // 12.3 If domain is not a registrable domain suffix of and is not equal to host, then return failure.
687            // 12.4 Let parsedDomain be the result of host parsing domain.
688            // 12.5 Assert: parsedDomain is not failure.
689            // Note: this function parses the host and returns the parsed host on success
690            let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
691
692            // 12.6 Let encodedDomain be the result of UTF-8 encoding parsedDomain.
693            let domain = domain.to_string();
694            // 12.7 If the byte sequence length of encodedDomain is greater than the maximum attribute value size, then return failure.
695            if domain.len() > 1024 {
696                return None;
697            }
698            // 12.8 Append (`Domain`, encodedDomain) to attributes.
699            cookie.inner_mut().set_domain(domain);
700        }
701
702        // 13. If expires is non-null:
703        if let Some(expiry) = properties.expires {
704            // TODO: update cookiestore to take new maxAge parameter
705            // 13.2 Append (`Expires`, expires (date serialized)) to attributes.
706            cookie.inner_mut().set_expires(
707                OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
708                    .expect("cookie expiry out of range"),
709            );
710        }
711
712        // 15. If path is the empty string, then set path to the serialized cookie default path of url.
713        let path = if properties.path.is_empty() {
714            // 15.1 Let cloneURL be a clone of url.
715            let mut cloned_url = url.clone();
716            {
717                // 15.2 Set cloneURL’s path to the cookie default path of cloneURL’s path.
718                // <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-layered-cookies#name-cookie-default-path>
719                let mut path_segments = cloned_url
720                    .as_mut_url()
721                    .path_segments_mut()
722                    .expect("document creation url cannot be a base");
723                // 2. If path's size is greater than 1, then remove path's last item
724                if url.path_segments().is_some_and(|ps| ps.count() > 1) {
725                    path_segments.pop();
726                } else {
727                    // 3. Otherwise, set path[0] to the empty string.
728                    path_segments.clear();
729                }
730            }
731            cloned_url.path().to_owned()
732        } else {
733            properties.path.to_string()
734        };
735        // 16. If path does not start with U+002F (/), then return failure.
736        if !path.starts_with('/') {
737            return None;
738        }
739        // 17. If path is not U+002F (/), and name, byte-lowercased, starts with `__host-`, then return failure.
740        if path != "/" && lowercased_name.starts_with("__host-") {
741            return None;
742        }
743        // 19. If the byte sequence length of encodedPath is greater than the maximum attribute value size, then return failure.
744        if path.len() > 1024 {
745            return None;
746        }
747        // 20. Append (`Path`, encodedPath) to attributes.
748        cookie.inner_mut().set_path(path);
749
750        Some(cookie.build())
751    }
752
753    /// <https://cookiestore.spec.whatwg.org/#set-cookie-algorithm>
754    fn contains_control_characters(val: &str) -> bool {
755        // If name or value contain U+003B (;), any C0 control character except U+0009 TAB, or U+007F DELETE, then return failure.
756        val.contains(
757            |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
758        )
759    }
760}