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:
zarzet committed 2026-09-14 22:58:27 +07:00
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
}
}