///|
priv struct StoredCookie {
name : String
value_text : String
domain : String
domain_explicit : Bool
path : String
http_only : Bool
secure : Bool
same_site : String
expiry : Int?
context_id : String?
user_context : String?
source_origin : String?
}
///|
priv struct StorageCookieFilter {
domain : String?
name : String?
path : String?
http_only : Bool?
secure : Bool?
expiry : Int?
size : Int?
same_site : String?
value_text : String?
}
///|
priv struct StoragePartitionInfo {
kind : String
context_id : String?
user_context : String?
source_origin : String?
partition_key : Map[String, Json]
}
///|
fn storage_now_seconds() -> Int {
(input_now_ms() / 1000.0).to_int()
}
///|
fn storage_slice_from(value : String, start : Int) -> String {
if start > value.length() {
""
} else {
value.unsafe_substring(start~, end=value.length())
}
}
///|
fn storage_strip_leading_dot(value : String) -> String {
if value.has_prefix(".") {
storage_slice_from(value, 1)
} else {
value
}
}
///|
fn storage_cookie_size(cookie : StoredCookie) -> Int {
cookie.name.length() + cookie.value_text.length()
}
///|
fn storage_cookie_value_json(value_text : String) -> Json {
make_object({
"type": Json::string("string"),
"value": Json::string(value_text),
})
}
///|
fn storage_cookie_to_json(cookie : StoredCookie) -> Json {
let obj : Map[String, Json] = {}
obj["name"] = Json::string(cookie.name)
obj["value"] = storage_cookie_value_json(cookie.value_text)
obj["domain"] = Json::string(
if cookie.domain_explicit && cookie.domain.length() > 0 {
"." + cookie.domain
} else {
cookie.domain
},
)
obj["path"] = Json::string(cookie.path)
obj["httpOnly"] = Json::boolean(cookie.http_only)
obj["secure"] = Json::boolean(cookie.secure)
obj["sameSite"] = Json::string(cookie.same_site)
obj["size"] = Json::number(storage_cookie_size(cookie).to_double())
match cookie.expiry {
Some(expiry) => obj["expiry"] = Json::number(expiry.to_double())
None => ()
}
make_object(obj)
}
///|
fn storage_cookie_to_request_json(cookie : StoredCookie) -> Json {
make_object({
"name": Json::string(cookie.name),
"value": storage_cookie_value_json(cookie.value_text),
})
}
///|
fn storage_same_site_is_valid(value : String) -> Bool {
value == "none" || value == "lax" || value == "strict" || value == "default"
}
///|
fn BidiProtocol::dispatch_storage(
self : BidiProtocol,
request : BidiRequest,
action : String,
) -> Result[Unit, String] {
match action {
"getCookies" => {
let cookie_filter = match
self.normalize_storage_cookie_filter(
request.id,
get_param_raw(request.params, "filter"),
) {
Some(filter) => filter
None => return Ok(())
}
let partition = match
self.normalize_storage_partition(
request.id,
get_param_raw(request.params, "partition"),
) {
Some(info) => info
None => return Ok(())
}
let cookies : Array[Json] = []
for cookie in self.list_storage_cookies() {
if self.storage_cookie_matches_partition(cookie, partition) &&
storage_cookie_matches_filter(cookie, cookie_filter) {
cookies.push(storage_cookie_to_json(cookie))
}
}
let result = make_object({
"cookies": Json::array(cookies),
"partitionKey": make_object(partition.partition_key),
})
self.send_success(request.id, Some(result))
Ok(())
}
"setCookie" => {
let raw_cookie = match get_param_raw(request.params, "cookie") {
Some(value) => value
None => {
self.send_error(request.id, "invalid argument", "Missing cookie")
return Ok(())
}
}
let cookie = match
self.normalize_storage_cookie_input(request.id, raw_cookie) {
Some(cookie) => cookie
None => return Ok(())
}
let partition = match
self.normalize_storage_partition(
request.id,
get_param_raw(request.params, "partition"),
) {
Some(info) => info
None => return Ok(())
}
let stored_cookie : StoredCookie = {
..cookie,
context_id: partition.context_id,
user_context: partition.user_context,
source_origin: partition.source_origin,
}
match stored_cookie.expiry {
Some(expiry) =>
if expiry <= storage_now_seconds() {
self.delete_matching_storage_cookie_identity(
stored_cookie.context_id,
stored_cookie.user_context,
stored_cookie.source_origin,
stored_cookie.name,
stored_cookie.domain,
stored_cookie.path,
)
} else {
self.store_storage_cookie(stored_cookie)
}
None => self.store_storage_cookie(stored_cookie)
}
let result = make_object({
"partitionKey": make_object(partition.partition_key),
})
self.send_success(request.id, Some(result))
Ok(())
}
"deleteCookies" => {
let cookie_filter = match
self.normalize_storage_cookie_filter(
request.id,
get_param_raw(request.params, "filter"),
) {
Some(filter) => filter
None => return Ok(())
}
let partition = match
self.normalize_storage_partition(
request.id,
get_param_raw(request.params, "partition"),
) {
Some(info) => info
None => return Ok(())
}
self.delete_storage_cookies(cookie_filter, partition)
let result = make_object({
"partitionKey": make_object(partition.partition_key),
})
self.send_success(request.id, Some(result))
Ok(())
}
"resolveRequestCookies" => {
let context_id = match get_param_raw(request.params, "context") {
Some(String(context_id)) => context_id
Some(_) | None => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return Ok(())
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + context_id,
)
return Ok(())
}
let request_url : String? = match
get_param_raw(request.params, "requestUrl") {
None | Some(Null) => None
Some(String(url)) => Some(url)
Some(_) => {
self.send_error(
request.id,
"invalid argument",
"requestUrl must be a string",
)
return Ok(())
}
}
let cookies = self.resolve_storage_request_cookies(
context_id, request_url,
)
self.send_success(
request.id,
Some(make_object({ "cookies": Json::array(cookies) })),
)
Ok(())
}
"rememberDocumentCookie" => {
let context_id = match get_param_raw(request.params, "context") {
Some(String(context_id)) => context_id
Some(_) | None => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return Ok(())
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + context_id,
)
return Ok(())
}
let cookie_assignment = match get_param_raw(request.params, "cookie") {
Some(String(cookie_assignment)) => cookie_assignment
Some(_) | None => {
self.send_error(
request.id,
"invalid argument",
"cookie must be a string",
)
return Ok(())
}
}
self.remember_document_cookie(context_id, cookie_assignment)
self.send_success(request.id, Some(make_object({})))
Ok(())
}
"getContextCookieInfo" => {
let context_id = match get_param_raw(request.params, "context") {
Some(String(context_id)) => context_id
Some(_) | None => {
self.send_error(
request.id,
"invalid argument",
"context must be a string",
)
return Ok(())
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request.id,
"no such frame",
"Unknown context: " + context_id,
)
return Ok(())
}
let base_url = self.get_context_cookie_base_url(context_id)
let result : Map[String, Json] = {
"baseUrl": Json::string(base_url),
"domain": Json::string(storage_cookie_domain_from_base_url(base_url)),
"defaultPath": Json::string(storage_cookie_default_path(base_url)),
"userContext": Json::string(
self.resolve_storage_user_context(context_id),
),
}
match storage_cookie_origin_from_base_url(base_url) {
Some(origin) => result["origin"] = Json::string(origin)
None => result["origin"] = Json::null()
}
self.send_success(request.id, Some(make_object(result)))
Ok(())
}
_ => {
self.send_error(
request.id,
"unknown command",
"Unknown method: storage." + action,
)
Ok(())
}
}
}
///|
fn BidiProtocol::normalize_storage_cookie_filter(
self : BidiProtocol,
request_id : Int,
raw_filter : Json?,
) -> StorageCookieFilter? {
match raw_filter {
None | Some(Null) =>
Some({
domain: None,
name: None,
path: None,
http_only: None,
secure: None,
expiry: None,
size: None,
same_site: None,
value_text: None,
})
Some(Object(map)) => {
let mut domain : String? = None
let mut name : String? = None
let mut path : String? = None
let mut http_only : Bool? = None
let mut secure : Bool? = None
let mut expiry : Int? = None
let mut size : Int? = None
let mut same_site : String? = None
let mut value_text : String? = None
match map.get("domain") {
Some(String(value)) =>
domain = Some(
storage_strip_leading_dot(value.trim().to_owned().to_lower()),
)
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.domain must be a string",
)
return None
}
}
match map.get("name") {
Some(String(value)) => name = Some(value)
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.name must be a string",
)
return None
}
}
match map.get("path") {
Some(String(value)) => path = Some(value)
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.path must be a string",
)
return None
}
}
match map.get("httpOnly") {
Some(True) => http_only = Some(true)
Some(False) => http_only = Some(false)
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.httpOnly must be a boolean",
)
return None
}
}
match map.get("secure") {
Some(True) => secure = Some(true)
Some(False) => secure = Some(false)
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.secure must be a boolean",
)
return None
}
}
match map.get("expiry") {
Some(value) =>
match storage_json_non_negative_int(value) {
Some(parsed) => expiry = Some(parsed)
None => {
self.send_error(
request_id, "invalid argument", "filter.expiry must be a non-negative integer",
)
return None
}
}
None => ()
}
match map.get("size") {
Some(value) =>
match storage_json_non_negative_int(value) {
Some(parsed) => size = Some(parsed)
None => {
self.send_error(
request_id, "invalid argument", "filter.size must be a non-negative integer",
)
return None
}
}
None => ()
}
match map.get("sameSite") {
Some(String(value)) =>
if storage_same_site_is_valid(value) {
same_site = Some(value)
} else {
self.send_error(
request_id, "invalid argument", "filter.sameSite is invalid",
)
return None
}
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter.sameSite must be a string",
)
return None
}
}
match map.get("value") {
Some(value) =>
match
self.storage_value_text_from_json(request_id, value, "filter.value") {
Some(text) => value_text = Some(text)
None => return None
}
None => ()
}
Some({
domain,
name,
path,
http_only,
secure,
expiry,
size,
same_site,
value_text,
})
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "filter must be an object",
)
None
}
}
}
///|
fn BidiProtocol::normalize_storage_cookie_input(
self : BidiProtocol,
request_id : Int,
raw_cookie : Json,
) -> StoredCookie? {
let map = match raw_cookie {
Object(map) => map
_ => {
self.send_error(
request_id, "invalid argument", "cookie must be an object",
)
return None
}
}
let domain = match map.get("domain") {
Some(String(value)) =>
storage_strip_leading_dot(value.trim().to_owned().to_lower())
_ => {
self.send_error(
request_id, "invalid argument", "cookie.domain must be a string",
)
return None
}
}
let domain_explicit = match map.get("domainExplicit") {
Some(True) => true
Some(False) | Some(Null) | None => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "cookie.domainExplicit must be a boolean",
)
return None
}
}
let name = match map.get("name") {
Some(String(value)) => value
_ => {
self.send_error(
request_id, "invalid argument", "cookie.name must be a string",
)
return None
}
}
let value_text = match map.get("value") {
Some(value) =>
match
self.storage_value_text_from_json(request_id, value, "cookie.value") {
Some(text) => text
None => return None
}
None => {
self.send_error(
request_id, "invalid argument", "cookie.value must be an object",
)
return None
}
}
let path = match map.get("path") {
Some(String(value)) => value
Some(Null) | None => "/"
Some(_) => {
self.send_error(
request_id, "invalid argument", "cookie.path must be a string",
)
return None
}
}
let http_only = match map.get("httpOnly") {
Some(True) => true
Some(False) => false
Some(Null) | None => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "cookie.httpOnly must be a boolean",
)
return None
}
}
let secure = match map.get("secure") {
Some(True) => true
Some(False) => false
Some(Null) | None => false
Some(_) => {
self.send_error(
request_id, "invalid argument", "cookie.secure must be a boolean",
)
return None
}
}
let same_site = match map.get("sameSite") {
Some(String(value)) =>
if storage_same_site_is_valid(value) {
value
} else {
self.send_error(
request_id, "invalid argument", "cookie.sameSite is invalid",
)
return None
}
Some(Null) | None => "none"
Some(_) => {
self.send_error(
request_id, "invalid argument", "cookie.sameSite must be a string",
)
return None
}
}
let expiry = match map.get("expiry") {
Some(value) =>
match storage_json_non_negative_int(value) {
Some(parsed) => Some(parsed)
None => {
self.send_error(
request_id, "invalid argument", "cookie.expiry must be a non-negative integer",
)
return None
}
}
None => None
}
Some({
name,
value_text,
domain,
domain_explicit,
path,
http_only,
secure,
same_site,
expiry,
context_id: None,
user_context: None,
source_origin: None,
})
}
///|
fn BidiProtocol::storage_value_text_from_json(
self : BidiProtocol,
request_id : Int,
raw_value : Json,
field_name : String,
) -> String? {
let map = match raw_value {
Object(map) => map
_ => {
self.send_error(
request_id,
"invalid argument",
field_name + " must be an object",
)
return None
}
}
let value_type = match map.get("type") {
Some(String(value_type)) => value_type
Some(_) => {
self.send_error(
request_id,
"invalid argument",
field_name + ".type must be a string",
)
return None
}
None => {
self.send_error(
request_id,
"invalid argument",
field_name + ".type must be a string",
)
return None
}
}
let payload = match map.get("value") {
Some(String(payload)) => payload
Some(_) => {
self.send_error(
request_id,
"invalid argument",
field_name + ".value must be a string",
)
return None
}
None => {
self.send_error(
request_id,
"invalid argument",
field_name + ".value must be a string",
)
return None
}
}
if value_type == "string" {
return Some(payload)
}
if value_type == "base64" {
return Some(decode_base64(payload))
}
self.send_error(
request_id,
"invalid argument",
field_name + ".type is invalid",
)
None
}
///|
fn BidiProtocol::normalize_storage_partition(
self : BidiProtocol,
request_id : Int,
raw_partition : Json?,
) -> StoragePartitionInfo? {
match raw_partition {
None | Some(Null) =>
Some({
kind: "default",
context_id: None,
user_context: None,
source_origin: None,
partition_key: {},
})
Some(Object(map)) => {
let partition_type = match map.get("type") {
Some(String(value)) => value
Some(_) => {
self.send_error(
request_id, "invalid argument", "partition.type must be a string",
)
return None
}
None => {
self.send_error(
request_id, "invalid argument", "partition.type must be a string",
)
return None
}
}
if partition_type == "context" {
let context_id = match map.get("context") {
Some(String(context_id)) => context_id
Some(_) => {
self.send_error(
request_id, "invalid argument", "partition.context must be a string",
)
return None
}
None => {
self.send_error(
request_id, "invalid argument", "partition.context must be a string",
)
return None
}
}
if !self.manager.has_session(context_id) {
self.send_error(
request_id,
"no such frame",
"Unknown context: " + context_id,
)
return None
}
let user_context = self.resolve_storage_user_context(context_id)
let partition_key : Map[String, Json] = {}
partition_key["userContext"] = Json::string(user_context)
match
storage_cookie_origin_from_base_url(
self.get_context_cookie_base_url(context_id),
) {
Some(source_origin) =>
if source_origin.length() > 0 {
partition_key["sourceOrigin"] = Json::string(source_origin)
}
None => ()
}
Some({
kind: "context",
context_id: Some(context_id),
user_context: Some(user_context),
source_origin: storage_cookie_origin_from_base_url(
self.get_context_cookie_base_url(context_id),
),
partition_key,
})
} else if partition_type == "storageKey" {
let partition_key : Map[String, Json] = {}
let mut user_context : String? = None
let mut source_origin : String? = None
match map.get("sourceOrigin") {
Some(String(origin)) => {
source_origin = Some(origin)
partition_key["sourceOrigin"] = Json::string(origin)
}
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "partition.sourceOrigin must be a string",
)
return None
}
}
match map.get("userContext") {
Some(String(user_context_id)) =>
if self.user_contexts.get(user_context_id).unwrap_or(false) {
user_context = Some(user_context_id)
partition_key["userContext"] = Json::string(user_context_id)
} else {
self.send_error(
request_id,
"no such user context",
"Unknown user context: " + user_context_id,
)
return None
}
Some(Null) | None => ()
Some(_) => {
self.send_error(
request_id, "invalid argument", "partition.userContext must be a string",
)
return None
}
}
Some({
kind: "storageKey",
context_id: None,
user_context,
source_origin,
partition_key,
})
} else {
self.send_error(
request_id, "invalid argument", "partition.type is invalid",
)
None
}
}
Some(_) => {
self.send_error(
request_id, "invalid argument", "partition must be an object",
)
None
}
}
}
///|
fn BidiProtocol::resolve_storage_user_context(
self : BidiProtocol,
ctx_id : String,
) -> String {
match self.context_user_context.get(ctx_id) {
Some(user_context) => user_context
None => {
let ancestry = self.storage_context_ancestry(ctx_id)
let mut idx = ancestry.length()
while idx > 0 {
idx = idx - 1
let candidate = ancestry[idx]
match self.context_user_context.get(candidate) {
Some(user_context) => return user_context
None => ()
}
}
"default"
}
}
}
///|
fn BidiProtocol::storage_context_ancestry(
self : BidiProtocol,
ctx_id : String,
) -> Array[String] {
let chain : Array[String] = []
let seen : Map[String, Bool] = {}
let mut current : String? = Some(ctx_id)
while current != None {
match current {
Some(current_id) =>
if current_id.length() == 0 || seen.contains(current_id) {
current = None
} else {
chain.push(current_id)
seen[current_id] = true
current = self.context_parent.get(current_id)
}
None => ()
}
}
let ordered : Array[String] = []
let mut idx = chain.length()
while idx > 0 {
idx = idx - 1
ordered.push(chain[idx])
}
ordered
}
///|
fn BidiProtocol::remember_synthetic_location_href(
self : BidiProtocol,
ctx_id : String,
href : String,
) -> Unit {
if href == "" {
self.synthetic_location_href_by_context.remove(ctx_id)
} else {
self.synthetic_location_href_by_context[ctx_id] = href
}
}
///|
fn BidiProtocol::resolve_synthetic_location_href(
self : BidiProtocol,
ctx_id : String,
) -> String? {
self.synthetic_location_href_by_context.get(ctx_id)
}
///|
fn BidiProtocol::clear_synthetic_location_href(
self : BidiProtocol,
ctx_id : String,
) -> Unit {
self.synthetic_location_href_by_context.remove(ctx_id)
}
///|
fn BidiProtocol::get_context_cookie_base_url(
self : BidiProtocol,
ctx_id : String,
) -> String {
match self.resolve_synthetic_location_href(ctx_id) {
Some(href) if href != "" => return href
_ => ()
}
match self.manager.get_session(ctx_id) {
Some(session) => {
let url = session.get_url()
if url == "about:blank" {
"http://localhost:8000/webdriver/tests/support/empty.html"
} else {
url
}
}
None => "http://localhost:8000/webdriver/tests/support/empty.html"
}
}
///|
fn storage_json_non_negative_int(value : Json) -> Int? {
match value {
Number(n, ..) =>
if n < 0.0 || n != n.to_int64().to_double() {
None
} else {
Some(n.to_int64().to_int())
}
_ => None
}
}
///|
fn storage_extract_url_fragment_param(url : String, key : String) -> String? {
match find_substring(url, "#", 0) {
Some(hash_idx) => {
let start = hash_idx + 1
if start >= url.length() {
return None
}
let fragment = url.unsafe_substring(start~, end=url.length())
for part in fragment.split("&") {
let raw = part.to_owned()
if raw.length() == 0 {
continue
}
match find_substring(raw, "=", 0) {
Some(eq_idx) => {
let part_key = raw.unsafe_substring(start=0, end=eq_idx)
if part_key == key {
let value_start = eq_idx + 1
if value_start <= raw.length() {
return Some(
raw.unsafe_substring(start=value_start, end=raw.length()),
)
}
return Some("")
}
}
None => if raw == key { return Some("") }
}
}
None
}
None => None
}
}
///|
fn storage_inline_cookie_host(url : String) -> String {
let domain_value = storage_extract_url_fragment_param(url, "domain").unwrap_or(
"",
)
let base_host = if domain_value == "" || domain_value == "default" {
"localhost"
} else if domain_value == "alt" {
"alt.localhost"
} else if domain_value.contains(".") || domain_value.contains(":") {
domain_value
} else {
domain_value + ".localhost"
}
let subdomain = storage_extract_url_fragment_param(url, "subdomain").unwrap_or(
"",
)
if subdomain == "" {
base_host
} else {
subdomain + "." + base_host
}
}
///|
fn storage_http_host(url : String, scheme_len : Int) -> String? {
let chars = url.to_array()
let mut end = chars.length()
for i = scheme_len; i < chars.length(); i = i + 1 {
if chars[i] == '/' || chars[i] == '?' || chars[i] == '#' {
end = i
break
}
}
if end <= scheme_len {
return None
}
let host = url.unsafe_substring(start=scheme_len, end~)
if host.length() == 0 {
None
} else {
Some(host)
}
}
///|
fn storage_strip_port(host : String) -> String {
match find_substring(host, ":", 0) {
Some(idx) => host.unsafe_substring(start=0, end=idx)
None => host
}
}
///|
fn storage_http_path(url : String, scheme_len : Int) -> String {
let chars = url.to_array()
let mut start = chars.length()
for i = scheme_len; i < chars.length(); i = i + 1 {
if chars[i] == '/' {
start = i
break
}
if chars[i] == '?' || chars[i] == '#' {
return "/"
}
}
if start >= chars.length() {
return "/"
}
let mut end = chars.length()
for i = start; i < chars.length(); i = i + 1 {
if chars[i] == '?' || chars[i] == '#' {
end = i
break
}
}
let path = url.unsafe_substring(start~, end~)
if path.length() == 0 {
"/"
} else {
path
}
}
///|
fn storage_cookie_origin_from_base_url(base_url : String) -> String? {
if base_url.has_prefix("http://") {
match storage_http_host(base_url, 7) {
Some(host) => Some("http://" + host)
None => None
}
} else if base_url.has_prefix("https://") {
match storage_http_host(base_url, 8) {
Some(host) => Some("https://" + host)
None => None
}
} else if base_url.has_prefix("data:") {
let protocol = match
storage_extract_url_fragment_param(base_url, "protocol") {
Some("https") => "https"
_ => "http"
}
Some(protocol + "://" + storage_inline_cookie_host(base_url) + ":8000")
} else {
None
}
}
///|
fn storage_cookie_domain_from_base_url(base_url : String) -> String {
if base_url.has_prefix("http://") {
match storage_http_host(base_url, 7) {
Some(host) => storage_strip_port(host).to_lower()
None => "localhost"
}
} else if base_url.has_prefix("https://") {
match storage_http_host(base_url, 8) {
Some(host) => storage_strip_port(host).to_lower()
None => "localhost"
}
} else if base_url.has_prefix("data:") {
storage_inline_cookie_host(base_url).to_lower()
} else {
"localhost"
}
}
///|
fn storage_cookie_default_path(base_url : String) -> String {
let path = if base_url.has_prefix("http://") {
storage_http_path(base_url, 7)
} else if base_url.has_prefix("https://") {
storage_http_path(base_url, 8)
} else {
return "/webdriver/tests/support"
}
if path == "/" || !path.has_prefix("/") {
return "/"
}
if path.has_suffix("/") {
let trimmed = path.unsafe_substring(start=0, end=path.length() - 1)
if trimmed.length() == 0 {
"/"
} else {
trimmed
}
} else {
match find_last_slash(path) {
Some(idx) =>
if idx == 0 {
"/"
} else {
path.unsafe_substring(start=0, end=idx)
}
None => "/"
}
}
}
///|
fn storage_request_path(request_url : String) -> String {
if request_url.has_prefix("http://") {
storage_http_path(request_url, 7)
} else if request_url.has_prefix("https://") {
storage_http_path(request_url, 8)
} else {
storage_cookie_default_path(request_url)
}
}
///|
fn find_last_slash(value : String) -> Int? {
let chars = value.to_array()
let mut idx = chars.length()
while idx > 0 {
idx = idx - 1
if chars[idx] == '/' {
return Some(idx)
}
}
None
}
///|
fn storage_origin_host(origin : String) -> String? {
if origin.has_prefix("http://") {
match storage_http_host(origin, 7) {
Some(host) => Some(storage_strip_port(host).to_lower())
None => None
}
} else if origin.has_prefix("https://") {
match storage_http_host(origin, 8) {
Some(host) => Some(storage_strip_port(host).to_lower())
None => None
}
} else {
None
}
}
///|
fn storage_origins_equivalent(lhs : String?, rhs : String?) -> Bool {
match (lhs, rhs) {
(Some(left), Some(right)) =>
if left == right {
true
} else {
match (storage_origin_host(left), storage_origin_host(right)) {
(Some(left_host), Some(right_host)) =>
left_host.length() > 0 && left_host == right_host
_ => false
}
}
(None, None) => true
_ => false
}
}
///|
fn storage_make_cookie_identity(cookie : StoredCookie) -> String {
cookie.name +
"|" +
cookie.domain +
"|" +
cookie.path +
"|" +
cookie.context_id.unwrap_or("") +
"|" +
cookie.user_context.unwrap_or("") +
"|" +
cookie.source_origin.unwrap_or("")
}
///|
fn storage_cookie_matches_request_url(
cookie : StoredCookie,
request_url : String?,
) -> Bool {
match request_url {
None => true
Some(url) =>
if url.length() == 0 {
true
} else {
let request_host = storage_cookie_domain_from_base_url(url).to_lower()
let request_path = storage_request_path(url)
let normalized_domain = storage_strip_leading_dot(
cookie.domain.to_lower(),
)
if normalized_domain.length() > 0 &&
request_host != normalized_domain &&
!request_host.has_suffix("." + normalized_domain) {
return false
}
let cookie_path = cookie.path
if cookie_path.length() == 0 || cookie_path == "/" {
return true
}
if request_path == cookie_path {
return true
}
if !request_path.has_prefix(cookie_path) {
return false
}
if cookie_path.has_suffix("/") {
return true
}
if request_path.length() == cookie_path.length() {
return true
}
let request_chars = request_path.to_array()
cookie_path.length() < request_chars.length() &&
request_chars[cookie_path.length()] == '/'
}
}
}
///|
fn BidiProtocol::store_storage_cookie(
self : BidiProtocol,
cookie : StoredCookie,
) -> Unit {
let scope_id = match cookie.context_id {
Some(context_id) => context_id
None =>
"storage::" +
cookie.user_context.unwrap_or("default") +
"::" +
cookie.source_origin.unwrap_or("")
}
let jar : Map[String, StoredCookie] = self.storage_cookies_by_scope
.get(scope_id)
.unwrap_or({})
jar[storage_make_cookie_identity(cookie)] = cookie
self.storage_cookies_by_scope[scope_id] = jar
}
///|
fn BidiProtocol::delete_matching_storage_cookie_identity(
self : BidiProtocol,
context_id : String?,
user_context : String?,
source_origin : String?,
name : String,
domain : String,
path : String,
) -> Unit {
let scope_ids : Array[String] = []
for scope_id, _ in self.storage_cookies_by_scope {
scope_ids.push(scope_id)
}
for scope_id in scope_ids {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) => {
let removal_ids : Array[String] = []
for cookie_id, cookie in jar {
if cookie.name != name ||
cookie.domain != domain ||
cookie.path != path {
continue
}
match context_id {
Some(expected_context) =>
if cookie.context_id != Some(expected_context) {
continue
}
None => ()
}
match user_context {
Some(expected_user_context) =>
if cookie.user_context != Some(expected_user_context) {
continue
}
None => ()
}
match source_origin {
Some(expected_source_origin) =>
if cookie.source_origin != Some(expected_source_origin) {
continue
}
None => ()
}
removal_ids.push(cookie_id)
}
for cookie_id in removal_ids {
jar.remove(cookie_id)
}
if jar.length() == 0 {
self.storage_cookies_by_scope.remove(scope_id)
} else {
self.storage_cookies_by_scope[scope_id] = jar
}
}
None => ()
}
}
}
///|
fn BidiProtocol::resolve_storage_request_cookies(
self : BidiProtocol,
ctx_id : String,
request_url : String?,
) -> Array[Json] {
self.prune_expired_storage_cookies()
let merged : Map[String, StoredCookie] = {}
for scope_id in self.storage_context_ancestry(ctx_id) {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) =>
for cookie_id, cookie in jar {
merged[cookie_id] = cookie
}
None => ()
}
}
let cookies : Array[Json] = []
for _, cookie in merged {
if storage_cookie_matches_request_url(cookie, request_url) {
cookies.push(storage_cookie_to_request_json(cookie))
}
}
cookies
}
///|
/// Return the full cookie attribute set for every cookie reachable from the
/// supplied browsing context (ancestry-aware). Used by the BiDi runtime fetch
/// shim to inject Cookie headers — see `set_runtime_context_cookies` and the
/// JS-side `__bidiResolveCookies` bridge.
fn BidiProtocol::cookies_for_context_full_json(
self : BidiProtocol,
ctx_id : String,
) -> Array[Json] {
self.prune_expired_storage_cookies()
let merged : Map[String, StoredCookie] = {}
for scope_id in self.storage_context_ancestry(ctx_id) {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) =>
for cookie_id, cookie in jar {
merged[cookie_id] = cookie
}
None => ()
}
}
let cookies : Array[Json] = []
for _, cookie in merged {
cookies.push(
make_object({
"name": Json::string(cookie.name),
"value": Json::string(cookie.value_text),
"domain": Json::string(cookie.domain),
"path": Json::string(cookie.path),
"secure": Json::boolean(cookie.secure),
"httpOnly": Json::boolean(cookie.http_only),
"sameSite": Json::string(cookie.same_site),
}),
)
}
cookies
}
///|
fn BidiProtocol::prune_expired_storage_cookies(self : BidiProtocol) -> Unit {
let now = storage_now_seconds()
let scope_ids : Array[String] = []
for scope_id, _ in self.storage_cookies_by_scope {
scope_ids.push(scope_id)
}
for scope_id in scope_ids {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) => {
let removal_ids : Array[String] = []
for cookie_id, cookie in jar {
match cookie.expiry {
Some(expiry) => if expiry <= now { removal_ids.push(cookie_id) }
None => ()
}
}
for cookie_id in removal_ids {
jar.remove(cookie_id)
}
if jar.length() == 0 {
self.storage_cookies_by_scope.remove(scope_id)
} else {
self.storage_cookies_by_scope[scope_id] = jar
}
}
None => ()
}
}
}
///|
fn BidiProtocol::list_storage_cookies(
self : BidiProtocol,
) -> Array[StoredCookie] {
self.prune_expired_storage_cookies()
let cookies : Array[StoredCookie] = []
for _, jar in self.storage_cookies_by_scope {
for _, cookie in jar {
cookies.push(cookie)
}
}
cookies
}
///|
fn BidiProtocol::storage_cookie_matches_partition(
self : BidiProtocol,
cookie : StoredCookie,
partition : StoragePartitionInfo,
) -> Bool {
if partition.kind == "default" {
return true
}
if partition.kind == "context" {
return cookie.context_id == partition.context_id
}
match partition.user_context {
Some(user_context) =>
if cookie.user_context != Some(user_context) {
return false
}
None => ()
}
match partition.source_origin {
Some(source_origin) => {
let candidate_source_origin = self.storage_cookie_effective_source_origin(
cookie,
)
storage_origins_equivalent(candidate_source_origin, Some(source_origin))
}
None => true
}
}
///|
fn BidiProtocol::storage_cookie_effective_source_origin(
self : BidiProtocol,
cookie : StoredCookie,
) -> String? {
match cookie.context_id {
Some(context_id) =>
match
storage_cookie_origin_from_base_url(
self.get_context_cookie_base_url(context_id),
) {
Some(origin) => Some(origin)
None => cookie.source_origin
}
None => cookie.source_origin
}
}
///|
fn storage_cookie_matches_filter(
cookie : StoredCookie,
filter : StorageCookieFilter,
) -> Bool {
match filter.domain {
Some(domain) => if cookie.domain != domain { return false }
None => ()
}
match filter.name {
Some(name) => if cookie.name != name { return false }
None => ()
}
match filter.path {
Some(path) => if cookie.path != path { return false }
None => ()
}
match filter.http_only {
Some(http_only) => if cookie.http_only != http_only { return false }
None => ()
}
match filter.secure {
Some(secure) => if cookie.secure != secure { return false }
None => ()
}
match filter.expiry {
Some(expiry) => if cookie.expiry != Some(expiry) { return false }
None => ()
}
match filter.size {
Some(size) => if storage_cookie_size(cookie) != size { return false }
None => ()
}
match filter.same_site {
Some(same_site) => if cookie.same_site != same_site { return false }
None => ()
}
match filter.value_text {
Some(value_text) => if cookie.value_text != value_text { return false }
None => ()
}
true
}
///|
fn BidiProtocol::delete_storage_cookies(
self : BidiProtocol,
cookie_filter : StorageCookieFilter,
partition : StoragePartitionInfo,
) -> Unit {
self.prune_expired_storage_cookies()
let scope_ids : Array[String] = []
for scope_id, _ in self.storage_cookies_by_scope {
scope_ids.push(scope_id)
}
for scope_id in scope_ids {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) => {
let removal_ids : Array[String] = []
for cookie_id, cookie in jar {
if self.storage_cookie_matches_partition(cookie, partition) &&
storage_cookie_matches_filter(cookie, cookie_filter) {
removal_ids.push(cookie_id)
}
}
for cookie_id in removal_ids {
jar.remove(cookie_id)
}
if jar.length() == 0 {
self.storage_cookies_by_scope.remove(scope_id)
} else {
self.storage_cookies_by_scope[scope_id] = jar
}
}
None => ()
}
}
}
///|
fn BidiProtocol::remove_storage_cookie_scope(
self : BidiProtocol,
scope_id : String,
) -> Unit {
self.storage_cookies_by_scope.remove(scope_id)
}
///|
fn BidiProtocol::remove_storage_cookies_for_user_context(
self : BidiProtocol,
user_context_id : String,
) -> Unit {
let scope_ids : Array[String] = []
for scope_id, _ in self.storage_cookies_by_scope {
scope_ids.push(scope_id)
}
for scope_id in scope_ids {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) => {
let removal_ids : Array[String] = []
for cookie_id, cookie in jar {
if cookie.user_context == Some(user_context_id) {
removal_ids.push(cookie_id)
}
}
for cookie_id in removal_ids {
jar.remove(cookie_id)
}
if jar.length() == 0 {
self.storage_cookies_by_scope.remove(scope_id)
} else {
self.storage_cookies_by_scope[scope_id] = jar
}
}
None => ()
}
}
}
///|
fn BidiProtocol::resolve_document_cookie(
self : BidiProtocol,
ctx_id : String,
) -> String? {
self.prune_expired_storage_cookies()
let merged : Map[String, StoredCookie] = {}
for scope_id in self.storage_context_ancestry(ctx_id) {
match self.storage_cookies_by_scope.get(scope_id) {
Some(jar) =>
for cookie_id, cookie in jar {
merged[cookie_id] = cookie
}
None => ()
}
}
let visible : Array[String] = []
for _, cookie in merged {
if cookie.http_only {
continue
}
visible.push(cookie.name + "=" + cookie.value_text)
}
if visible.length() == 0 {
None
} else {
Some(visible.join("; "))
}
}
///|
fn BidiProtocol::remember_document_cookie(
self : BidiProtocol,
ctx_id : String,
cookie_assignment : String,
) -> Unit {
let base_url = self.get_context_cookie_base_url(ctx_id)
let parsed_cookie = match
@crater_network.synthetic_network_parse_document_cookie_assignment(
cookie_assignment,
base_url,
storage_cookie_default_path(base_url),
storage_now_seconds(),
) {
Some(cookie) => cookie
None => return
}
let user_context = self.resolve_storage_user_context(ctx_id)
let source_origin = storage_cookie_origin_from_base_url(base_url)
let cookie : StoredCookie = {
name: parsed_cookie.name,
value_text: parsed_cookie.value_text,
domain: parsed_cookie.domain,
domain_explicit: parsed_cookie.domain_explicit,
path: parsed_cookie.path,
http_only: parsed_cookie.http_only,
secure: parsed_cookie.secure,
same_site: parsed_cookie.same_site,
expiry: parsed_cookie.expiry,
context_id: Some(ctx_id),
user_context: Some(user_context),
source_origin,
}
match cookie.expiry {
Some(cookie_expiry) =>
if cookie_expiry <= storage_now_seconds() {
self.delete_matching_storage_cookie_identity(
cookie.context_id,
cookie.user_context,
cookie.source_origin,
cookie.name,
cookie.domain,
cookie.path,
)
} else {
self.store_storage_cookie(cookie)
}
None => self.store_storage_cookie(cookie)
}
}
///|
fn extract_document_cookie_assignment(expression : String) -> String? {
let trimmed = expression.trim().to_owned()
if !trimmed.has_prefix("document.cookie") {
return None
}
let eq_idx = match find_substring(trimmed, "=", 0) {
Some(idx) => idx
None => return None
}
let chars = trimmed.to_array()
let mut cursor = eq_idx + 1
while cursor < chars.length() && chars[cursor] == ' ' {
cursor = cursor + 1
}
if cursor >= chars.length() {
return None
}
let quote = chars[cursor]
if quote != '\'' && quote != '"' {
return None
}
let mut end = cursor + 1
while end < chars.length() && chars[end] != quote {
end = end + 1
}
if end >= chars.length() {
return None
}
let start = cursor + 1
if start > end {
return Some("")
}
Some(trimmed.unsafe_substring(start~, end~))
}
///|
fn BidiProtocol::try_handle_synthetic_window_location_href_eval(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
realm_id : String,
expression : String,
unwrap_result : Bool,
) -> Bool {
let expr = expression.trim().to_owned()
if expr != "window.location.href" {
return false
}
match self.resolve_synthetic_location_href(ctx_id) {
Some(href) => {
let remote_value = make_object({
"type": Json::string("string"),
"value": Json::string(href),
})
if unwrap_result {
self.send_script_synthetic_value_response(request_id, remote_value)
} else {
self.send_script_remote_value_response(
request_id, realm_id, remote_value, false,
)
}
true
}
None => false
}
}
///|
fn BidiProtocol::try_handle_synthetic_document_cookie_eval(
self : BidiProtocol,
request_id : Int,
ctx_id : String,
realm_id : String,
expression : String,
unwrap_result : Bool,
) -> Bool {
let expr = expression.trim().to_owned()
if expr == "document.cookie" {
let value = self.resolve_document_cookie(ctx_id).unwrap_or("")
self.send_script_remote_value_response(
request_id,
realm_id,
make_object({
"type": Json::string("string"),
"value": Json::string(value),
}),
unwrap_result,
)
return true
}
match extract_document_cookie_assignment(expr) {
Some(cookie_assignment) => {
self.remember_document_cookie(ctx_id, cookie_assignment)
self.send_script_undefined_result(request_id, realm_id)
true
}
None => false
}
}
///|
/// Persist Set-Cookie response headers from an outbound fetch into the
/// partition cookie jar, scoped to `ctx_id`'s user context. Each entry in
/// `set_cookie_headers` is the raw `Set-Cookie` header value (no name=
/// prefix; just the cookie definition like
/// `sid=abc; Path=/; HttpOnly; SameSite=Lax`). Mirrors what the network
/// pipeline does in `bidi_network_continue_response.mbt` for intercepted
/// responses, but flows from the fetch shim's response handler.
///
/// Closes `bug.bidi.form-submit-no-followup-navigation`: POST /login
/// returns 302 + Set-Cookie, the fetch shim follows the redirect to
/// /dashboard, but the follow-up GET had no Cookie attached because the
/// inbound Set-Cookie was being discarded. Outbound resolution was already
/// in place via `__bidiResolveCookies`; this is the inbound counterpart.
fn BidiProtocol::persist_cookies_from_response(
self : BidiProtocol,
ctx_id : String,
request_url : String,
set_cookie_headers : Array[String],
) -> Unit {
if set_cookie_headers.length() == 0 {
return
}
let base_url = if request_url.length() > 0 {
request_url
} else {
self.get_context_cookie_base_url(ctx_id)
}
let user_context = self.resolve_storage_user_context(ctx_id)
let source_origin = storage_cookie_origin_from_base_url(base_url)
let now = storage_now_seconds()
for header_value in set_cookie_headers {
let parsed = match
@crater_network.synthetic_network_parse_set_cookie_header_value(
header_value, base_url, now,
) {
Some(cookie) => cookie
None => continue
}
let stored : StoredCookie = {
name: parsed.name,
value_text: parsed.value_text,
domain: parsed.domain,
domain_explicit: parsed.domain_explicit,
path: parsed.path,
http_only: parsed.http_only,
secure: parsed.secure,
same_site: parsed.same_site,
expiry: parsed.expiry,
context_id: Some(ctx_id),
user_context: Some(user_context),
source_origin,
}
match stored.expiry {
Some(expiry) =>
if expiry <= now {
self.delete_matching_storage_cookie_identity(
stored.context_id,
stored.user_context,
stored.source_origin,
stored.name,
stored.domain,
stored.path,
)
} else {
self.store_storage_cookie(stored)
}
None => self.store_storage_cookie(stored)
}
}
}
///|
/// Drain the JS-side `__bidiPendingCookieIngest` buffer and persist each
/// recorded Set-Cookie observation into the partition jar. Called from
/// `apply_effective_viewport_to_runtime_context` so every script.evaluate
/// boundary catches cookies that the fetch shim recorded since the last
/// evaluate. Mirrors the outbound-snapshot push pattern used by
/// `set_runtime_context_cookies`.
fn BidiProtocol::flush_pending_cookie_ingest(self : BidiProtocol) -> Unit {
let drained = drain_pending_cookie_ingest()
if drained.length() == 0 {
return
}
let parsed = @json.parse(drained) catch { _ => return }
let entries = match parsed {
Json::Array(items) => items
_ => return
}
for entry in entries {
let map = match entry {
Json::Object(map) => map
_ => continue
}
let ctx_id = match map.get("ctx") {
Some(Json::String(value)) => value
_ => continue
}
if !self.manager.has_session(ctx_id) {
continue
}
let request_url = match map.get("url") {
Some(Json::String(value)) => value
_ => ""
}
let headers : Array[String] = []
match map.get("cookies") {
Some(Json::Array(items)) =>
for item in items {
match item {
Json::String(value) => headers.push(value)
_ => ()
}
}
_ => ()
}
self.persist_cookies_from_response(ctx_id, request_url, headers)
}
}