// Set-Cookie 的解析(RFC 6265 §5.2)与过期时刻计算。
//
// 有意从简的两处:
// - `Expires` 只认标准 IMF-fixdate(`Sun, 06 Nov 1994 08:49:37 GMT`),
// RFC 850 / asctime 两种过时格式不认——认不出时该 cookie 退化为会话
// cookie(`Max-Age` 优先于 `Expires`,见 RFC 6265 §5.3 step 3);
// - `HttpOnly` 解析出来直接丢弃:客户端库没有 JS 环境的概念,无从谈起。
///|
/// 解析出来的零件:还缺归属域 / 归属路径——那要拿到请求 URL 才算得出
/// (`store` 阶段补上,见 jar.mbt)。
priv struct ParsedSetCookie {
name : String
value : String
/// Domain 属性(去前导点、小写);没带(或剥完点后为空)是 None
domain : String?
/// Path 属性(以 `/` 开头才有效);没带或不合法是 None
path : String?
/// Expires 属性解析出的时刻(Unix 毫秒);没带或认不出是 None
expires_at : UInt64?
/// Max-Age 属性(秒,可负);没带或不是合法整数是 None
max_age : Int64?
secure : Bool
}
///|
/// 解析一条 Set-Cookie 头值。名字为空、或第一段没有 `=` 时整条作废
/// (RFC 6265 §5.2 的口径:坏的 cookie 静默丢弃,不报错)。
fn parse_set_cookie(header : String) -> ParsedSetCookie? {
// 第一段是 name=value,其余每段一个属性
let head = match header.find(";") {
Some(index) => header[:index]
None => header
}
let (name, value) = match head.find("=") {
Some(index) =>
(head[:index].trim().to_owned(), head[index + 1:].trim().to_owned())
None => return None
}
if name.is_empty() {
return None
}
let attrs_text = match header.find(";") {
Some(index) => header[index + 1:].to_owned()
None => ""
}
let mut domain : String? = None
let mut path : String? = None
let mut expires_at : UInt64? = None
let mut max_age : Int64? = None
let mut secure = false
for attr in attrs_text.split(";") {
let attr = attr.trim()
if attr.is_empty() {
continue
}
// 属性名大小写不敏感(RFC 6265 §5.2 开头);无值的属性按空值处理
let (attr_name, attr_value) = match attr.find("=") {
Some(index) =>
(
attr[:index].trim().to_lower().to_owned(),
attr[index + 1:].trim().to_owned(),
)
None => (attr.to_lower().to_owned(), "")
}
match attr_name {
"expires" => expires_at = parse_http_date(attr_value)
"max-age" => max_age = parse_max_age(attr_value)
"domain" => {
// 前导点按历史习惯剥掉(RFC 6265 §5.2.3);剥完为空则忽略该属性
let stripped = if attr_value.has_prefix(".") {
attr_value[1:]
} else {
attr_value
}
if !stripped.is_empty() {
domain = Some(stripped.to_lower().to_owned())
}
}
"path" => if attr_value.has_prefix("/") { path = Some(attr_value) }
"secure" => secure = true
// httponly 与一切未知属性:认出来只为忽略
_ => ()
}
}
Some({ name, value, domain, path, expires_at, max_age, secure, })
}
///|
/// 解析 Max-Age(RFC 6265 §5.2.2):首字符是数字或 `-`、其余全为数字。
/// 非法一律返回 `None`(该属性被忽略,而不是整条 cookie 作废)。
fn parse_max_age(text : String) -> Int64? {
let body = if text.has_prefix("-") { text[1:] } else { text }
if body.is_empty() {
return None
}
let mut value : Int64 = 0L
for ch in body {
guard ch >= '0' && ch <= '9' else { return None }
value = value * 10L + (ch.to_int() - 48).to_int64()
}
Some(if text.has_prefix("-") { -value } else { value })
}
///|
/// 解析 IMF-fixdate(`Sun, 06 Nov 1994 08:49:37 GMT`,RFC 7231 §7.1.1.1)。
///
/// 按「字段按位置取」的思路切:空白切分后,第一段不是数字就当星期跳过
/// (RFC 6265 §5.1.1 对星期不校验),其后依次是日、月、年、`时:分:秒`,
/// GMT 尾段可有可无。认不出返回 `None`,调用方把它当会话 cookie 处理。
fn parse_http_date(text : String) -> UInt64? {
let fields : Array[String] = []
for field in text.split(" ") {
if !field.is_empty() {
fields.push(field.to_owned())
}
}
// 第一段是纯数字时视为「没有星期」:日、月、年、时间从 0 号位开始
let offset = if fields.length() > 0 && is_digits(fields[0]) { 0 } else { 1 }
guard fields.length() >= offset + 4 else { return None }
let day = fields[offset]
let year = fields[offset + 2]
// 年份严格 4 位:两位年份属于过时的 RFC 850 格式,这里有意不认
let day_num = match parse_uint(day, 2) {
Some(number) => number
None => return None
}
let year_num = match parse_uint(year, 4) {
Some(number) if year.length() == 4 => number
_ => return None
}
let month = match month_number(fields[offset + 1]) {
Some(number) => number
None => return None
}
let (hour, minute, second) = match parse_hms(fields[offset + 3]) {
Some(parts) => parts
None => return None
}
let days = days_from_civil(year_num, month, day_num)
let seconds = days * 86400L +
hour.to_int64() * 3600L +
minute.to_int64() * 60L +
second.to_int64()
// 1970 年之前(负秒数)钳成 0:语义上就是「已经过期」
if seconds < 0L {
return Some(0UL)
}
Some((seconds * 1000L).reinterpret_as_uint64())
}
///|
/// 解析 `时:分:秒`;三段都必须是 1~2 位数字。
fn parse_hms(text : String) -> (Int, Int, Int)? {
let parts : Array[String] = []
for part in text.split(":") {
parts.push(part.to_owned())
}
if parts.length() != 3 {
return None
}
let hour = match parse_uint(parts[0], 2) {
Some(number) => number
None => return None
}
let minute = match parse_uint(parts[1], 2) {
Some(number) => number
None => return None
}
let second = match parse_uint(parts[2], 2) {
Some(number) => number
None => return None
}
Some((hour, minute, second))
}
///|
/// 解析一段 1~`max_digits` 位(位数上限只用来挡异常输入,不前导补零)的
/// 非负整数。项目里既有的解析口径是 `@string.parse_int`。
fn parse_uint(text : String, max_digits : Int) -> Int? {
guard text.length() >= 1 && text.length() <= max_digits else { return None }
guard is_digits(text) else { return None }
Some(@string.parse_int(text)) catch {
_ => None
}
}
///|
/// 非空且全为数字。
fn is_digits(text : String) -> Bool {
if text.is_empty() {
return false
}
for ch in text {
guard ch >= '0' && ch <= '9' else { return false }
}
true
}
///|
/// 月份名转序号:只看前三个字母(RFC 6265 §5.1.1 的宽松口径)。
fn month_number(name : StringView) -> Int? {
if name.length() < 3 {
return None
}
match name[:3].to_lower().to_owned() {
"jan" => Some(1)
"feb" => Some(2)
"mar" => Some(3)
"apr" => Some(4)
"may" => Some(5)
"jun" => Some(6)
"jul" => Some(7)
"aug" => Some(8)
"sep" => Some(9)
"oct" => Some(10)
"nov" => Some(11)
"dec" => Some(12)
_ => None
}
}
///|
/// 公历日期到「自 1970-01-01 起的天数」(Howard Hinnant 的
/// days_from_civil 算法,闰年规则内建)。
fn days_from_civil(year : Int, month : Int, day : Int) -> Int64 {
let y = if month <= 2 { year - 1 } else { year }
let m = if month <= 2 { month + 9 } else { month - 3 }
let era = (if y >= 0 { y } else { y - 399 }) / 400
let yoe = y - era * 400
let doy = (153 * m + 2) / 5 + day - 1
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
era.to_int64() * 146097L + doe.to_int64() - 719468L
}
///|
/// Max-Age 换算成毫秒数(供 `store` 加到当前时刻上)。上限钳在约 28 万年:
/// 再大的值语义上等同「永不过期」,钳住是为了乘法不溢出。
fn add_max_age(now : UInt64, max_age : Int64) -> UInt64 {
let clamped = if max_age > 9_000_000_000_000L {
9_000_000_000_000L
} else {
max_age
}
now + (clamped * 1000L).reinterpret_as_uint64()
}