pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use serde::Deserialize;

pub const MAX_PUBLISH_TIMESTAMP: u64 = 253_402_300_799;

pub fn deserialize_optional_publish_timestamp<'de, D>(
    deserializer: D,
) -> Result<Option<u64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    if value.is_null() {
        return Ok(None);
    }
    unix_seconds_from_json(value)
        .filter(|timestamp| *timestamp <= MAX_PUBLISH_TIMESTAMP)
        .ok_or_else(|| serde::de::Error::custom("published_at must be whole UTC Unix seconds"))
        .map(Some)
}

fn unix_seconds_from_json(value: serde_json::Value) -> Option<u64> {
    match value {
        serde_json::Value::Number(number) => number.as_u64(),
        serde_json::Value::String(legacy) => unix_seconds_from_legacy_string(&legacy),
        _ => None,
    }
}

fn unix_seconds_from_legacy_string(value: &str) -> Option<u64> {
    value
        .parse::<u64>()
        .ok()
        .or_else(|| unix_seconds_from_rfc3339(value))
}

fn unix_seconds_from_rfc3339(value: &str) -> Option<u64> {
    let bytes = value.as_bytes();
    if bytes.len() < 20 {
        return None;
    }
    let year = digits(bytes, 0, 4)?;
    require_byte(bytes, 4, b'-')?;
    let month = digits(bytes, 5, 2)?;
    require_byte(bytes, 7, b'-')?;
    let day = digits(bytes, 8, 2)?;
    require_byte(bytes, 10, b'T')?;
    let hour = digits(bytes, 11, 2)?;
    require_byte(bytes, 13, b':')?;
    let minute = digits(bytes, 14, 2)?;
    require_byte(bytes, 16, b':')?;
    let second = digits(bytes, 17, 2)?;
    let mut index = 19;
    if bytes.get(index) == Some(&b'.') {
        index += 1;
        let fraction_start = index;
        while index < bytes.len() && bytes[index].is_ascii_digit() {
            index += 1;
        }
        if index == fraction_start {
            return None;
        }
    }
    let (offset_seconds, end) = timezone_offset(bytes, index)?;
    if end != bytes.len()
        || !valid_date(year, month, day)
        || hour > 23
        || minute > 59
        || second > 59
    {
        return None;
    }
    days_from_civil(year, month, day)
        .checked_mul(86_400)?
        .checked_add(i64::from(hour) * 3600 + i64::from(minute) * 60 + i64::from(second))?
        .checked_sub(offset_seconds)
        .and_then(|utc| u64::try_from(utc).ok())
}

fn timezone_offset(bytes: &[u8], index: usize) -> Option<(i64, usize)> {
    match bytes.get(index)? {
        b'Z' => Some((0, index + 1)),
        b'+' | b'-' => {
            let sign = if bytes[index] == b'+' { 1 } else { -1 };
            let hour = digits(bytes, index + 1, 2)?;
            require_byte(bytes, index + 3, b':')?;
            let minute = digits(bytes, index + 4, 2)?;
            if hour > 23 || minute > 59 {
                return None;
            }
            Some((
                sign * (i64::from(hour) * 3600 + i64::from(minute) * 60),
                index + 6,
            ))
        }
        _ => None,
    }
}

fn valid_date(year: u32, month: u32, day: u32) -> bool {
    let days_in_month = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)) {
                29
            } else {
                28
            }
        }
        _ => return false,
    };
    day >= 1 && day <= days_in_month
}

fn days_from_civil(year: u32, month: u32, day: u32) -> i64 {
    let mut year = i64::from(year);
    if month <= 2 {
        year -= 1;
    }
    let era = year.div_euclid(400);
    let year_of_era = (year - era * 400) as u32;
    let shifted_month = i64::from(month) + if month > 2 { -3 } else { 9 };
    let day_of_year = (153 * shifted_month + 2) / 5 + i64::from(day) - 1;
    let day_of_era =
        i64::from(year_of_era) * 365 + i64::from(year_of_era / 4 - year_of_era / 100) + day_of_year;
    era * 146_097 + day_of_era - 719_468
}

fn digits(bytes: &[u8], start: usize, width: usize) -> Option<u32> {
    let slice = bytes.get(start..start + width)?;
    if !slice.iter().all(u8::is_ascii_digit) {
        return None;
    }
    std::str::from_utf8(slice).ok()?.parse().ok()
}

fn require_byte(bytes: &[u8], index: usize, expected: u8) -> Option<()> {
    (bytes.get(index) == Some(&expected)).then_some(())
}