canaad-core 0.3.0

Core library for AAD canonicalization per RFC 8785
Documentation
//! AAD field extractor: parses and validates AAD JSON objects.

use super::scan::parse_json_with_duplicate_check;
use crate::error::{AadError, JsonType};
use crate::types::{
    ExtensionValue, Extensions, FieldKey, Purpose, Resource, SafeInt, Tenant, RESERVED_KEYS,
};
use serde_json::{Map, Value};

/// Maximum serialized AAD size in bytes (16 KiB).
pub const MAX_AAD_SIZE: usize = 16 * 1024;

/// Current supported schema version.
pub const CURRENT_VERSION: u64 = 1;

/// Parsed AAD fields before full validation.
#[derive(Debug)]
pub(crate) struct ParsedAad {
    /// Schema version
    pub(crate) version: SafeInt,
    /// Tenant identifier
    pub(crate) tenant: Tenant,
    /// Resource path
    pub(crate) resource: Resource,
    /// Usage purpose
    pub(crate) purpose: Purpose,
    /// Optional timestamp
    pub(crate) timestamp: Option<SafeInt>,
    /// Extension fields
    pub(crate) extensions: Extensions,
}

/// Parses, duplicate-checks, and validates an AAD JSON string against the spec.
///
/// # Errors
///
/// Returns an error if the JSON is syntactically invalid, contains duplicate keys,
/// or violates any AAD specification constraint.
pub fn parse_aad(json: &str) -> Result<ParsedAad, AadError> {
    if json.len() > MAX_AAD_SIZE {
        return Err(AadError::SerializedTooLarge {
            max_bytes: MAX_AAD_SIZE,
            actual_bytes: json.len(),
        });
    }

    let value = parse_json_with_duplicate_check(json)?;
    let obj = value.as_object().ok_or_else(|| AadError::InvalidJson {
        message: "AAD must be a JSON object".to_string(),
    })?;

    let version = extract_version(obj)?;
    validate_field_names(obj)?;

    let tenant = extract_string_field(obj, "tenant").and_then(Tenant::new)?;
    let resource = extract_string_field(obj, "resource").and_then(Resource::new)?;
    let purpose = extract_string_field(obj, "purpose").and_then(Purpose::new)?;
    let timestamp = extract_optional_timestamp(obj)?;
    let extensions = extract_extensions(obj)?;

    Ok(ParsedAad { version, tenant, resource, purpose, timestamp, extensions })
}

/// Extracts and validates the version field.
fn extract_version(obj: &Map<String, Value>) -> Result<SafeInt, AadError> {
    match obj.get("v") {
        None => Err(AadError::MissingRequiredField { field: "v" }),
        Some(v) => {
            let n = v.as_u64().ok_or_else(|| AadError::WrongFieldType {
                field: "v",
                expected: "integer",
                actual: JsonType::from(v),
            })?;
            if n != CURRENT_VERSION {
                return Err(AadError::UnsupportedVersion { version: n });
            }
            SafeInt::new(n)
        }
    }
}

/// Validates all field names in the object.
fn validate_field_names(obj: &Map<String, Value>) -> Result<(), AadError> {
    for key in obj.keys() {
        if RESERVED_KEYS.contains(&key.as_str()) {
            continue;
        }
        let field_key = FieldKey::new(key.clone()).map_err(|_| AadError::InvalidFieldKey {
            key: key.clone(),
            reason: "field keys must match pattern [a-z_]+".to_string(),
        })?;
        if !key.starts_with("x_") {
            return Err(AadError::UnknownField { field: key.clone(), version: CURRENT_VERSION });
        }
        field_key.validate_as_extension()?;
    }
    Ok(())
}

/// Extracts a required string field.
fn extract_string_field(obj: &Map<String, Value>, field: &'static str) -> Result<String, AadError> {
    obj.get(field).map_or(Err(AadError::MissingRequiredField { field }), |v| {
        v.as_str().map(String::from).ok_or_else(|| AadError::WrongFieldType {
            field,
            expected: "string",
            actual: JsonType::from(v),
        })
    })
}

/// Extracts the optional timestamp field.
fn extract_optional_timestamp(obj: &Map<String, Value>) -> Result<Option<SafeInt>, AadError> {
    match obj.get("ts") {
        None => Ok(None),
        Some(v) => match v.as_u64() {
            Some(n) => Ok(Some(SafeInt::new(n)?)),
            None => v.as_i64().map_or_else(
                || {
                    Err(AadError::WrongFieldType {
                        field: "ts",
                        expected: "integer",
                        actual: JsonType::from(v),
                    })
                },
                |i| Err(AadError::NegativeInteger { value: i }),
            ),
        },
    }
}

/// Extracts all extension fields.
fn extract_extensions(obj: &Map<String, Value>) -> Result<Extensions, AadError> {
    let mut extensions = Extensions::new();
    for (key, value) in obj {
        if key.starts_with("x_") {
            let field_key = FieldKey::new(key)?;
            field_key.validate_as_extension()?;
            let ext_value = parse_extension_value(value)?;
            extensions.insert(field_key, ext_value);
        }
    }
    Ok(extensions)
}

/// Parses an extension value (string or integer).
fn parse_extension_value(value: &Value) -> Result<ExtensionValue, AadError> {
    match value {
        Value::String(s) => ExtensionValue::string(s),
        Value::Number(n) => n.as_u64().map_or_else(
            || {
                n.as_i64().map_or(
                    Err(AadError::WrongFieldType {
                        field: "extension",
                        expected: "string or integer",
                        actual: JsonType::Number,
                    }),
                    |i| Err(AadError::NegativeInteger { value: i }),
                )
            },
            ExtensionValue::integer,
        ),
        _ => Err(AadError::WrongFieldType {
            field: "extension",
            expected: "string or integer",
            actual: JsonType::from(value),
        }),
    }
}