//! Build extensible language packs and pinned, authenticated Swagger UI assets. use serde_json::{json, Value}; use std::{collections::HashSet, env, fs, path::PathBuf, process::Command}; // Prepare the actual embedded bytes here, not in a dependency's earlier build. // Cargo config discovery is deliberately irrelevant to the pinned default. fn swagger_assets( root: &std::path::Path, out: &std::path::Path, ) -> Result<(), Box> { for name in [ "SWAGGER_UI_DOWNLOAD_URL", "SWAGGER_UI_ARCHIVE_SHA256", "RUST_PROXY_PYTHON", "CARGO_NET_OFFLINE", ] { println!("cargo:rerun-if-env-changed={name}"); } let helper = root.join("scripts/swagger_assets.py"); println!("cargo:rerun-if-changed={}", helper.display()); let interpreters = match env::var_os("RUST_PROXY_PYTHON") { Some(value) if !value.is_empty() => vec![value], _ => vec!["python3".into(), "python".into()], }; for interpreter in interpreters { match Command::new(&interpreter).arg(&helper).arg("--out-dir").arg(out).status() { Ok(status) if status.success() => return Ok(()), Ok(_) => return Err("Swagger UI asset preparation failed; see the diagnostic above and docs/API_ACCESS.md".into()), Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, Err(error) => return Err(error.into()), } } Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into()) } fn main() -> Result<(), Box> { let root = PathBuf::from( env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?, ); let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?); swagger_assets(&root, &out)?; let langs = root.join("langs"); println!("cargo:rerun-if-changed={}", langs.display()); let mut paths: Vec<_> = fs::read_dir(&langs) .expect("langs directory is required") .map(|entry| entry.expect("read language entry").path()) .filter(|path| path.extension().is_some_and(|ext| ext == "json")) .collect(); paths.sort(); assert!( !paths.is_empty() && paths.len() <= 128, "expected 1..128 language packs" ); let mut languages = Vec::new(); let mut locales = HashSet::new(); for path in paths { println!("cargo:rerun-if-changed={}", path.display()); let id = path .file_stem() .unwrap() .to_str() .expect("language filename must be UTF-8"); assert!( id.len() <= 64 && id .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'), "invalid language filename: {}", path.display() ); let content = fs::read(&path).expect("read language JSON"); assert!(content.len() <= 262_144, "language pack exceeds 256 KiB"); let pack: Value = serde_json::from_slice(&content).unwrap_or_else(|e| panic!("{}: {e}", path.display())); let meta = pack["meta"] .as_object() .expect("language meta object is required"); for key in ["locale", "name", "native_name", "direction"] { assert!( meta.get(key) .and_then(Value::as_str) .is_some_and(|v| !v.is_empty() && v.len() <= 128), "{id}: missing/invalid {key}" ); } assert!( matches!(meta["direction"].as_str(), Some("ltr" | "rtl")), "{id}: invalid direction" ); let locale = meta["locale"].as_str().unwrap(); assert!( locale.split('-').all(|part| !part.is_empty() && part.len() <= 8 && part.bytes().all(|b| b.is_ascii_alphanumeric())), "{id}: use a BCP-47 style locale" ); assert!( locales.insert(locale.to_ascii_lowercase()), "duplicate locale {locale}" ); let translations = pack["translations"] .as_object() .expect("translations object is required"); assert!( translations.len() <= 4096 && translations.iter().all(|(k, v)| !k.is_empty() && k.len() <= 128 && v.as_str().is_some_and(|s| s.len() <= 16_384)), "{id}: translations must be bounded strings" ); languages.push(json!({"id":id,"meta":meta,"translations":translations})); } assert!( languages.iter().any(|p| p["id"] == "en_US"), "en_US.json is the required fallback pack" ); let login_path = root.join("src/admin/web/login-keys.json"); println!("cargo:rerun-if-changed={}", login_path.display()); let login_keys: HashSet = serde_json::from_slice(&fs::read(login_path).expect("read public translation keys")) .expect("parse public translation keys"); let mut login_languages = languages.clone(); for language in &mut login_languages { language["translations"] .as_object_mut() .expect("translations object") .retain(|key, _| login_keys.contains(key)); } let login_bundle = serde_json::to_vec(&json!({"default":"en_US","languages":login_languages})).unwrap(); fs::write(out.join("login-langs.json"), login_bundle).expect("write login translations"); let bundle = serde_json::to_vec(&json!({"default":"en_US","languages":languages})).unwrap(); assert!(bundle.len() <= 8_388_608, "language bundle exceeds 8 MiB"); fs::write(out.join("langs.json"), bundle).expect("write language bundle"); Ok(()) }