hisi-rf-link 0.1.0-alpha.26

Host-side linker, archive normalization, and ABI inventory for HiSilicon radio blobs
Documentation
use serde::Deserialize;
use std::{env, fs, path::PathBuf, process::Command};

mod task_profile;

#[derive(Deserialize)]
struct Profile {
    wifi_archives: Vec<WifiArchive>,
    wpa_archives: Vec<WpaArchive>,
}

#[derive(Deserialize)]
struct WifiArchive {
    name: String,
    transform_order: u16,
}

#[derive(Deserialize)]
struct WpaArchive {
    source: String,
    relative: String,
    order: u16,
    profiles: Vec<String>,
}

const COMMANDS: &[(&str, &str)] = &[
    ("patch-reloc", include_str!("../python/patch-reloc.py")),
    ("verify-layout", include_str!("../python/verify-layout.py")),
    (
        "generate-rom-patch",
        include_str!("../python/generate-rom-patch.py"),
    ),
    (
        "patch-from-oracle",
        include_str!("../python/patch-from-oracle.py"),
    ),
];

fn usage() -> ! {
    eprintln!(
        "usage: hisi-rf-link <inspect|normalize|verify-normalized|verify-guarded-sites|ble-profile|sle-profile|ble-init-profile|rebuild-native-supplicant|archive-paths|task-profile|{}> [arguments...]",
        COMMANDS
            .iter()
            .map(|(name, _)| *name)
            .collect::<Vec<_>>()
            .join("|")
    );
    std::process::exit(2);
}

fn required_option(arguments: &mut Vec<std::ffi::OsString>, name: &str) -> std::ffi::OsString {
    let position = arguments
        .iter()
        .position(|argument| argument == name)
        .unwrap_or_else(|| usage());
    if position + 1 >= arguments.len() {
        usage();
    }
    arguments.remove(position);
    arguments.remove(position)
}

fn normalize(args: impl Iterator<Item = std::ffi::OsString>) {
    let mut arguments = args.collect::<Vec<_>>();
    let profile_revision = required_option(&mut arguments, "--profile-revision")
        .into_string()
        .unwrap_or_else(|_| usage());
    let output_directory = PathBuf::from(required_option(&mut arguments, "--out-dir"));
    let manifest_path = PathBuf::from(required_option(&mut arguments, "--manifest"));
    if arguments.is_empty() {
        usage();
    }
    let mut artifacts = Vec::new();
    for argument in arguments {
        let input = PathBuf::from(argument);
        let file_name = input.file_name().unwrap_or_else(|| usage());
        let output = output_directory.join(file_name);
        let artifact =
            hisi_rf_link::normalize::normalize_archive(&input, &output).unwrap_or_else(|error| {
                eprintln!("normalize {}: {error}", input.display());
                std::process::exit(1);
            });
        println!(
            "{}: R58={} R59={} R59-section RELAX={} R61={} -> {}",
            artifact.archive,
            artifact.transformations.llui48_to_riscv32,
            artifact.transformations.branchi_same_section_encoded,
            artifact
                .transformations
                .relax_markers_removed_from_branchi_sections,
            artifact.transformations.llui_rep_markers_removed,
            output.display()
        );
        artifacts.push(artifact);
    }
    let manifest = hisi_rf_link::normalize::NormalizationManifest {
        schema_version: 1,
        normalizer: format!("hisi-rf-link {}", env!("CARGO_PKG_VERSION")),
        profile_revision,
        artifacts,
    };
    if let Some(parent) = manifest_path.parent() {
        fs::create_dir_all(parent).unwrap_or_else(|error| {
            panic!("create manifest directory {}: {error}", parent.display())
        });
    }
    fs::write(
        &manifest_path,
        serde_json::to_vec_pretty(&manifest).expect("serialize normalization manifest"),
    )
    .unwrap_or_else(|error| panic!("write {}: {error}", manifest_path.display()));
}

