// URL 解析、规范化与相对地址展开。
//
// 规范化的目标是折叠「服务端必然视为同一资源」的书写差异:scheme/host
// 大小写、协议默认端口、点段、fragment。这些在 RFC 3986 §6.2.2 中属于
// 语法层面的等价,折叠不会改变请求含义。
//
// 查询参数顺序刻意不在默认折叠范围内。RFC 3986 §6.2.3 把 `a=1&b=2` 与
// `b=2&a=1` 归为「可能等价、需 scheme 定义」:服务端完全可以把参数顺序
// 当作语义(有序签名、重复键取值、后端按位置读参都会因此改变结果)。
// 因此默认保留原始顺序,宁可多抓一次也不误判为同一资源;确实需要按
// 无序多重集去重的调用方,显式使用 `normalize_url_sorted_query`。
//
// 内部扫描统一基于 Array[Char],规避 String 索引返回 UTF-16 码元的
// 语义差异;URL 实际场景以 ASCII 为主,代价可忽略。
///|
/// 解析后的 URL。fragment 在解析阶段即被丢弃(不参与去重与请求)。
pub struct Url {
scheme : String // 小写,"http" 或 "https"
host : String // 小写(ASCII)
port : Int32? // None 表示协议默认端口
path : String // 以 '/' 开头
query : String? // 不含 '?'
}
///|
fn chars_of(s : String) -> Array[Char] {
let out : Array[Char] = []
for c in s {
out.push(c)
}
out
}
///|
fn slice(cs : Array[Char], start : Int32, end_ : Int32) -> String {
let buf = StringBuilder()
let mut i : Int32 = start
while i < end_ {
buf.write_char(cs[i])
i = i + 1
}
buf.to_string()
}
///|
fn ascii_lower_slice(cs : Array[Char], a : Int32, b : Int32) -> String {
let buf = StringBuilder()
let mut i : Int32 = a
while i < b {
let c = cs[i]
if c >= 'A' && c <= 'Z' {
buf.write_char((c.to_int() + 32).to_char().unwrap())
} else {
buf.write_char(c)
}
i = i + 1
}
buf.to_string()
}
///|
fn ascii_lower_str(s : String) -> String {
ascii_lower_slice(chars_of(s), 0, s.length())
}
///|
fn find_from(cs : Array[Char], needle : Array[Char], from : Int32) -> Int32? {
let n : Int32 = cs.length()
let m : Int32 = needle.length()
if m == 0 {
return Some(from)
}
let mut i : Int32 = from
while i + m <= n {
let mut j : Int32 = 0
let mut ok : Bool = true
while j < m {
if cs[i + j] != needle[j] {
ok = false
break
}
j = j + 1
}
if ok {
return Some(i)
}
i = i + 1
}
None
}
///|
fn find_char_from_last(cs : Array[Char], c : Char) -> Int32? {
let mut i : Int32 = cs.length() - 1
while i >= 0 {
if cs[i] == c {
return Some(i)
}
i = i - 1
}
None
}
///|
/// 拆分 host[:port]。支持 '[IPv6]' 字面量与 userinfo 之后的尾部。
/// 端口非法时返回 None。
fn split_port(
hp : Array[Char],
start : Int32,
end_ : Int32,
) -> (String, Int32?)? {
let mut colon : Int32 = -1
let mut in_brackets : Bool = false
let mut i : Int32 = start
while i < end_ {
let c = hp[i]
if c == '[' {
in_brackets = true
} else if c == ']' {
in_brackets = false
} else if c == ':' && !in_brackets {
colon = i
break
}
i = i + 1
}
if colon < 0 {
return Some((slice(hp, start, end_), None))
}
let host = slice(hp, start, colon)
let mut j : Int32 = colon + 1
if j == end_ {
// "host:" 视为无端口
return Some((host, None))
}
let mut p : Int32 = 0
while j < end_ {
let c = hp[j]
if c >= '0' && c <= '9' {
p = p * 10 + (c.to_int() - '0'.to_int())
} else {
return None
}
j = j + 1
}
if p < 1 || p > 65535 {
return None
}
Some((host, Some(p)))
}
///|
/// 解析绝对 http/https URL。
/// 非绝对地址、非 http(s) 协议或端口非法时返回 None。
pub fn parse_url(input : String) -> Url? {
let s = chars_of(input)
let full_n : Int32 = s.length()
// fragment 分隔符全局截断:'#' 之后不参与解析
let mut n : Int32 = full_n
let mut fi : Int32 = 0
while fi < full_n {
if s[fi] == '#' {
n = fi
break
}
fi = fi + 1
}
let sep_at = match find_from(s, chars_of("://"), 0) {
Some(i) => i
None => return None
}
let scheme = ascii_lower_slice(s, 0, sep_at)
if scheme != "http" && scheme != "https" {
return None
}
let after : Int32 = sep_at + 3
// authority 结束于第一个 '/' 或 '?'
let mut auth_end : Int32 = n
let mut path_start : Int32 = n
let mut query_start : Int32 = -1
let mut i : Int32 = after
while i < n {
let c = s[i]
if c == '/' {
auth_end = i
path_start = i
break
} else if c == '?' {
auth_end = i
path_start = i
query_start = i
break
}
i = i + 1
}
if query_start < 0 && path_start < n {
let mut j : Int32 = path_start
while j < n {
if s[j] == '?' {
query_start = j
break
}
j = j + 1
}
}
// authority 取最后一个 '@' 之后的部分(丢弃 userinfo)
let mut hp_start : Int32 = after
let mut k : Int32 = after
while k < auth_end {
if s[k] == '@' {
hp_start = k + 1
}
k = k + 1
}
let (host_raw, port) = match split_port(s, hp_start, auth_end) {
Some(hp) => hp
None => return None
}
if host_raw.length() == 0 {
return None
}
let query = if query_start >= 0 {
Some(slice(s, query_start + 1, n))
} else {
None
}
let path_raw = if path_start >= n {
""
} else if query_start >= 0 {
slice(s, path_start, query_start)
} else {
slice(s, path_start, n)
}
let path = if path_raw == "" { "/" } else { path_raw }
Some({ scheme, host: ascii_lower_str(host_raw), port, path, query, })
}
///|
fn is_default_port(scheme : String, p : Int32) -> Bool {
(scheme == "http" && p == 80) || (scheme == "https" && p == 443)
}
///|
/// 折叠路径中的 '.' / '..' 段与重复斜杠,保留结尾斜杠语义。
fn normalize_path(p : String) -> String {
let cs = chars_of(p)
let n : Int32 = cs.length()
if n == 0 {
return "/"
}
let segs : Array[String] = []
let mut start : Int32 = 1
let mut i : Int32 = 1
let mut trailing : Bool = false
while i <= n {
if i == n || cs[i] == '/' {
if i > start {
let seg = slice(cs, start, i)
if seg == ".." {
if segs.length() > 0 {
let _ = segs.pop()
}
} else if seg != "." {
segs.push(seg)
}
}
trailing = i == n && n > 1 && cs[n - 1] == '/'
if i == n {
break
}
i = i + 1
start = i
} else {
i = i + 1
}
}
let buf = StringBuilder()
let mut first : Bool = true
for seg in segs {
if !first {
buf.write_string("/")
}
buf.write_string(seg)
first = false
}
let out = "/" + buf.to_string()
if trailing && out != "/" {
out + "/"
} else {
out
}
}
///|
fn str_cmp(a : String, b : String) -> Int32 {
let ac = chars_of(a)
let bc = chars_of(b)
let n = if ac.length() < bc.length() { ac.length() } else { bc.length() }
let mut i : Int32 = 0
while i < n {
let ca = ac[i].to_int()
let cb = bc[i].to_int()
if ca != cb {
return ca - cb
}
i = i + 1
}
ac.length() - bc.length()
}
///|
fn pair_cmp(x : (String, Bool, String), y : (String, Bool, String)) -> Int32 {
let (k1, h1, v1) = x
let (k2, h2, v2) = y
let c = str_cmp(k1, k2)
if c != 0 {
return c
}
if h1 != h2 {
// 无值参数排在有值参数之前
return if h1 { 1 } else { -1 }
}
str_cmp(v1, v2)
}
///|
/// query 参数按 key(再按 value)字典序排序后重组。
///
/// 仅用于 `normalize_url_sorted_query`:调用方必须先确认目标站点把
/// query 视为无序多重集,否则会改变请求含义(有序签名、重复键取值、
/// 后端按位置读参都可能因此指向不同资源)。
fn sort_query_pairs(q : String) -> String {
let cs = chars_of(q)
let n : Int32 = cs.length()
let pairs : Array[(String, Bool, String)] = []
let mut start : Int32 = 0
let mut i : Int32 = 0
while i <= n {
if i == n || cs[i] == '&' {
if i > start {
let mut eq : Int32 = -1
let mut j : Int32 = start
while j < i {
if cs[j] == '=' {
eq = j
break
}
j = j + 1
}
if eq >= 0 {
pairs.push((slice(cs, start, eq), true, slice(cs, eq + 1, i)))
} else {
pairs.push((slice(cs, start, i), false, ""))
}
}
i = i + 1
start = i
} else {
i = i + 1
}
}
// 插入排序:单个 URL 的参数数量很小
let len : Int32 = pairs.length()
let mut a : Int32 = 1
while a < len {
let cur = pairs[a]
let mut b : Int32 = a - 1
while b >= 0 && pair_cmp(pairs[b], cur) > 0 {
pairs[b + 1] = pairs[b]
b = b - 1
}
pairs[b + 1] = cur
a = a + 1
}
let buf = StringBuilder()
let mut first : Bool = true
for kv in pairs {
let (k, has_v, v) = kv
if !first {
buf.write_string("&")
}
first = false
buf.write_string(k)
if has_v {
buf.write_string("=")
buf.write_string(v)
}
}
buf.to_string()
}
///|
/// 规范化渲染。`sort_query` 决定是否折叠查询参数顺序——默认关闭。
fn render_url(u : Url, sort_query : Bool) -> String {
let buf = StringBuilder()
buf.write_string(u.scheme)
buf.write_string("://")
buf.write_string(u.host)
match u.port {
Some(p) =>
if !is_default_port(u.scheme, p) {
buf.write_string(":")
buf.write_string(p.to_string())
}
None => ()
}
buf.write_string(normalize_path(u.path))
match u.query {
Some(q) =>
if q != "" {
buf.write_string("?")
if sort_query {
buf.write_string(sort_query_pairs(q))
} else {
buf.write_string(q)
}
}
None => ()
}
buf.to_string()
}
///|
/// 保守规范化:小写 scheme/host、省略协议默认端口、折叠点段、
/// 丢弃 fragment,**保留查询参数的原始顺序**。
///
/// 折叠的每一项都是 RFC 3986 §6.2.2 意义上的语法等价;参数顺序涉及
/// §6.2.3 的「需 scheme 定义」情形,默认不动。
pub fn normalize_url(u : Url) -> String {
render_url(u, false)
}
///|
/// 激进规范化:在 `normalize_url` 基础上再把查询参数按 key/value 排序。
///
/// 仅当调用方确认目标站点把 query 当作**无序多重集**时使用。对签名 URL、
/// 依赖参数位置的接口、以重复键表达多值的接口,这样做会把不同资源折叠成
/// 同一个 URL,造成漏抓。
pub fn normalize_url_sorted_query(u : Url) -> String {
render_url(u, true)
}
///|
fn starts_with_root(pp : String) -> Bool {
let cs = chars_of(pp)
cs.length() > 0 && cs[0] == '/'
}
///|
/// 检测 href 是否以非 http(s) 的 scheme 前缀开头
/// (javascript:、mailto: 等),是则拒绝解析。
fn has_foreign_scheme(h : Array[Char]) -> Bool {
let n : Int32 = h.length()
if n == 0 {
return false
}
let first = h[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) {
return false
}
let mut i : Int32 = 0
while i < n {
let c = h[i]
if c == ':' {
let sch = ascii_lower_slice(h, 0, i)
return sch != "http" && sch != "https"
}
let is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
let is_scheme_extra = i > 0 &&
((c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.')
if !is_alpha && !is_scheme_extra {
return false
}
i = i + 1
}
false
}
///|
/// 以 base 为基准展开相对地址。
/// 空地址、纯 fragment、javascript:/mailto: 等非 http(s) 地址返回 None。
pub fn resolve_url(base : Url, href : String) -> Url? {
let h = chars_of(href.trim().to_owned())
let hn : Int32 = h.length()
if hn == 0 {
return None
}
if h[0] == '#' {
return None
}
if has_foreign_scheme(h) {
return None
}
// 丢弃 fragment
let mut end2 : Int32 = hn
let mut f : Int32 = 0
while f < hn {
if h[f] == '#' {
end2 = f
break
}
f = f + 1
}
if end2 == 0 {
return None
}
// 绝对地址
let mut a : Int32 = 0
while a + 3 <= end2 {
if h[a] == ':' && h[a + 1] == '/' && h[a + 2] == '/' {
return parse_url(slice(h, 0, end2))
}
a = a + 1
}
// scheme 相对://host/path
if end2 >= 2 && h[0] == '/' && h[1] == '/' {
return parse_url(base.scheme + ":" + slice(h, 0, end2))
}
// 查询相对:?x=1
if h[0] == '?' {
return Some({
scheme: base.scheme,
host: base.host,
port: base.port,
path: base.path,
query: Some(slice(h, 1, end2)),
})
}
let mut qpos : Int32 = -1
let mut q : Int32 = 0
while q < end2 {
if h[q] == '?' {
qpos = q
break
}
q = q + 1
}
let (path_part, query) = if qpos >= 0 {
(slice(h, 0, qpos), Some(slice(h, qpos + 1, end2)))
} else {
(slice(h, 0, end2), None)
}
let merged = if starts_with_root(path_part) {
path_part
} else {
let bcs = chars_of(base.path)
let dir = match find_char_from_last(bcs, '/') {
Some(idx) => slice(bcs, 0, idx + 1)
None => "/"
}
normalize_path(dir + path_part)
}
Some({
scheme: base.scheme,
host: base.host,
port: base.port,
path: merged,
query,
})
}