use json_dotpath::DotPaths;
use quote::quote;
use serde_json::{Value, json};
use std::path::Path;
use std::string::ToString;
use std::{fs, io};
use thiserror::Error;
use typify::{TypeSpace, TypeSpaceSettings};
#[derive(Error, Debug)]
pub enum BuildError {
#[error("I/O error")]
IoError(#[from] io::Error),
#[error("JSON schema error")]
SchemaError(#[from] typify::Error),
#[error("Rust syntax error")]
SyntaxError(#[from] syn::Error),
#[error("JSON parsing error")]
JsonError(#[from] serde_json::Error),
#[error("other error")]
Other,
}
fn main() -> Result<(), BuildError> {
println!("cargo:rerun-if-changed=build.rs");
let schema_configs = [
(
"assets/csaf_2.0_json_schema.json",
"csaf2_0/schema.generated.rs",
Some(&fix_2_0_schema as &dyn Fn(&mut Value)),
),
(
"assets/csaf_2.1_json_schema.json",
"csaf2_1/schema.generated.rs",
Some(&fix_2_1_schema),
),
(
"assets/decision_point_json_schema.json",
"csaf2_1/ssvc_dp.generated.rs",
None,
),
(
"assets/decision_point_selection_list_json_schema.json",
"csaf2_1/ssvc_dp_selection_list.generated.rs",
None,
),
];
for (input, _, _) in &schema_configs {
println!("cargo:rerun-if-changed={}", input);
}
for (input, output, schema_patch) in &schema_configs {
build(input, output, schema_patch)?;
}
generate_language_subtags()?;
Ok(())
}
pub static GENERATED_CODE_HEADER: &str = "
* This file is automatically generated by build.rs.
* Do not edit manually!
";
fn add_ignore_rustfmt(file: &mut syn::File) {
let doc_attr = syn::parse_quote! { #![cfg_attr(any(), rustfmt::skip)] };
file.attrs.insert(0, doc_attr);
}
fn add_ignore_clippy(file: &mut syn::File) {
let doc_attr = syn::parse_quote! { #![allow(clippy::all)] };
file.attrs.insert(0, doc_attr);
}
fn build(input: &str, output: &str, schema_patch: &Option<&dyn Fn(&mut Value)>) -> Result<(), BuildError> {
let content = fs::read_to_string(input)?;
let mut schema_value = serde_json::from_str(&content)?;
if let Some(patch_fn) = schema_patch {
patch_fn(&mut schema_value);
}
let schema: schemars::schema::RootSchema = serde_json::from_value(schema_value)?;
let mut type_space = TypeSpace::new(
TypeSpaceSettings::default()
.with_struct_builder(true)
.with_derive("PartialEq".into())
.with_derive("Eq".into()),
);
type_space.add_root_schema(schema)?;
let mut file = syn::parse2::<syn::File>(type_space.to_stream())?;
add_ignore_rustfmt(&mut file);
add_ignore_clippy(&mut file);
let doc_attr = syn::parse_quote! { #![doc = #GENERATED_CODE_HEADER] };
file.attrs.insert(0, doc_attr);
let content = prettyplease::unparse(&file);
let mut out_file = Path::new("src").to_path_buf();
out_file.push(output);
Ok(fs::write(out_file, content)?)
}
fn fix_2_0_schema(value: &mut Value) {
let prefix = "properties.vulnerabilities.items.properties.scores.items.properties";
let fix_paths = [format!("{}.cvss_v2", prefix), format!("{}.cvss_v3", prefix)];
for path in fix_paths {
value.dot_set(path.as_str(), json!({"type": "object"})).unwrap();
}
remove_datetime_formats(value);
}
fn fix_2_1_schema(value: &mut Value) {
let prefix = "properties.vulnerabilities.items.properties.metrics.items.properties.content.properties";
let fix_paths = [
format!("{}.cvss_v2", prefix),
format!("{}.cvss_v3", prefix),
format!("{}.cvss_v4", prefix),
format!("{}.ssvc_v1", prefix),
format!("{}.ssvc_v2", prefix),
];
for path in fix_paths {
value.dot_set(path.as_str(), json!({"type": "object"})).unwrap();
}
remove_datetime_formats(value);
}
fn remove_datetime_formats(value: &mut Value) {
if let Value::Object(map) = value {
if let Some(format) = map.get("format") {
if format.as_str() == Some("date-time") {
map.remove("format");
}
}
for (_, v) in map.iter_mut() {
remove_datetime_formats(v);
}
} else if let Value::Array(arr) = value {
for item in arr.iter_mut() {
remove_datetime_formats(item);
}
}
}
const LANGUAGE_REGISTRY: &str = include_str!("assets/language-subtag-registry.txt");
fn generate_language_subtags() -> Result<(), BuildError> {
let mut subtags = Vec::new();
let mut current_entry_type = None;
for line in LANGUAGE_REGISTRY.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("%%") {
current_entry_type = None;
continue;
}
if let Some(type_value) = line.strip_prefix("Type: ") {
current_entry_type = Some(type_value.to_string());
continue;
}
if let Some(ref entry_type) = current_entry_type {
if entry_type == "language" {
if let Some(subtag) = line.strip_prefix("Subtag: ") {
subtags.push(subtag.to_string());
}
}
}
}
subtags.sort_unstable();
let subtags_iter = subtags.iter().map(|s| s.as_str());
let tokens = quote! {
#![doc = #GENERATED_CODE_HEADER]
pub static LANGUAGE_SUBTAGS_ARRAY: &[&str] = &[
#(#subtags_iter),*
];
pub fn is_valid_language_subtag(subtag: &str) -> bool {
LANGUAGE_SUBTAGS_ARRAY.binary_search(&subtag).is_ok()
}
};
let mut file: syn::File = syn::parse2(tokens)?;
add_ignore_rustfmt(&mut file);
add_ignore_clippy(&mut file);
let code = prettyplease::unparse(&file);
let out_path = Path::new("src").join("generated").join("language_subtags.rs");
fs::write(&out_path, code)?;
println!("cargo:rerun-if-changed=../assets/language-subtag-registry.txt");
Ok(())
}