mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +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.
160 lines
5.7 KiB
Python
160 lines
5.7 KiB
Python
#!/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())
|