rsmp4decrypt 0.2.0

Rust bindings and a CLI for Bento4 mp4decrypt
use std::{
    env, fs,
    path::{Path, PathBuf},
    process::Command,
};

fn collect_cpp_files(dir: &Path) -> Vec<PathBuf> {
    let mut files = fs::read_dir(dir)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", dir.display()))
        .filter_map(|entry| {
            let path = entry.ok()?.path();
            (path.extension().and_then(|ext| ext.to_str()) == Some("cpp")).then_some(path)
        })
        .collect::<Vec<_>>();

    files.sort();
    files
}

fn bento4_include_dirs(source_root: &Path) -> [PathBuf; 4] {
    [
        source_root.join("Core"),
        source_root.join("Codecs"),
        source_root.join("Crypto"),
        source_root.join("MetaData"),
    ]
}

fn add_bento4_sources(build: &mut cc::Build, source_root: &Path) {
    for component in ["Codecs", "Core", "Crypto", "MetaData"] {
        for file in collect_cpp_files(&source_root.join(component)) {
            build.file(file);
        }
    }

    build.file(
        source_root
            .join("System")
            .join("StdC")
            .join("Ap4StdCFileByteStream.cpp"),
    );

    let random_source = if env::var("CARGO_CFG_TARGET_OS")
        .expect("CARGO_CFG_TARGET_OS env variable not set")
        == "windows"
    {
        source_root
            .join("System")
            .join("Win32")
            .join("Ap4Win32Random.cpp")
    } else {
        source_root
            .join("System")
            .join("Posix")
            .join("Ap4PosixRandom.cpp")
    };
    build.file(random_source);
}

fn add_include_flags(command: &mut Command, compiler: &cc::Tool, include_dirs: &[PathBuf]) {
    for dir in include_dirs {
        if compiler.is_like_msvc() {
            command.arg(format!("/I{}", dir.display()));
        } else {
            command.arg("-I").arg(dir);
        }
    }
}

fn compiled_library_path(out_dir: &Path) -> PathBuf {
    if env::var("CARGO_CFG_TARGET_ENV").expect("CARGO_CFG_TARGET_ENV env variable not set")
        == "msvc"
    {
        out_dir.join("rsmp4decrypt.lib")
    } else {
        out_dir.join("librsmp4decrypt.a")
    }
}

fn worker_binary_name() -> &'static str {
    if env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS env variable not set")
        == "windows"
    {
        "rsmp4decrypt-worker.exe"
    } else {
        "rsmp4decrypt-worker"
    }
}

fn compile_worker_executable(manifest_dir: &Path, source_root: &Path, out_dir: &Path) {
    let include_dirs = bento4_include_dirs(source_root);
    let native_dir = manifest_dir.join("src").join("native");
    let library_path = compiled_library_path(out_dir);
    let worker_source = native_dir.join("rsmp4decrypt_worker.cpp");
    let worker_output = out_dir.join(worker_binary_name());
    let worker_object = if env::var("CARGO_CFG_TARGET_ENV")
        .expect("CARGO_CFG_TARGET_ENV env variable not set")
        == "msvc"
    {
        out_dir.join("rsmp4decrypt_worker.obj")
    } else {
        out_dir.join("rsmp4decrypt_worker.o")
    };

    if !library_path.exists() {
        panic!(
            "native library archive is missing. Expected {}",
            library_path.display()
        );
    }

    let mut build = cc::Build::new();
    build
        .cpp(true)
        .warnings(false)
        .extra_warnings(false)
        .includes(&include_dirs)
        .include(&native_dir);

    let compiler = build.get_compiler();
    let mut command = compiler.to_command();
    command.current_dir(out_dir);
    add_include_flags(&mut command, &compiler, &include_dirs);
    if compiler.is_like_msvc() {
        command.arg(format!("/I{}", native_dir.display()));
        command.arg(format!("/Fo:{}", worker_object.display()));
        command.arg(&worker_source);
        command.arg(&library_path);
        command.arg(format!("/Fe:{}", worker_output.display()));
    } else {
        command.arg("-I").arg(&native_dir);
        command.arg(&worker_source);
        command.arg(&library_path);
        command.arg("-o").arg(&worker_output);
    }

    let status = command
        .status()
        .unwrap_or_else(|error| panic!("failed to invoke {}: {error}", compiler.path().display()));
    if !status.success() {
        panic!(
            "failed to build native worker executable with {}",
            compiler.path().display()
        );
    }

    println!(
        "cargo:rustc-env=RSMP4DECRYPT_WORKER_EMBED={}",
        worker_output.display()
    );
}

fn main() {
    let manifest_dir = PathBuf::from(
        env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env variable not set"),
    );
    let bento4_root = manifest_dir.join("vendor").join("bento4-src");
    let source_root = bento4_root.join("Source").join("C++");
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR env variable not set"));

    if !source_root.join("Core").join("Ap4.h").exists() {
        panic!(
            "Bento4 source snapshot is missing. Expected sources under {}",
            manifest_dir.join("vendor").join("bento4-src").display()
        );
    }

    println!("cargo:rerun-if-changed={}", bento4_root.display());
    println!("cargo:rerun-if-changed=src/native/rsmp4decrypt.cpp");
    println!("cargo:rerun-if-changed=src/native/rsmp4decrypt.h");
    println!("cargo:rerun-if-changed=src/native/rsmp4decrypt_worker.cpp");

    let include_dirs = bento4_include_dirs(&source_root);
    let mut build = cc::Build::new();
    build
        .cpp(true)
        .warnings(false)
        .extra_warnings(false)
        .includes(&include_dirs);

    add_bento4_sources(&mut build, &source_root);

    build.file(
        manifest_dir
            .join("src")
            .join("native")
            .join("rsmp4decrypt.cpp"),
    );
    build.compile("rsmp4decrypt");

    compile_worker_executable(&manifest_dir, &source_root, &out_dir);
}