fn verify_normalized(args: impl Iterator<Item = std::ffi::OsString>) {
    let mut arguments = args.collect::<Vec<_>>();
    let manifest_path = PathBuf::from(required_option(&mut arguments, "--manifest"));
    let archive_directory = PathBuf::from(required_option(&mut arguments, "--archive-dir"));
    if !arguments.is_empty() {
        usage();
    }
    let manifest: hisi_rf_link::normalize::NormalizationManifest = serde_json::from_slice(
        &fs::read(&manifest_path)
            .unwrap_or_else(|error| panic!("read {}: {error}", manifest_path.display())),
    )
    .unwrap_or_else(|error| panic!("parse {}: {error}", manifest_path.display()));
    if manifest.schema_version != 1 {
        eprintln!(
            "unsupported normalization manifest schema {}",
            manifest.schema_version
        );
        std::process::exit(1);
    }
    for artifact in &manifest.artifacts {
        let path = archive_directory.join(&artifact.archive);
        hisi_rf_link::normalize::verify_normalized_archive(&path, artifact).unwrap_or_else(
            |error| {
                eprintln!("verify {}: {error}", path.display());
                std::process::exit(1);
            },
        );
        println!("verified {}", path.display());
    }
}

fn verify_guarded_sites(args: impl Iterator<Item = std::ffi::OsString>) {
    let mut arguments = args.collect::<Vec<_>>();
    let manifest = PathBuf::from(required_option(&mut arguments, "--manifest"));
    let final_elf = PathBuf::from(required_option(&mut arguments, "--final-elf"));
    let archive_directory = PathBuf::from(required_option(&mut arguments, "--archive-dir"));
    if !arguments.is_empty() {
        usage();
    }
    let parity =
        hisi_rf_link::normalize::verify_guarded_sites(&manifest, &final_elf, &archive_directory)
            .unwrap_or_else(|error| {
                eprintln!("verify guarded sites: {error}");
                std::process::exit(1);
            });
    println!(
        "verified normalized bytes against guarded lane: R58 exact={}, merged-equivalent={}, legacy-merged-string-corrections={}; R59 exact={}",
        parity.llui48_exact,
        parity.llui48_merged_string_equivalent,
        parity.llui48_legacy_merged_string_corrections,
        parity.branchi_exact,
    );
}

fn inspect(mut args: impl Iterator<Item = std::ffi::OsString>) {
    let mut arguments = args.by_ref().collect::<Vec<_>>();
    let summary = arguments
        .first()
        .is_some_and(|argument| argument == "--summary");
    if summary {
        arguments.remove(0);
    }
    let mut inventories = Vec::new();
    for argument in arguments {
        let path = PathBuf::from(argument);
        let inventory = hisi_rf_link::normalize::inspect_archive(&path).unwrap_or_else(|error| {
            eprintln!("inspect {}: {error}", path.display());
            std::process::exit(1);
        });
        inventories.push(inventory);
    }
    if inventories.is_empty() {
        usage();
    }
    if summary {
        serde_json::to_writer_pretty(
            std::io::stdout(),
            &hisi_rf_link::normalize::summarize(&inventories),
        )
        .expect("write relocation summary");
    } else {
        serde_json::to_writer_pretty(std::io::stdout(), &inventories)
            .expect("write relocation inventory");
    }
    println!();
}

fn archive_profile(args: impl Iterator<Item = std::ffi::OsString>, label: &str) {
    let mut arguments = args.collect::<Vec<_>>();
    let profile = PathBuf::from(required_option(&mut arguments, "--profile"));
    let archive_root = PathBuf::from(required_option(&mut arguments, "--archive-root"));
    let rom_symbols = PathBuf::from(required_option(&mut arguments, "--rom-symbols"));
    let output = PathBuf::from(required_option(&mut arguments, "--output"));
    let check = if let Some(position) = arguments.iter().position(|argument| argument == "--check")
    {
        arguments.remove(position);
        true
    } else {
        false
    };
    if !arguments.is_empty() {
        usage();
    }
    hisi_rf_link::ble_profile::write_or_check(
        &profile,
        &archive_root,
        &rom_symbols,
        &output,
        check,
    )
    .unwrap_or_else(|error| {
        eprintln!("{label}: {error}");
        std::process::exit(1);
    });
}

