corteq-onepassword 0.1.5

Secure 1Password SDK wrapper with FFI bindings for Rust applications
Documentation
//! Build script for corteq-onepassword.
//!
//! This script handles downloading and extracting the 1Password SDK native library
//! from PyPI at build time. SHA256 checksums are fetched dynamically from PyPI's
//! JSON API to ensure integrity verification.

use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// 1Password SDK version to download.
const SDK_VERSION: &str = "0.3.2";

/// Platform-specific configuration for library download.
struct PlatformConfig {
    /// Platform tag fragment to match in wheel filenames (e.g., "manylinux_2_32_x86_64").
    platform_tag: &'static str,
    /// Output library filename.
    lib_name: &'static str,
    /// Directory name for bundled libraries (e.g., "linux-x86_64").
    bundled_dir: &'static str,
}

/// Information about a wheel file from PyPI.
struct WheelInfo {
    /// Download URL for the wheel.
    url: String,
    /// SHA256 checksum from PyPI.
    sha256: String,
}

/// Get platform configuration for the target.
fn get_platform_config(target_os: &str, target_arch: &str) -> Option<PlatformConfig> {
    match (target_os, target_arch) {
        ("linux", "x86_64") => Some(PlatformConfig {
            platform_tag: "manylinux_2_32_x86_64",
            lib_name: "libop_uniffi_core.so",
            bundled_dir: "linux-x86_64",
        }),
        ("linux", "aarch64") => Some(PlatformConfig {
            platform_tag: "manylinux_2_32_aarch64",
            lib_name: "libop_uniffi_core.so",
            bundled_dir: "linux-aarch64",
        }),
        ("macos", "x86_64") => Some(PlatformConfig {
            platform_tag: "macosx_10_9_x86_64",
            lib_name: "libop_uniffi_core.dylib",
            bundled_dir: "macos-x86_64",
        }),
        ("macos", "aarch64") => Some(PlatformConfig {
            platform_tag: "macosx_11_0_arm64",
            lib_name: "libop_uniffi_core.dylib",
            bundled_dir: "macos-aarch64",
        }),
        _ => None,
    }
}

/// Find bundled library in src/libs/{platform}/.
///
/// Returns the path to the library if it exists, None otherwise.
fn find_bundled_library(config: &PlatformConfig) -> Option<PathBuf> {
    // Get the manifest directory (where Cargo.toml is)
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").ok()?;
    let bundled_path = PathBuf::from(manifest_dir)
        .join("src")
        .join("libs")
        .join(config.bundled_dir)
        .join(config.lib_name);

    if bundled_path.exists() {
        Some(bundled_path)
    } else {
        None
    }
}

fn main() {
    // Emit rerun directives
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=ONEPASSWORD_LIB_PATH");
    println!("cargo:rerun-if-env-changed=ONEPASSWORD_SKIP_DOWNLOAD");
    println!("cargo:rerun-if-env-changed=ONEPASSWORD_ALLOW_SYSTEM_LIB");
    println!("cargo:rerun-if-env-changed=DOCS_RS");

    // Detect docs.rs environment - skip library handling since it's not needed for docs
    if env::var("DOCS_RS").is_ok() {
        println!("cargo:warning=Building for docs.rs - skipping library setup");
        return;
    }

    // Get target platform info
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_else(|_| "unknown".to_string());
    let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| "unknown".to_string());

    println!("cargo:warning=Building for {target_os}-{target_arch}");

    // Check for custom library path override
    if let Ok(custom_path) = env::var("ONEPASSWORD_LIB_PATH") {
        let path = PathBuf::from(&custom_path);
        if path.exists() {
            let path_display = path.display();
            println!("cargo:warning=Using custom library path: {path_display}");
            if let Some(parent) = path.parent() {
                let parent_display = parent.display();
                println!("cargo:rustc-link-search=native={parent_display}");
            }
            return;
        } else {
            panic!("ONEPASSWORD_LIB_PATH points to non-existent file: {custom_path}");
        }
    }

    // Get platform configuration
    let config = get_platform_config(&target_os, &target_arch).unwrap_or_else(|| {
        panic!(
            "Unsupported platform: {target_os}-{target_arch}. \
             Supported platforms: linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64"
        );
    });

    // Check for bundled library in src/libs/{platform}/
    if let Some(bundled_path) = find_bundled_library(&config) {
        let bundled_display = bundled_path.display();
        println!("cargo:warning=Using bundled library: {bundled_display}");
        if let Some(parent) = bundled_path.parent() {
            let parent_display = parent.display();
            println!("cargo:rustc-link-search=native={parent_display}");
        }
        return;
    }

    // Check if download should be skipped (for offline builds with pre-cached library)
    if env::var("ONEPASSWORD_SKIP_DOWNLOAD").is_ok() {
        println!("cargo:warning=Skipping library download (ONEPASSWORD_SKIP_DOWNLOAD set)");
        return;
    }

    // Determine output directory
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let lib_dir = out_dir.join("lib");
    fs::create_dir_all(&lib_dir).expect("Failed to create lib directory");

    let lib_path = lib_dir.join(config.lib_name);

    // Check if library already exists
    if lib_path.exists() {
        let lib_path_display = lib_path.display();
        let lib_dir_display = lib_dir.display();
        println!("cargo:warning=Library already exists: {lib_path_display}");
        println!("cargo:rustc-link-search=native={lib_dir_display}");
        return;
    }

    // Download and extract the library
    println!("cargo:warning=Downloading 1Password SDK library...");

    match download_and_extract(&config, &lib_dir) {
        Ok(_) => {
            let lib_path_display = lib_path.display();
            let lib_dir_display = lib_dir.display();
            println!("cargo:warning=Library extracted to: {lib_path_display}");
            println!("cargo:rustc-link-search=native={lib_dir_display}");
        }
        Err(e) => {
            panic!("Failed to download 1Password SDK library: {e}");
        }
    }
}

