mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +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,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that a release APK contains the selected native backend."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Sequence, Tuple
|
||||
|
||||
|
||||
ABI_LAYOUT = {
|
||||
"arm64-v8a": (2, 183),
|
||||
"armeabi-v7a": (1, 40),
|
||||
}
|
||||
CORE_LIBRARIES = ("libapp.so", "libflutter.so")
|
||||
|
||||
|
||||
class AuditError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_abis(raw: str) -> Tuple[str, ...]:
|
||||
abis = tuple(part.strip() for part in raw.split(",") if part.strip())
|
||||
if not abis:
|
||||
raise AuditError("--abis must contain at least one ABI")
|
||||
unknown = sorted(set(abis) - set(ABI_LAYOUT))
|
||||
if unknown:
|
||||
raise AuditError("unsupported ABI(s): " + ", ".join(unknown))
|
||||
if len(set(abis)) != len(abis):
|
||||
raise AuditError("--abis contains a duplicate ABI")
|
||||
return abis
|
||||
|
||||
|
||||
def artifact_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while True:
|
||||
chunk = stream.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def required_entry(infos: Sequence[zipfile.ZipInfo], path: str) -> zipfile.ZipInfo:
|
||||
matches = [info for info in infos if info.filename == path and not info.is_dir()]
|
||||
if not matches:
|
||||
raise AuditError("missing APK entry: " + path)
|
||||
if len(matches) > 1:
|
||||
raise AuditError("duplicate APK entry: " + path)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def check_abi_directories(names: Iterable[str], expected: Sequence[str]) -> None:
|
||||
actual = set()
|
||||
for name in names:
|
||||
parts = name.split("/")
|
||||
if len(parts) >= 3 and parts[0] == "lib" and parts[1]:
|
||||
actual.add(parts[1])
|
||||
unexpected = sorted(actual - set(expected))
|
||||
if unexpected:
|
||||
raise AuditError("unexpected lib ABI directory(s): " + ", ".join(unexpected))
|
||||
|
||||
|
||||
def check_elf(data: bytes, path: str, abi: str) -> None:
|
||||
expected_class, expected_machine = ABI_LAYOUT[abi]
|
||||
if len(data) < 20 or data[:4] != b"\x7fELF":
|
||||
raise AuditError(path + ": invalid ELF header")
|
||||
if data[4] != expected_class:
|
||||
raise AuditError(f"{path}: ELF class {data[4]} does not match {abi}")
|
||||
if data[5] != 1:
|
||||
raise AuditError(path + ": Android ARM libraries must be little-endian")
|
||||
machine = struct.unpack_from("<H", data, 18)[0]
|
||||
if machine != expected_machine:
|
||||
raise AuditError(f"{path}: e_machine {machine} does not match {abi}")
|
||||
|
||||
|
||||
def check_backend_markers(
|
||||
zf: zipfile.ZipFile,
|
||||
infos: Sequence[zipfile.ZipInfo],
|
||||
backend: str,
|
||||
) -> None:
|
||||
names = [info.filename for info in infos]
|
||||
if backend == "rust":
|
||||
if any(name.rsplit("/", 1)[-1] == "libgojni.so" for name in names):
|
||||
raise AuditError("Rust APK contains forbidden libgojni.so")
|
||||
go_resource = next(
|
||||
(name for name in names if name.startswith(("gobackend/", "go/"))), None
|
||||
)
|
||||
if go_resource is not None:
|
||||
raise AuditError(f"Rust APK contains forbidden Go resource: {go_resource}")
|
||||
for info in infos:
|
||||
if info.is_dir() or not info.filename.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(info)
|
||||
marker = next(
|
||||
(marker for marker in (b"Lgobackend/", b"Lgo/") if marker in data), None
|
||||
)
|
||||
if marker is not None:
|
||||
raise AuditError(f"Rust APK DEX {info.filename} contains {marker.decode()}")
|
||||
elif any(
|
||||
name.rsplit("/", 1)[-1] == "libspotiflac_mobile.so" for name in names
|
||||
):
|
||||
raise AuditError("Go APK contains forbidden libspotiflac_mobile.so")
|
||||
|
||||
|
||||
def audit(path: Path, backend: str, abis: Sequence[str]) -> str:
|
||||
if not path.is_file():
|
||||
raise AuditError("APK is not a regular file: " + str(path))
|
||||
digest = artifact_sha256(path)
|
||||
try:
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
infos = zf.infolist()
|
||||
names = [info.filename for info in infos]
|
||||
if "assets/flutter_assets/kernel_blob.bin" in names:
|
||||
raise AuditError("debug APK contains assets/flutter_assets/kernel_blob.bin")
|
||||
check_abi_directories(names, abis)
|
||||
check_backend_markers(zf, infos, backend)
|
||||
backend_library = (
|
||||
"libspotiflac_mobile.so" if backend == "rust" else "libgojni.so"
|
||||
)
|
||||
libraries = CORE_LIBRARIES + (backend_library,)
|
||||
if backend == "rust":
|
||||
libraries += ("libjnidispatch.so",)
|
||||
for abi in abis:
|
||||
for library in libraries:
|
||||
entry_path = f"lib/{abi}/{library}"
|
||||
entry = required_entry(infos, entry_path)
|
||||
with zf.open(entry, "r") as stream:
|
||||
check_elf(stream.read(20), entry_path, abi)
|
||||
except zipfile.BadZipFile as exc:
|
||||
raise AuditError("invalid APK/ZIP: " + str(exc)) from exc
|
||||
return digest
|
||||
|
||||
|
||||
def make_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("apk", type=Path, help="release APK to audit")
|
||||
parser.add_argument("--backend", choices=("rust", "go"), required=True)
|
||||
parser.add_argument("--abis", required=True, metavar="ABI[,ABI...]", help="expected APK ABIs")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] = None) -> int:
|
||||
args = make_parser().parse_args(argv)
|
||||
try:
|
||||
abis = parse_abis(args.abis)
|
||||
digest = audit(args.apk, args.backend, abis)
|
||||
except (AuditError, OSError) as exc:
|
||||
print("error: " + str(exc), file=sys.stderr)
|
||||
return 1
|
||||
print(f"OK sha256={digest} backend={args.backend} abis={','.join(abis)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user