use super::arg_str;
use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
pub const MODULE_METHODS: &[&str] = &[
"parse",
"format",
"fileURLToPath",
"fileURLToPathBuffer",
"pathToFileURL",
"domainToASCII",
"domainToUnicode",
"urlToHttpOptions",
"resolve",
"resolveObject",
];
struct Parts {
protocol: String,
username: String,
password: String,
hostname: String,
port: String,
pathname: String,
search: String,
hash: String,
}
impl Parts {
fn host(&self) -> String {
if self.port.is_empty() {
self.hostname.clone()
} else {
format!("{}:{}", self.hostname, self.port)
}
}
fn origin(&self) -> String {
if self.hostname.is_empty() {
"null".into()
} else {
format!("{}//{}", self.protocol, self.host())
}
}
fn href(&self) -> String {
let auth = if self.username.is_empty() {
String::new()
} else if self.password.is_empty() {
format!("{}@", self.username)
} else {
format!("{}:{}@", self.username, self.password)
};
format!(
"{}//{auth}{}{}{}{}",
self.protocol,
self.host(),
self.pathname,
self.search,
self.hash
)
}
}
fn parse_absolute(input: &str) -> Option<Parts> {
let (scheme, rest) = input.split_once("://")?;
if scheme.is_empty()
|| !scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
{
return None;
}
let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
let authority = &rest[..auth_end];
let mut tail = &rest[auth_end..];
let (userinfo, hostport) = match authority.rsplit_once('@') {
Some((u, h)) => (u, h),
None => ("", authority),
};
let (username, password) = match userinfo.split_once(':') {
Some((u, p)) => (u.to_string(), p.to_string()),
None => (userinfo.to_string(), String::new()),
};
let (hostname, port) = match hostport.split_once(':') {
Some((h, p)) => (h.to_string(), p.to_string()),
None => (hostport.to_string(), String::new()),
};
let hash = match tail.find('#') {
Some(i) => {
let h = tail[i..].to_string();
tail = &tail[..i];
h
}
None => String::new(),
};
let search = match tail.find('?') {
Some(i) => {
let s = tail[i..].to_string();
tail = &tail[..i];
s
}
None => String::new(),
};
let pathname = if tail.is_empty() {
"/".to_string()
} else {
normalize_path(tail)
};
Some(Parts {
protocol: format!("{scheme}:"),
username,
password,
hostname,
port,
pathname,
search,
hash,
})
}
fn normalize_path(path: &str) -> String {
if !path.contains('.') {
return path.to_string();
}
let rooted = path.starts_with('/');
let mut out: Vec<&str> = Vec::new();
let mut trailing_slash = false;
for seg in path.split('/') {
match seg {
"." => trailing_slash = true,
".." => {
out.pop();
trailing_slash = true;
}
_ => {
out.push(seg);
trailing_slash = false;
}
}
}
if rooted && out.first() != Some(&"") {
out.insert(0, "");
}
let mut joined = out.join("/");
if trailing_slash && !joined.ends_with('/') {
joined.push('/');
}
if joined.is_empty() {
joined.push('/');
}
joined
}
pub fn construct(args: &[Value]) -> Result<Value, String> {
let input = arg_str(args, 0);
let parts = parse_absolute(&input)
.or_else(|| {
if args.len() > 1 {
let base = arg_str(args, 1);
parse_absolute(&base).map(|mut b| {
let mut rest = input.as_str();
let hash = match rest.find('#') {
Some(i) => {
let h = rest[i..].to_string();
rest = &rest[..i];
h
}
None => String::new(),
};
let search = match rest.find('?') {
Some(i) => {
let q = rest[i..].to_string();
rest = &rest[..i];
q
}
None => String::new(),
};
let merged = if rest.starts_with('/') {
rest.to_string()
} else if rest.is_empty() {
b.pathname.clone()
} else {
let dir = match b.pathname.rfind('/') {
Some(i) => &b.pathname[..=i],
None => "/",
};
format!("{dir}{rest}")
};
b.pathname = normalize_path(&merged);
b.search = search;
b.hash = hash;
b
})
} else {
None
}
})
.ok_or_else(|| {
crate::host::plain_coded_error("TypeError", "ERR_INVALID_URL", "Invalid URL")
})?;
Ok(build(&parts))
}
fn build(p: &Parts) -> Value {
let query = p.search.strip_prefix('?').unwrap_or(&p.search);
let search_params = make_search_params(&parse_query(query));
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("URL"));
m.insert("href".into(), h.new_str(p.href()));
m.insert("origin".into(), h.new_str(p.origin()));
m.insert("protocol".into(), h.new_str(p.protocol.clone()));
m.insert("username".into(), h.new_str(p.username.clone()));
m.insert("password".into(), h.new_str(p.password.clone()));
m.insert("host".into(), h.new_str(p.host()));
m.insert("hostname".into(), h.new_str(p.hostname.clone()));
m.insert("port".into(), h.new_str(p.port.clone()));
m.insert("pathname".into(), h.new_str(p.pathname.clone()));
m.insert("search".into(), h.new_str(p.search.clone()));
m.insert("searchParams".into(), search_params);
m.insert("hash".into(), h.new_str(p.hash.clone()));
h.new_object(m)
})
}
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"parse" => legacy_parse(args).map(|u| super::url_legacy::to_js(&u)),
"format" => super::url_legacy::format_value(&args.first().cloned().unwrap_or(Value::Undef)),
"fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
"fileURLToPathBuffer" => {
file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
}
"pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
"domainToASCII" => Ok(punycode_domain(args, true)),
"domainToUnicode" => Ok(punycode_domain(args, false)),
"urlToHttpOptions" => Ok(url_to_http_options(
&args.first().cloned().unwrap_or(Value::Undef),
)),
"resolve" => {
let from = arg_str(args, 0);
let to = arg_str(args, 1);
Ok(with_host(|h| h.new_str(legacy_resolve(&from, &to))))
}
"resolveObject" => {
let from = arg_str(args, 0);
let to = arg_str(args, 1);
let resolved = legacy_resolve(&from, &to);
super::url_legacy::parse(&resolved, false, false).map(|u| super::url_legacy::to_js(&u))
}
_ => return None,
})
}
fn legacy_parse(args: &[Value]) -> Result<super::url_legacy::Url, String> {
super::process::emit_deprecation_warning(
"DEP0169",
"`url.parse()` behavior is not standardized and prone to errors that \
have security implications. Use the WHATWG URL API instead. CVEs are \
not issued for `url.parse()` vulnerabilities.",
);
let input = arg_str(args, 0);
let truthy = |i: usize| {
args.get(i)
.map(|v| with_host(|h| h.truthy(v)))
.unwrap_or(false)
};
super::url_legacy::parse(&input, truthy(1), truthy(2))
}
pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
match method {
"toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("href").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
})),
_ => Err(crate::host::type_error(&format!(
"url.{method} is not a function"
))),
}
}
fn url_href(v: &Value) -> String {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
Some("URL") => p.get("href").map(|x| h.str_of(x)).unwrap_or_default(),
_ => h.str_of(v),
},
_ => h.str_of(v),
})
}
fn file_url_to_path(args: &[Value]) -> Result<String, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
let href = url_href(&v);
let rest = href.strip_prefix("file://").ok_or_else(|| {
crate::host::plain_coded_error(
"TypeError",
"ERR_INVALID_URL_SCHEME",
"The URL must be of scheme file",
)
})?;
let path = match rest.find('/') {
Some(0) => rest,
Some(i) => &rest[i..],
None => "/",
};
Ok(percent_decode(path))
}
fn path_to_file_url(path: &str) -> Value {
let enc = encode_path_component(path);
let pathname = if enc.starts_with('/') {
enc
} else {
format!("/{enc}")
};
let parts = Parts {
protocol: "file:".into(),
username: String::new(),
password: String::new(),
hostname: String::new(),
port: String::new(),
pathname,
search: String::new(),
hash: String::new(),
};
build(&parts)
}
fn punycode_domain(args: &[Value], ascii: bool) -> Value {
let method = if ascii { "toASCII" } else { "toUnicode" };
match super::punycode::call(method, args) {
Some(Ok(v)) => v,
_ => with_host(|h| h.new_str("")),
}
}
fn url_to_http_options(v: &Value) -> Value {
let get = |key: &str| -> String {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
_ => String::new(),
})
};
let protocol = get("protocol");
let mut hostname = get("hostname");
if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
hostname = hostname[1..hostname.len() - 1].to_string();
}
let hash = get("hash");
let search = get("search");
let pathname = get("pathname");
let href = get("href");
let port = get("port");
let username = get("username");
let password = get("password");
let path = format!("{pathname}{search}");
let auth = if username.is_empty() && password.is_empty() {
None
} else {
Some(format!(
"{}:{}",
percent_decode(&username),
percent_decode(&password)
))
};
let port_num = if port.is_empty() {
None
} else {
port.parse::<f64>().ok()
};
with_host(|h| {
let mut m = IndexMap::new();
m.insert("protocol".into(), h.new_str(protocol));
m.insert("hostname".into(), h.new_str(hostname));
m.insert("hash".into(), h.new_str(hash));
m.insert("search".into(), h.new_str(search));
m.insert("pathname".into(), h.new_str(pathname));
m.insert("path".into(), h.new_str(path));
m.insert("href".into(), h.new_str(href));
if let Some(n) = port_num {
m.insert("port".into(), Value::Float(n));
}
if let Some(a) = auth {
m.insert("auth".into(), h.new_str(a));
}
h.new_object(m)
})
}
pub(crate) fn percent_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'%' && i + 2 < b.len() {
if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(b[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn encode_path_component(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
let keep = b.is_ascii_alphanumeric()
|| matches!(
b,
b'/' | b'-'
| b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
);
if keep {
out.push(b as char);
} else {
out.push('%');
out.push(hex_upper(b >> 4));
out.push(hex_upper(b & 0x0f));
}
}
out
}
struct UriRef {
scheme: Option<String>,
authority: Option<String>,
path: String,
query: Option<String>,
fragment: Option<String>,
}
fn split_uri(input: &str) -> UriRef {
let mut rest = input;
let mut scheme = None;
if let Some(colon) = rest.find(':') {
let cand = &rest[..colon];
let scheme_ok = !cand.is_empty()
&& cand.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
&& cand
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
&& cand.find(['/', '?', '#']).is_none();
if scheme_ok {
scheme = Some(cand.to_string());
rest = &rest[colon + 1..];
}
}
let mut fragment = None;
if let Some(h) = rest.find('#') {
fragment = Some(rest[h + 1..].to_string());
rest = &rest[..h];
}
let mut query = None;
if let Some(q) = rest.find('?') {
query = Some(rest[q + 1..].to_string());
rest = &rest[..q];
}
let mut authority = None;
if let Some(r) = rest.strip_prefix("//") {
let end = r.find('/').unwrap_or(r.len());
authority = Some(r[..end].to_string());
rest = &r[end..];
}
UriRef {
scheme,
authority,
path: rest.to_string(),
query,
fragment,
}
}
fn merge_paths(base: &UriRef, ref_path: &str) -> String {
if base.authority.is_some() && base.path.is_empty() {
format!("/{ref_path}")
} else {
match base.path.rfind('/') {
Some(i) => format!("{}{ref_path}", &base.path[..=i]),
None => ref_path.to_string(),
}
}
}
fn remove_last_segment(output: &mut String) {
match output.rfind('/') {
Some(pos) => output.truncate(pos),
None => output.clear(),
}
}
fn remove_dot_segments(path: &str) -> String {
let mut input = path.to_string();
let mut output = String::new();
while !input.is_empty() {
if let Some(r) = input.strip_prefix("../") {
input = r.to_string();
} else if let Some(r) = input.strip_prefix("./") {
input = r.to_string();
} else if let Some(r) = input.strip_prefix("/./") {
input = format!("/{r}");
} else if input == "/." {
input = "/".to_string();
} else if let Some(r) = input.strip_prefix("/../") {
input = format!("/{r}");
remove_last_segment(&mut output);
} else if input == "/.." {
input = "/".to_string();
remove_last_segment(&mut output);
} else if input == "." || input == ".." {
input.clear();
} else {
let start = usize::from(input.starts_with('/'));
let end = input[start..]
.find('/')
.map(|i| start + i)
.unwrap_or(input.len());
output.push_str(&input[..end]);
input.drain(..end);
}
}
output
}
fn resolve_ref(base: &UriRef, r: &UriRef) -> UriRef {
if r.scheme.is_some() {
return UriRef {
scheme: r.scheme.clone(),
authority: r.authority.clone(),
path: remove_dot_segments(&r.path),
query: r.query.clone(),
fragment: r.fragment.clone(),
};
}
let (authority, path, query) = if r.authority.is_some() {
(
r.authority.clone(),
remove_dot_segments(&r.path),
r.query.clone(),
)
} else if r.path.is_empty() {
let q = if r.query.is_some() {
r.query.clone()
} else {
base.query.clone()
};
(base.authority.clone(), base.path.clone(), q)
} else if r.path.starts_with('/') {
(
base.authority.clone(),
remove_dot_segments(&r.path),
r.query.clone(),
)
} else {
(
base.authority.clone(),
remove_dot_segments(&merge_paths(base, &r.path)),
r.query.clone(),
)
};
UriRef {
scheme: base.scheme.clone(),
authority,
path,
query,
fragment: r.fragment.clone(),
}
}
fn recompose(u: &UriRef) -> String {
let mut s = String::new();
if let Some(sc) = &u.scheme {
s.push_str(sc);
s.push(':');
}
if let Some(a) = &u.authority {
s.push_str("//");
s.push_str(a);
}
s.push_str(&u.path);
if let Some(q) = &u.query {
s.push('?');
s.push_str(q);
}
if let Some(f) = &u.fragment {
s.push('#');
s.push_str(f);
}
s
}
fn legacy_resolve(from: &str, to: &str) -> String {
recompose(&resolve_ref(&split_uri(from), &split_uri(to)))
}
pub const SEARCH_PARAMS_METHODS: &[&str] = &[
"get",
"getAll",
"has",
"set",
"append",
"delete",
"keys",
"values",
"entries",
"forEach",
"toString",
"sort",
"@@iterator",
];
fn make_search_params(pairs: &[(String, String)]) -> Value {
with_host(|h| {
let items: Vec<Value> = pairs
.iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
h.new_array(kv)
})
.collect();
let arr = h.new_array(items);
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("URLSearchParams"));
m.insert("@@pairs".into(), arr);
m.insert("size".into(), Value::Float(pairs.len() as f64));
let obj = h.new_object(m);
h.hide_prop(&obj, "size");
obj
})
}
fn pairs_of(recv: &Value) -> Vec<(String, String)> {
with_host(|h| {
let items: Vec<Value> = match h.get(recv) {
Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
items
.iter()
.map(|it| match h.get(it) {
Some(JsObj::Array(kv)) => {
let kv = kv.clone();
let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
(k, v)
}
_ => (h.str_of(it), String::new()),
})
.collect()
})
}
fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
with_host(|h| {
let items: Vec<Value> = pairs
.iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
h.new_array(kv)
})
.collect();
let arr = h.new_array(items);
let n = Value::Float(pairs.len() as f64);
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("@@pairs".into(), arr);
p.insert("size".into(), n);
}
h.hide_prop(recv, "size");
});
}
pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
let pairs = match args.first() {
None => Vec::new(),
Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
Some(v) => pairs_from_init(v),
};
Ok(make_search_params(&pairs))
}
fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
if super::native_tag(v).as_deref() == Some("URLSearchParams") {
return pairs_of(v);
}
if let Some(s) = with_host(|h| h.as_str(v)) {
return parse_query(s.strip_prefix('?').unwrap_or(&s));
}
with_host(|h| match h.get(v) {
Some(JsObj::Array(items)) => {
let items = items.clone();
items
.iter()
.map(|it| match h.get(it) {
Some(JsObj::Array(kv)) => {
let kv = kv.clone();
let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
(k, val)
}
_ => (h.str_of(it), String::new()),
})
.collect()
}
Some(JsObj::Object(p)) => {
let entries: Vec<(String, Value)> = p
.iter()
.filter(|(k, _)| !k.starts_with("@@"))
.map(|(k, val)| (k.clone(), val.clone()))
.collect();
entries
.into_iter()
.map(|(k, val)| (k, h.str_of(&val)))
.collect()
}
_ => Vec::new(),
})
}
pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"get" => {
let name = arg_str(args, 0);
match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
None => Ok(with_host(|h| h.null())),
}
}
"getAll" => {
let name = arg_str(args, 0);
let vals: Vec<String> = pairs_of(recv)
.into_iter()
.filter(|(k, _)| *k == name)
.map(|(_, v)| v)
.collect();
Ok(with_host(|h| {
let items = vals.into_iter().map(|v| h.new_str(v)).collect();
h.new_array(items)
}))
}
"has" => {
let name = arg_str(args, 0);
let pairs = pairs_of(recv);
let found = if args.len() > 1 {
let val = arg_str(args, 1);
pairs.iter().any(|(k, v)| *k == name && *v == val)
} else {
pairs.iter().any(|(k, _)| *k == name)
};
Ok(Value::Bool(found))
}
"append" => {
let mut pairs = pairs_of(recv);
pairs.push((arg_str(args, 0), arg_str(args, 1)));
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"set" => {
let name = arg_str(args, 0);
let val = arg_str(args, 1);
let mut pairs = pairs_of(recv);
let mut seen = false;
pairs.retain_mut(|(k, v)| {
if *k == name {
if seen {
false
} else {
*v = val.clone();
seen = true;
true
}
} else {
true
}
});
if !seen {
pairs.push((name, val));
}
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"delete" => {
let name = arg_str(args, 0);
let mut pairs = pairs_of(recv);
if args.len() > 1 {
let val = arg_str(args, 1);
pairs.retain(|(k, v)| !(*k == name && *v == val));
} else {
pairs.retain(|(k, _)| *k != name);
}
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"sort" => {
let mut pairs = pairs_of(recv);
pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"toString" => {
let s = pairs_of(recv)
.iter()
.map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
.collect::<Vec<_>>()
.join("&");
Ok(with_host(|h| h.new_str(s)))
}
"keys" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"values" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"entries" | "@@iterator" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs
.into_iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k), h.new_str(v)];
h.new_array(kv)
})
.collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"forEach" => {
let cb = args.first().cloned().unwrap_or(Value::Undef);
let this_arg = args.get(1).cloned();
for (k, v) in pairs_of(recv) {
let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
}
Ok(Value::Undef)
}
_ => Err(crate::host::type_error(&format!(
"urlSearchParams.{method} is not a function"
))),
}
}
fn parse_query(q: &str) -> Vec<(String, String)> {
q.split('&')
.filter(|s| !s.is_empty())
.map(|seg| match seg.split_once('=') {
Some((k, v)) => (form_decode(k), form_decode(v)),
None => (form_decode(seg), String::new()),
})
.collect()
}
fn form_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
match b[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
(Some(hi), Some(lo)) => {
out.push((hi << 4) | lo);
i += 3;
}
_ => {
out.push(b'%');
i += 1;
}
},
c => {
out.push(c);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn form_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b' ' => out.push('+'),
b'*' | b'-' | b'.' | b'_' => out.push(b as char),
_ if b.is_ascii_alphanumeric() => out.push(b as char),
_ => {
out.push('%');
out.push(hex_upper(b >> 4));
out.push(hex_upper(b & 0x0f));
}
}
}
out
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
fn hex_upper(n: u8) -> char {
char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
}