1use std::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9use super::js_trim;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ParsedSource {
13 pub uri: String,
15 pub scheme: String,
16 pub name: String,
17 pub params: BTreeMap<String, String>,
19}
20
21fn decode(part: &str) -> String {
24 fn hex(b: u8) -> Option<u8> {
25 (b as char).to_digit(16).map(|d| d as u8)
26 }
27 let spaced = part.replace('+', " ");
28 let bytes = spaced.as_bytes();
29 let mut out = Vec::with_capacity(bytes.len());
30 let mut i = 0;
31 while i < bytes.len() {
32 if bytes[i] == b'%' {
33 match (bytes.get(i + 1).copied().and_then(hex), bytes.get(i + 2).copied().and_then(hex)) {
34 (Some(h), Some(l)) => {
35 out.push(h * 16 + l);
36 i += 3;
37 }
38 _ => return part.to_string(),
39 }
40 } else {
41 out.push(bytes[i]);
42 i += 1;
43 }
44 }
45 String::from_utf8(out).unwrap_or_else(|_| part.to_string())
46}
47
48fn is_line_terminator(c: char) -> bool {
49 matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
50}
51
52pub fn parse_source(uri: &str) -> Option<ParsedSource> {
56 let text = js_trim(uri);
57 let (scheme, rest) = text.split_once(':')?;
58 let mut chars = scheme.chars();
59 if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) || !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-')) {
60 return None;
61 }
62 let (raw_name, query) = match rest.split_once('?') {
63 Some((n, q)) => (n, Some(q)),
64 None => (rest, None),
65 };
66 if raw_name.is_empty() || query.is_some_and(|q| q.chars().any(is_line_terminator)) {
67 return None;
68 }
69 let name = decode(raw_name);
70 if name.is_empty() {
71 return None;
72 }
73 let mut params = BTreeMap::new();
74 for pair in query.unwrap_or("").split('&') {
75 if pair.is_empty() {
76 continue;
77 }
78 let (k, v) = match pair.find('=') {
79 Some(eq) => (&pair[..eq], Some(&pair[eq + 1..])),
80 None => (pair, None),
81 };
82 let key = decode(k);
83 if key.is_empty() {
84 continue;
85 }
86 params.insert(key, v.map(decode).unwrap_or_default());
87 }
88 Some(ParsedSource { uri: uri.to_string(), scheme: scheme.to_lowercase(), name, params })
89}
90
91pub(crate) fn encode_component(s: &str) -> String {
94 let mut out = String::with_capacity(s.len());
95 for b in s.bytes() {
96 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')') {
97 out.push(b as char);
98 } else {
99 out.push_str(&format!("%{b:02X}"));
100 }
101 }
102 out
103}
104
105pub fn source_with_params(source: &str, params: Option<&serde_json::Value>) -> String {
111 let Some(params) = params.and_then(serde_json::Value::as_object) else { return source.to_string() };
112 let mut keys: Vec<&String> = params.keys().collect();
113 keys.sort();
114 let mut pairs = Vec::new();
115 for key in keys {
116 let v = ¶ms[key];
117 if v.is_null() {
118 continue;
119 }
120 let text = match v {
121 serde_json::Value::String(s) => s.clone(),
122 serde_json::Value::Object(_) | serde_json::Value::Array(_) => v.to_string(),
123 other => crate::json::to_js_string(other),
124 };
125 pairs.push(format!("{}={}", encode_component(key), encode_component(&text)));
126 }
127 if pairs.is_empty() {
128 return source.to_string();
129 }
130 format!("{source}{}{}", if source.contains('?') { '&' } else { '?' }, pairs.join("&"))
131}