Files
SpotiFLAC-Mobile/rust_backend/crates/network/src/query.rs
T
zarzet aa99726439 feat(backend): migrate production backend to Rust
Replace the Go backend with the Rust workspace and route Android/iOS through
UniFFI bindings. Include the migrated extension runtime, network, providers,
media metadata, downloads and library operations, with version 5.0.0+147.

Remove Go sources, adapters, native build selection and CI dependencies.
Build Rust unconditionally and lock the iOS Rust pod using a relative path.
Retain legacy source and migration evidence in a local ignored archive.

Validation: Android Kotlin compile and 53 native tests; Swift Rust-branch
equivalence and syntax; CocoaPods install; workflow, shell, Ruby, plist and
diff checks. Reuse the preceding 100 Rust tests, fmt/Clippy and five-ABI
build checkpoint; no new APK or iOS application build for this cleanup.

Known follow-up: URL/URLSearchParams globals are missing from the Rust JS
runtime. A controlled extension replay confirms a URL-resolution regression;
this commit does not fix that runtime gap.
2026-09-14 22:58:27 +07:00

80 lines
2.1 KiB
Rust

//! Go net/url query encoding, including invalid-pair and semicolon handling.
use std::collections::BTreeMap;
pub type Query = BTreeMap<Vec<u8>, Vec<Vec<u8>>>;
pub fn parse(query: &str) -> Query {
let mut values = Query::new();
for pair in query
.split('&')
.filter(|pair| !pair.is_empty() && !pair.contains(';'))
{
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
if let (Some(key), Some(value)) = (decode(key), decode(value)) {
values.entry(key).or_default().push(value);
}
}
values
}
pub fn set(values: &mut Query, key: &str, value: &str) {
values.insert(key.as_bytes().to_vec(), vec![value.as_bytes().to_vec()]);
}
pub fn encode(values: &Query) -> String {
values
.iter()
.flat_map(|(key, values)| {
values
.iter()
.map(move |value| format!("{}={}", escape(key), escape(value)))
})
.collect::<Vec<_>>()
.join("&")
}
fn escape(value: &[u8]) -> String {
let mut result = String::new();
for byte in value {
if byte.is_ascii_alphanumeric() || b"-_.~".contains(byte) {
result.push(char::from(*byte));
} else if *byte == b' ' {
result.push('+');
} else {
use std::fmt::Write;
let _ = write!(result, "%{byte:02X}");
}
}
result
}
fn decode(value: &str) -> Option<Vec<u8>> {
let mut result = Vec::new();
let mut bytes = value.bytes();
while let Some(byte) = bytes.next() {
result.push(match byte {
b'+' => b' ',
b'%' => {
(char::from(bytes.next()?).to_digit(16)? * 16
+ char::from(bytes.next()?).to_digit(16)?) as u8
}
_ => byte,
});
}
Some(result)
}
#[cfg(test)]
mod tests {
#[test]
fn query_keeps_bytes_duplicates_and_go_escaping() {
assert_eq!(
super::encode(&super::parse(
"z=one+two&x=%FF&x=%2f&bad=%Q0&semi=a;b&empty&~!=*"
)),
"empty=&x=%FF&x=%2F&z=one+two&~%21=%2A"
);
}
}