134 lines
5.5 KiB
Rust
134 lines
5.5 KiB
Rust
use serde_json::{json, Value};
|
|
use std::{collections::HashSet, env, fs, path::PathBuf, process::Command};
|
|
|
|
fn swagger_assets(
|
|
root: &std::path::Path,
|
|
out: &std::path::Path,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
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/build/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<dyn std::error::Error>> {
|
|
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<String> =
|
|
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(())
|
|
}
|