/// Download the wheel from PyPI and extract the native library.
fn download_and_extract(config: &PlatformConfig, lib_dir: &Path) -> Result<(), String> {
    // Get wheel info (URL and SHA256) from PyPI
    let wheel_info = get_wheel_info(config)?;
    println!("cargo:warning=Downloading from: {}", wheel_info.url);
    println!("cargo:warning=Expected SHA256: {}", wheel_info.sha256);

    // Download the wheel
    let wheel_data = download_file(&wheel_info.url)?;

    // Verify SHA256 checksum from PyPI
    verify_checksum(&wheel_data, &wheel_info.sha256)?;

    // Extract the library from the wheel (which is a ZIP file)
    extract_library_from_wheel(&wheel_data, lib_dir, config.lib_name)?;

    Ok(())
}

/// Get wheel download URL and SHA256 checksum from PyPI JSON API.
fn get_wheel_info(config: &PlatformConfig) -> Result<WheelInfo, String> {
    let api_url = format!("https://pypi.org/pypi/onepassword-sdk/{SDK_VERSION}/json");

    let response = reqwest::blocking::get(&api_url)
        .map_err(|e| format!("Failed to fetch PyPI metadata: {e}"))?;

    if !response.status().is_success() {
        let status = response.status();
        return Err(format!("PyPI API returned status: {status}"));
    }

    let json: serde_json::Value = response
        .json()
        .map_err(|e| format!("Failed to parse PyPI response: {e}"))?;

    // Find the wheel matching our platform
    let urls = json["urls"].as_array().ok_or("No urls in PyPI response")?;

    // Look for a wheel matching our platform tag
    for url_info in urls {
        let filename = url_info["filename"].as_str().unwrap_or("");

        // Check if this wheel matches our platform
        if filename.contains(config.platform_tag) && filename.ends_with(".whl") {
            let url = url_info["url"]
                .as_str()
                .ok_or("No url field in wheel info")?
                .to_string();

            let sha256 = url_info["digests"]["sha256"]
                .as_str()
                .ok_or("No sha256 digest in wheel info")?
                .to_string();

            println!("cargo:warning=Found wheel: {filename}");

            return Ok(WheelInfo { url, sha256 });
        }
    }

    Err(format!(
        "Could not find wheel for platform '{}' in SDK version {SDK_VERSION}",
        config.platform_tag
    ))
}

/// Download a file from a URL.
fn download_file(url: &str) -> Result<Vec<u8>, String> {
    let response = reqwest::blocking::get(url).map_err(|e| format!("Download failed: {e}"))?;

    if !response.status().is_success() {
        let status = response.status();
        return Err(format!("Download returned status: {status}"));
    }

    response
        .bytes()
        .map(|b| b.to_vec())
        .map_err(|e| format!("Failed to read response: {e}"))
}

/// Calculate SHA256 checksum of data.
fn calculate_sha256(data: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(data);
    hex::encode(hasher.finalize())
}

/// Verify SHA256 checksum.
fn verify_checksum(data: &[u8], expected: &str) -> Result<(), String> {
    let actual = calculate_sha256(data);

    if actual != expected.to_lowercase() {
        return Err(format!(
            "SHA256 checksum mismatch!\nExpected: {expected}\nActual:   {actual}\n\
             The downloaded file may be corrupted or tampered with."
        ));
    }

    println!("cargo:warning=SHA256 checksum verified");
    Ok(())
}

/// Extract the native library from a wheel (ZIP) file.
fn extract_library_from_wheel(
    wheel_data: &[u8],
    output_dir: &Path,
    lib_name: &str,
) -> Result<(), String> {
    use std::io::Cursor;
    use zip::ZipArchive;

    let reader = Cursor::new(wheel_data);
    let mut archive =
        ZipArchive::new(reader).map_err(|e| format!("Failed to open wheel as ZIP: {e}"))?;

    // Collect all files for debugging
    let mut all_files = Vec::new();

    // Find and extract the library by searching for the filename
    for i in 0..archive.len() {
        let mut file = archive
            .by_index(i)
            .map_err(|e| format!("Failed to read ZIP entry: {e}"))?;

        let name = file.name().to_string();
        all_files.push(name.clone());

        // Check if this file ends with our library name
        if name.ends_with(lib_name) {
            let output_path = output_dir.join(lib_name);

            let mut output_file = fs::File::create(&output_path)
                .map_err(|e| format!("Failed to create output file: {e}"))?;

            std::io::copy(&mut file, &mut output_file)
                .map_err(|e| format!("Failed to write library: {e}"))?;

            // Make the library executable on Unix
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let mut perms = fs::metadata(&output_path)
                    .map_err(|e| format!("Failed to get file permissions: {e}"))?
                    .permissions();
                perms.set_mode(0o755);
                fs::set_permissions(&output_path, perms)
                    .map_err(|e| format!("Failed to set permissions: {e}"))?;
            }

            let output_display = output_path.display();
            println!("cargo:warning=Extracted library from {name} to {output_display}");
            return Ok(());
        }
    }

    let files_list = all_files.join("\n");
    Err(format!(
        "Library '{lib_name}' not found in wheel. Available files:\n{files_list}"
    ))
}