csaf-rs 0.1.0

A parser for the CSAF standard written in Rust
use std::path::Path;
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> {
    build(
        "./src/csaf/csaf2_0/csaf_json_schema.json",
        "csaf/csaf2_0/schema.rs",
        false,
        true,
    )?;
    build(
        "./src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json",
        "csaf/csaf2_1/ssvc_schema.rs",
        false,
        false,
    )?;
    build(
        "./src/csaf/csaf2_1/csaf.json",
        "csaf/csaf2_1/schema.rs",
        false,
        true,
    )?;
    build(
        "ssvc/data/schema/v1/Decision_Point-1-0-1.schema.json",
        "csaf/csaf2_1/ssvc_dp_schema.rs",
        true,
        false,
    )?;

    Ok(())
}

fn build(input: &str, output: &str, from_root: bool, no_date_time: bool) -> Result<(), BuildError> {
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let input_path = if from_root {
        let mut manifest_path = manifest_dir.to_path_buf();
        
        // Handle cargo publish case where we're in target/package/<version>
        if manifest_path.to_string_lossy().contains("target/package") {
            // Go up 3 levels: target/package/<version> -> project root
            for _ in 0..3 {
                manifest_path.pop();
            }
        } else {
            // Go up 1 level: csaf-rs -> project root
            manifest_path.pop();
        }
        
        manifest_path.join(input)
    } else {
        manifest_dir.join(input)
    };

    let content = fs::read_to_string(&input_path)?;
    let mut schema_value = serde_json::from_str(&content)?;
    if no_date_time {
        // Recursively search for "format": "date-time" and remove this format
        remove_datetime_formats(&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));
    type_space.add_root_schema(schema)?;

    let content = prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream())?);

    let mut out_file = Path::new("src").to_path_buf();
    out_file.push(output);
    Ok(fs::write(out_file, content)?)
}

fn remove_datetime_formats(value: &mut serde_json::Value) {
    if let serde_json::Value::Object(map) = value {
        if let Some(format) = map.get("format") {
            if format.as_str() == Some("date-time") {
                // Remove the format property entirely
                map.remove("format");
            }
        }

        // Recursively process all values in the object
        for (_, v) in map.iter_mut() {
            remove_datetime_formats(v);
        }
    } else if let serde_json::Value::Array(arr) = value {
        for item in arr.iter_mut() {
            remove_datetime_formats(item);
        }
    }
}