mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-04 15:16:58 +02:00
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.
148 lines
4.5 KiB
Rust
148 lines
4.5 KiB
Rust
//! Matching compatibility uses Go's simple case mapping and UTF-8 byte distance.
|
|
|
|
/// Unlike String::to_lowercase, Go's unicode.ToLower maps one rune to one rune.
|
|
pub fn lowercase(value: &str) -> String {
|
|
value
|
|
.chars()
|
|
.map(|ch| match ch {
|
|
// Go 1.26 uses Unicode 15.0; these lowercase mappings were added
|
|
// in Unicode 16/17, used by the pinned Rust toolchain.
|
|
'\u{1c89}'
|
|
| '\u{a7cb}'
|
|
| '\u{a7cc}'
|
|
| '\u{a7ce}'
|
|
| '\u{a7d2}'
|
|
| '\u{a7d4}'
|
|
| '\u{a7da}'
|
|
| '\u{a7dc}'
|
|
| '\u{10d50}'..='\u{10d65}'
|
|
| '\u{16ea0}'..='\u{16eb8}' => ch,
|
|
_ => ch.to_lowercase().next().expect("lowercase character"),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Go's Unicode 15 simple uppercase does not expand characters such as ß.
|
|
pub fn uppercase(value: &str) -> String {
|
|
value
|
|
.chars()
|
|
.map(|ch| match ch {
|
|
'\u{019b}'
|
|
| '\u{0264}'
|
|
| '\u{1c8a}'
|
|
| '\u{a7cd}'
|
|
| '\u{a7cf}'
|
|
| '\u{a7d3}'
|
|
| '\u{a7d5}'
|
|
| '\u{a7db}'
|
|
| '\u{10d70}'..='\u{10d85}'
|
|
| '\u{16ebb}'..='\u{16ed3}' => ch,
|
|
// Full uppercase expands these Greek letters; simple uppercase uses
|
|
// the corresponding single precomposed capital instead.
|
|
'\u{1f80}'..='\u{1f87}' | '\u{1f90}'..='\u{1f97}' | '\u{1fa0}'..='\u{1fa7}' => {
|
|
char::from_u32(ch as u32 + 8).expect("Greek capital")
|
|
}
|
|
'\u{1fb3}' => '\u{1fbc}',
|
|
'\u{1fc3}' => '\u{1fcc}',
|
|
'\u{1ff3}' => '\u{1ffc}',
|
|
_ => {
|
|
let mut mapped = ch.to_uppercase();
|
|
let first = mapped.next().expect("uppercase character");
|
|
if mapped.next().is_none() { first } else { ch }
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn normalize(value: &str) -> String {
|
|
let mut value = lowercase(value);
|
|
for suffix in [
|
|
" (remastered)",
|
|
" (remaster)",
|
|
" - remastered",
|
|
" - remaster",
|
|
" (deluxe)",
|
|
" (deluxe edition)",
|
|
" - deluxe",
|
|
" - deluxe edition",
|
|
" (explicit)",
|
|
" (clean)",
|
|
" [explicit]",
|
|
" [clean]",
|
|
" (album version)",
|
|
" (single version)",
|
|
" (radio edit)",
|
|
" (feat.",
|
|
" (ft.",
|
|
" feat.",
|
|
" ft.",
|
|
] {
|
|
if let Some(index) = value.find(suffix) {
|
|
value.truncate(index);
|
|
}
|
|
}
|
|
let filtered: String = value
|
|
.chars()
|
|
.filter(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || *ch == ' ')
|
|
.collect();
|
|
filtered.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
pub fn compare_strings(
|
|
first: &str,
|
|
second: &str,
|
|
check: &dyn Fn() -> Result<(), String>,
|
|
) -> Result<f64, String> {
|
|
check()?;
|
|
let first = lowercase(first.trim());
|
|
let second = lowercase(second.trim());
|
|
if first == second {
|
|
return Ok(1.0);
|
|
}
|
|
let maximum = first.len().max(second.len());
|
|
if first.is_empty() || second.is_empty() {
|
|
return Ok(0.0);
|
|
}
|
|
let mut first = first.as_bytes();
|
|
let mut second = second.as_bytes();
|
|
let prefix = first.iter().zip(second).take_while(|(a, b)| a == b).count();
|
|
first = &first[prefix..];
|
|
second = &second[prefix..];
|
|
let suffix = first
|
|
.iter()
|
|
.rev()
|
|
.zip(second.iter().rev())
|
|
.take_while(|(a, b)| a == b)
|
|
.count();
|
|
first = &first[..first.len() - suffix];
|
|
second = &second[..second.len() - suffix];
|
|
if first.len() < second.len() {
|
|
std::mem::swap(&mut first, &mut second);
|
|
}
|
|
let mut row: Vec<_> = (0..=second.len()).collect();
|
|
for (i, a) in first.iter().enumerate() {
|
|
check()?;
|
|
let mut diagonal = row[0];
|
|
row[0] = i + 1;
|
|
for (j, b) in second.iter().enumerate() {
|
|
if j % 4096 == 0 {
|
|
check()?;
|
|
}
|
|
let old = row[j + 1];
|
|
row[j + 1] = (old + 1)
|
|
.min(row[j] + 1)
|
|
.min(diagonal + usize::from(a != b));
|
|
diagonal = old;
|
|
}
|
|
}
|
|
Ok(1.0 - row[second.len()] as f64 / maximum as f64)
|
|
}
|
|
|
|
/// Go's `int` is native-width and arithmetic wraps, including abs(MIN).
|
|
pub fn compare_duration(first: i64, second: i64, tolerance: i64) -> bool {
|
|
let difference = (first as isize)
|
|
.wrapping_sub(second as isize)
|
|
.wrapping_abs();
|
|
difference <= tolerance as isize
|
|
}
|