synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Shared time-string parsing helpers for the builder modules.
//!
//! Two entry points are provided:
//!
//! - [`parse_time`] — for X.509 certificate / CRL use (RFC 5280 §4.1.2.5):
//!   chooses `UTCTime` for years 1950–2049 and `GeneralizedTime` otherwise.
//! - [`parse_generalized_time`] — always produces a `GeneralizedTime`, for
//!   use in OCSP (`producedAt`, `thisUpdate`, `nextUpdate`) and Attribute
//!   Certificates.
//!
//! Both functions accept either of the two common string formats:
//!
//! | Format | Length | Example |
//! |--------|--------|---------|
//! | `YYYYMMDDHHmmssZ` | 15 chars | `"20240101120000Z"` |
//! | `YYMMDDHHmmssZ` | 13 chars | `"240101120000Z"` — promoted to 4-digit year |
//!
//! The parsing logic lives in [`synta::GeneralizedTime::parse`]; these
//! functions are thin wrappers that add the X.509 type-selection rule.

use synta::{GeneralizedTime, UtcTime};

use crate::Time;

// ── Public API ────────────────────────────────────────────────────────────────

/// Parse a time string into the `Time` CHOICE (UTCTime or GeneralizedTime).
///
/// This follows RFC 5280 §4.1.2.5: years 1950–2049 are encoded as
/// `UTCTime`; years outside that range use `GeneralizedTime`.
///
/// Accepted formats:
///
/// | Format | Example |
/// |--------|---------|
/// | `YYYYMMDDHHmmssZ` (15 chars) | `"20240101120000Z"` |
/// | `YYMMDDHHmmssZ` (13 chars)   | `"240101120000Z"` |
///
/// Returns `Err` with a descriptive message if the string is malformed
/// or the field values are out of range.
pub fn parse_time(s: &str) -> Result<Time, String> {
    let gt = GeneralizedTime::parse(s)?;
    if (1950..=2049).contains(&gt.year) {
        UtcTime::new(gt.year, gt.month, gt.day, gt.hour, gt.minute, gt.second)
            .map(Time::UtcTime)
            .map_err(|e| format!("invalid UTCTime '{s}': {e}"))
    } else {
        Ok(Time::GeneralTime(gt))
    }
}

/// Parse a time string into a [`GeneralizedTime`].
///
/// Unlike [`parse_time`], this always produces a `GeneralizedTime` regardless
/// of the year.  Use this for OCSP fields (`producedAt`, `thisUpdate`,
/// `nextUpdate`) and Attribute Certificate validity fields, where the ASN.1
/// type is fixed as `GeneralizedTime`.
///
/// Delegates to [`synta::GeneralizedTime::parse`].
///
/// Accepted formats:
///
/// | Format | Example |
/// |--------|---------|
/// | `YYYYMMDDHHmmssZ` (15 chars) | `"20240101120000Z"` |
/// | `YYMMDDHHmmssZ` (13 chars)   | `"240101120000Z"` |
///
/// Returns `Err` with a descriptive message if the string is malformed
/// or the field values are out of range.
pub fn parse_generalized_time(s: &str) -> Result<GeneralizedTime, String> {
    GeneralizedTime::parse(s)
}

// ── Tests ──────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_time_generalized_form() {
        let t = parse_time("20240101120000Z").unwrap();
        assert!(matches!(t, Time::UtcTime(_)), "2024 should be UTCTime");
    }

    #[test]
    fn parse_time_generalized_future() {
        let t = parse_time("20500101120000Z").unwrap();
        assert!(
            matches!(t, Time::GeneralTime(_)),
            "2050 should be GeneralizedTime"
        );
    }

    #[test]
    fn parse_time_short_form() {
        let t = parse_time("240101120000Z").unwrap();
        assert!(matches!(t, Time::UtcTime(_)));
    }

    #[test]
    fn parse_generalized_time_basic() {
        let t = parse_generalized_time("20240601120000Z").unwrap();
        let _ = t;
    }

    #[test]
    fn parse_generalized_time_short_form() {
        let t = parse_generalized_time("240601120000Z").unwrap();
        let _ = t;
    }

    #[test]
    fn invalid_time_returns_error() {
        assert!(parse_time("not-a-time").is_err());
        assert!(parse_generalized_time("not-a-time").is_err());
    }

    #[test]
    fn invalid_time_error_contains_input() {
        let err = parse_time("bad").unwrap_err();
        assert!(
            err.contains("bad"),
            "error should echo the bad input: {err}"
        );
    }
}