mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-01 05:39:34 +02:00
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.
This commit is contained in:
1 parent
1da0da18a6
commit
aa99726439
452 files changed
+64417
-65088
No files matched your search
@@ -0,0 +1,227 @@
|
||||
use crate::host::decode_go_utf8;
|
||||
use base64::{
|
||||
Engine, alphabet,
|
||||
engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD},
|
||||
};
|
||||
use rquickjs::{Object, TypedArray, Value};
|
||||
|
||||
pub(crate) fn option_string(
|
||||
options: Option<&Object<'_>>,
|
||||
key: &str,
|
||||
fallback: &str,
|
||||
) -> Result<String, String> {
|
||||
let Some(options) = options else {
|
||||
return Ok(fallback.into());
|
||||
};
|
||||
let value: Value = options.get(key).map_err(|error| error.to_string())?;
|
||||
if let Some(value) = value.as_string() {
|
||||
let value = value.to_string().map_err(|error| error.to_string())?;
|
||||
if !value.trim().is_empty() {
|
||||
return Ok(value.trim().into());
|
||||
}
|
||||
} else if let Ok(value) = TypedArray::<u8>::from_value(value) {
|
||||
let bytes = copy_typed(&value);
|
||||
if !bytes.is_empty() {
|
||||
return Ok(decode_go_utf8(&bytes));
|
||||
}
|
||||
}
|
||||
Ok(fallback.into())
|
||||
}
|
||||
|
||||
pub(crate) fn option_i64(options: &Object<'_>, key: &str, fallback: i64) -> Result<i64, String> {
|
||||
let value: Value = options.get(key).map_err(|error| error.to_string())?;
|
||||
if let Some(number) = value.as_number() {
|
||||
return Ok(go_float_i64(number));
|
||||
}
|
||||
if let Some(value) = value.as_string() {
|
||||
let value = value.to_string().map_err(|error| error.to_string())?;
|
||||
let value = value.trim();
|
||||
let prefix: String = value
|
||||
.chars()
|
||||
.enumerate()
|
||||
.take_while(|(index, ch)| {
|
||||
ch.is_ascii_digit() || (*index == 0 && (*ch == '+' || *ch == '-'))
|
||||
})
|
||||
.map(|(_, ch)| ch)
|
||||
.collect();
|
||||
return Ok(prefix.parse().unwrap_or(fallback));
|
||||
}
|
||||
Ok(fallback)
|
||||
}
|
||||
|
||||
pub(crate) fn option_bool(options: &Object<'_>, key: &str, fallback: bool) -> Result<bool, String> {
|
||||
let value: Value = options.get(key).map_err(|error| error.to_string())?;
|
||||
if let Some(value) = value.as_bool() {
|
||||
return Ok(value);
|
||||
}
|
||||
if let Some(value) = value.as_number() {
|
||||
return Ok(value != 0.0);
|
||||
}
|
||||
if let Some(value) = value.as_string() {
|
||||
return Ok(
|
||||
match value
|
||||
.to_string()
|
||||
.map_err(|error| error.to_string())?
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"1" | "true" | "yes" | "on" => true,
|
||||
"0" | "false" | "no" | "off" => false,
|
||||
_ => fallback,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(fallback)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_string(value: &str, encoding: &str) -> Result<Vec<u8>, String> {
|
||||
match encoding.trim().to_lowercase().as_str() {
|
||||
"" | "utf8" | "utf-8" | "text" => Ok(value.as_bytes().to_vec()),
|
||||
"base64" => {
|
||||
decode_base64(value.trim()).map_err(|error| format!("invalid base64 data: {error}"))
|
||||
}
|
||||
"hex" => {
|
||||
let value = value.trim().as_bytes();
|
||||
let mut bytes = Vec::with_capacity(value.len() / 2);
|
||||
let digit = |byte: u8| {
|
||||
(byte as char)
|
||||
.to_digit(16)
|
||||
.map(|value| value as u8)
|
||||
.ok_or_else(|| format!("invalid hex data: invalid byte 0x{byte:02x}"))
|
||||
};
|
||||
for pair in value.chunks(2) {
|
||||
let first = digit(pair[0])?;
|
||||
if pair.len() < 2 {
|
||||
return Err("invalid hex data: encoding/hex: odd length hex string".into());
|
||||
}
|
||||
bytes.push(first * 16 + digit(pair[1])?);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
_ => Err(format!("unsupported byte encoding: {encoding}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode_base64(value: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
GeneralPurpose::new(
|
||||
&alphabet::STANDARD,
|
||||
GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
|
||||
)
|
||||
.decode(value.replace(['\r', '\n'], ""))
|
||||
}
|
||||
|
||||
pub(crate) fn decode_option_bytes(
|
||||
options: Option<&Object<'_>>,
|
||||
key: &str,
|
||||
encoding: &str,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
if let Some(options) = options {
|
||||
let value: Value = options.get(key).map_err(|error| error.to_string())?;
|
||||
if let Ok(value) = TypedArray::<u8>::from_value(value) {
|
||||
// runtimeOptionString converts []byte to a Go string without UTF-8
|
||||
// replacement or trimming. Preserve arbitrary binary keys and IVs.
|
||||
if matches!(
|
||||
encoding.trim().to_lowercase().as_str(),
|
||||
"" | "utf8" | "utf-8" | "text"
|
||||
) {
|
||||
return Ok(copy_typed(&value));
|
||||
}
|
||||
}
|
||||
}
|
||||
decode_string(&option_string(options, key, "")?, encoding)
|
||||
}
|
||||
|
||||
pub(crate) fn encode(bytes: &[u8], encoding: &str) -> Result<String, String> {
|
||||
match encoding.trim().to_lowercase().as_str() {
|
||||
"" | "base64" => Ok(STANDARD.encode(bytes)),
|
||||
"hex" => {
|
||||
use std::fmt::Write;
|
||||
let mut value = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
let _ = write!(value, "{byte:02x}");
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
"utf8" | "utf-8" | "text" => Ok(decode_go_utf8(bytes)),
|
||||
_ => Err(format!("unsupported byte encoding: {encoding}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode_value(value: Value<'_>, encoding: &str) -> Result<Vec<u8>, String> {
|
||||
if let Some(text) = value.as_string() {
|
||||
return decode_string(
|
||||
&text.to_string().map_err(|error| error.to_string())?,
|
||||
encoding,
|
||||
);
|
||||
}
|
||||
if let Ok(bytes) = TypedArray::<u8>::from_value(value.clone()) {
|
||||
return Ok(copy_typed(&bytes));
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
return array
|
||||
.iter::<Value>()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let value = item.map_err(|error| error.to_string())?;
|
||||
let number = value
|
||||
.as_number()
|
||||
.ok_or_else(|| format!("unsupported byte array item at index {index}"))?;
|
||||
// Goja exports integral values within int64 as int64. Other
|
||||
// numbers use Go's float64 -> native int conversion first.
|
||||
if number.fract() == 0.0
|
||||
&& (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&number)
|
||||
{
|
||||
return Ok(number as i64 as u8);
|
||||
}
|
||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||
return Ok(number as isize as u8);
|
||||
#[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))]
|
||||
Ok(go_float_i64(number) as u8)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Err("unsupported byte payload type".into())
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn copy_typed(bytes: &TypedArray<'_, u8>) -> Vec<u8> {
|
||||
// SAFETY: only this worker can access the VM. Finish the owned copy before
|
||||
// any engine call, getter, callback, or operation that can detach a buffer.
|
||||
unsafe { bytes.as_bytes() }.unwrap_or_default().to_vec()
|
||||
}
|
||||
|
||||
fn go_float_i64(number: f64) -> i64 {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
return number as i64;
|
||||
#[cfg(target_arch = "arm")]
|
||||
{
|
||||
// Go ARM32 uses runtime._d2v for int64 conversions, while byte-array
|
||||
// conversion above uses the hardware's native int32 instruction.
|
||||
let bits = number.to_bits();
|
||||
let shift = ((bits >> 52) & 0x7ff) as i32 - 1075;
|
||||
let mantissa = (bits & ((1_u64 << 52) - 1)) | (1_u64 << 52);
|
||||
let magnitude = if shift < -63 {
|
||||
0
|
||||
} else if shift < 0 {
|
||||
mantissa >> -shift
|
||||
} else if shift <= 11 {
|
||||
mantissa << shift
|
||||
} else {
|
||||
u64::from(number as u32) << 32
|
||||
};
|
||||
return if number.is_sign_negative() {
|
||||
magnitude.wrapping_neg() as i64
|
||||
} else {
|
||||
magnitude as i64
|
||||
};
|
||||
}
|
||||
#[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))]
|
||||
if number.is_finite()
|
||||
&& (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&number)
|
||||
{
|
||||
number as i64
|
||||
} else {
|
||||
i64::MIN
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user