use boa_cat::Value;
use boa_cat::fuel::Fuel;
use boa_cat::heap::Heap;
use boa_cat::outcome::{EvalResult, Outcome};
use boa_cat::value::{AccessorPair, Object, ObjectId};
pub const PROPERTY_KEY: &str = "cookie";
pub const VISIBLE_KEY: &str = "__cookies_visible__";
pub const WRITES_KEY: &str = "__cookie_writes__";
#[must_use]
pub fn install_cookie_accessor(document_value: &Value, heap: Heap) -> Heap {
let pair =
object_id_of(document_value).and_then(|id| heap.object(id).cloned().map(|obj| (id, obj)));
pair.into_iter().fold(heap, |heap, (id, obj)| {
let accessor = AccessorPair::new(
Some(Value::Native(cookie_getter_impl)),
Some(Value::Native(cookie_setter_impl)),
);
let updated = obj
.with(VISIBLE_KEY.to_owned(), Value::String(String::new()))
.with(WRITES_KEY.to_owned(), Value::String(String::new()))
.with_accessor(PROPERTY_KEY.to_owned(), accessor);
heap.store_object(id, updated).unwrap_or_else(|h| h)
})
}
#[must_use]
pub fn set_document_cookie(document_value: &Value, heap: Heap, cookies: &str) -> Heap {
let pair =
object_id_of(document_value).and_then(|id| heap.object(id).cloned().map(|obj| (id, obj)));
let cookies_owned = cookies.to_owned();
pair.into_iter().fold(heap, |heap, (id, obj)| {
let updated = obj
.with(VISIBLE_KEY.to_owned(), Value::String(cookies_owned.clone()))
.with(WRITES_KEY.to_owned(), Value::String(String::new()));
heap.store_object(id, updated).unwrap_or_else(|h| h)
})
}
#[must_use]
pub fn get_document_cookie(document_value: &Value, heap: &Heap) -> Option<String> {
read_string_property(document_value, heap, VISIBLE_KEY)
}
#[must_use]
pub fn read_cookie_writes(document_value: &Value, heap: &Heap) -> Vec<String> {
read_string_property(document_value, heap, WRITES_KEY)
.filter(|s| !s.is_empty())
.map(|s| s.split('\n').map(str::to_owned).collect())
.unwrap_or_default()
}
fn read_string_property(value: &Value, heap: &Heap, key: &str) -> Option<String> {
let id = object_id_of(value)?;
let obj = heap.object(id)?;
obj.get(key).and_then(|value| match value {
Value::String(s) => Some(s.clone()),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::Object(_)
| Value::Function(_)
| Value::Native(_) => None,
})
}
fn object_id_of(value: &Value) -> Option<ObjectId> {
match value {
Value::Object(id) => Some(*id),
Value::Undefined
| Value::Null
| Value::Boolean(_)
| Value::Number(_)
| Value::String(_)
| Value::Function(_)
| Value::Native(_) => None,
}
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
fn cookie_getter_impl(_args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let visible = read_string_property(&this, &heap, VISIBLE_KEY).unwrap_or_default();
Ok((Outcome::Normal(Value::String(visible)), heap, fuel))
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
fn cookie_setter_impl(args: Vec<Value>, this: Value, heap: Heap, fuel: Fuel) -> EvalResult {
let raw = string_arg(&args, 0);
let outcome_heap = object_id_of(&this)
.and_then(|id| heap.object(id).cloned().map(|obj| (id, obj)))
.map_or(heap.clone(), |(id, obj)| {
let visible = read_string_property(&this, &heap, VISIBLE_KEY).unwrap_or_default();
let writes = read_string_property(&this, &heap, WRITES_KEY).unwrap_or_default();
let new_visible = update_visible(&visible, &raw);
let new_writes = append_write_log(&writes, &raw);
let updated = update_hidden_state(&obj, new_visible, new_writes);
heap.clone().store_object(id, updated).unwrap_or_else(|h| h)
});
Ok((Outcome::Normal(Value::Undefined), outcome_heap, fuel))
}
fn update_hidden_state(obj: &Object, visible: String, writes: String) -> Object {
obj.with(VISIBLE_KEY.to_owned(), Value::String(visible))
.with(WRITES_KEY.to_owned(), Value::String(writes))
}
fn string_arg(args: &[Value], idx: usize) -> String {
match args.get(idx) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => format!("{n}"),
Some(Value::Boolean(b)) => format!("{b}"),
Some(Value::Null) => "null".to_owned(),
Some(Value::Undefined) | None => String::new(),
Some(Value::Object(_) | Value::Function(_) | Value::Native(_)) => "[object]".to_owned(),
}
}
fn append_write_log(existing: &str, entry: &str) -> String {
if existing.is_empty() {
entry.to_owned()
} else {
format!("{existing}\n{entry}")
}
}
fn update_visible(visible: &str, raw_write: &str) -> String {
let head = raw_write.split(';').next().unwrap_or("").trim();
parse_name(head).map_or_else(
|| visible.to_owned(),
|name| {
let kept = retain_other_entries(visible, name);
if kept.is_empty() {
head.to_owned()
} else {
format!("{kept}; {head}")
}
},
)
}
fn parse_name(entry: &str) -> Option<&str> {
entry
.split_once('=')
.map(|(name, _value)| name.trim())
.filter(|name| !name.is_empty())
}
fn retain_other_entries(visible: &str, name_to_drop: &str) -> String {
visible
.split(';')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.filter(|entry| {
parse_name(entry).is_none_or(|name| !name.eq_ignore_ascii_case(name_to_drop))
})
.collect::<Vec<_>>()
.join("; ")
}