fn ble_init_profile(args: impl Iterator<Item = std::ffi::OsString>) {
    let mut arguments = args.collect::<Vec<_>>();
    let profile = PathBuf::from(required_option(&mut arguments, "--profile"));
    let b0_profile = PathBuf::from(required_option(&mut arguments, "--b0-profile"));
    let archive_root = PathBuf::from(required_option(&mut arguments, "--archive-root"));
    let rom_symbols = PathBuf::from(required_option(&mut arguments, "--rom-symbols"));
    let rom_callbacks = PathBuf::from(required_option(&mut arguments, "--rom-callbacks"));
    let output = PathBuf::from(required_option(&mut arguments, "--output"));
    let check = if let Some(position) = arguments.iter().position(|argument| argument == "--check")
    {
        arguments.remove(position);
        true
    } else {
        false
    };
    if !arguments.is_empty() {
        usage();
    }
    hisi_rf_link::ble_init_profile::write_or_check(
        &profile,
        &b0_profile,
        &archive_root,
        &rom_symbols,
        &rom_callbacks,
        &output,
        check,
    )
    .unwrap_or_else(|error| {
        eprintln!("BLE B1 init profile: {error}");
        std::process::exit(1);
    });
}

fn print_path(path: PathBuf) {
    if !path.is_file() {
        eprintln!("radio archive is missing: {}", path.display());
        std::process::exit(2);
    }
    println!("{}", path.display());
}

fn archive_paths(mut args: impl Iterator<Item = std::ffi::OsString>) {
    let profile: Profile =
        toml::from_str(hisi_rf_link::WS63_ARCHIVE_PROFILE).expect("parse embedded WS63 profile");
    match args
        .next()
        .and_then(|arg| arg.into_string().ok())
        .as_deref()
    {
        Some("wifi") => {
            let root = args.next().map(PathBuf::from).unwrap_or_else(|| usage());
            if args.next().is_some() {
                usage();
            }
            let mut archives = profile.wifi_archives;
            archives.sort_by_key(|archive| archive.transform_order);
            for archive in archives {
                print_path(root.join("lib").join(format!("lib{}.a", archive.name)));
            }
        }
        Some("wpa") => {
            let sdk = args.next().map(PathBuf::from).unwrap_or_else(|| usage());
            let override_archive = args.next().map(PathBuf::from).unwrap_or_else(|| usage());
            let selected_profile = args
                .next()
                .and_then(|argument| argument.into_string().ok())
                .unwrap_or_else(|| "wpa2-personal".to_owned());
            if args.next().is_some() {
                usage();
            }
            let mut archives = profile.wpa_archives;
            archives.sort_by_key(|archive| archive.order);
            let mut selected = 0;
            for archive in archives.into_iter().filter(|archive| {
                archive
                    .profiles
                    .iter()
                    .any(|name| name == &selected_profile)
            }) {
                selected += 1;
                match archive.source.as_str() {
                    "override" => print_path(override_archive.clone()),
                    "sdk" => print_path(sdk.join(archive.relative)),
                    source => panic!("unsupported archive source {source}"),
                }
            }
            if selected == 0 {
                eprintln!("unsupported WS63 WPA archive profile: {selected_profile}");
                std::process::exit(2);
            }
        }
        _ => usage(),
    }
}

fn main() {
    let mut args = env::args_os();
    let _program = args.next();
    let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
        usage();
    };
    if command == "archive-paths" {
        archive_paths(args);
        return;
    }
    if command == "inspect" {
        inspect(args);
        return;
    }
    if command == "normalize" {
        normalize(args);
        return;
    }
    if command == "verify-normalized" {
        verify_normalized(args);
        return;
    }
    if command == "ble-profile" {
        archive_profile(args, "BLE archive profile");
        return;
    }
    if command == "sle-profile" {
        archive_profile(args, "SLE archive profile");
        return;
    }
    if command == "ble-init-profile" {
        ble_init_profile(args);
        return;
    }
    if command == "verify-guarded-sites" {
        verify_guarded_sites(args);
        return;
    }
    if command == "rebuild-native-supplicant" {
        hisi_rf_link::native_supplicant::run(args).unwrap_or_else(|error| {
            eprintln!("rebuild native supplicant: {error}");
            std::process::exit(1);
        });
        return;
    }
    if command == "task-profile" {
        task_profile::run(args);
        return;
    }
    let Some((_, script)) = COMMANDS.iter().find(|(name, _)| *name == command) else {
        usage();
    };

    let directory = tempfile::Builder::new()
        .prefix("hisi-rf-link-")
        .tempdir()
        .expect("create temporary script directory");
    let path = directory.path().join(format!("{command}.py"));
    fs::write(&path, script).expect("write embedded post-link tool");

    let status = Command::new("uv")
        .args(["run", "--script"])
        .arg(path)
        .args(args)
        .status()
        .expect("execute legacy post-link oracle through uv");
    if !status.success() {
        std::process::exit(status.code().unwrap_or(1));
    }
}