haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Append-version bounds are separate from times inside a document.

use super::{Input, Refused, Spanned, Stage, spans};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bound {
    Version(u64),
    Instant(u64),
    Now,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Window {
    pub from: Bound,
    pub to: Bound,
}

/// Read one stage from its original start; leave the following pipe untouched.
pub(in crate::serve) fn read(text: &str, start: usize) -> Result<Spanned<Stage>, Refused> {
    let span = spans::stage(text, start);
    let refuse = |code, message: String| Refused::Window(code, message, span);
    let mut input = Input { text, rest: text };
    let grammar = |error: Refused| refuse("query_window_bound", error.to_string());
    input
        .require("window", "the stage names window")
        .map_err(grammar)?;
    input
        .require("(", "window takes from and to in brackets")
        .map_err(grammar)?;
    let from = bound(&mut input, false).map_err(|(code, message)| refuse(code, message))?;
    input
        .require(";", "window separates from and to with a semicolon")
        .map_err(grammar)?;
    let to = bound(&mut input, true).map_err(|(code, message)| refuse(code, message))?;
    input
        .require(")", "window takes two bounds and a closing bracket")
        .map_err(grammar)?;
    match (from, to) {
        (Bound::Version(from), Bound::Version(to)) | (Bound::Instant(from), Bound::Instant(to)) => {
            if from > to {
                return Err(refuse(
                    "query_window_reversed",
                    "window from is after to".to_owned(),
                ));
            }
        }
        (Bound::Version(_) | Bound::Instant(_), Bound::Now) => {}
        _ => {
            return Err(refuse(
                "query_window_bound",
                "window uses two versions or two RFC 3339 instants".to_owned(),
            ));
        }
    }
    Ok(Spanned {
        value: Stage::Window(Window { from, to }),
        span: super::Span {
            start,
            end: start + input.position(),
        },
    })
}

fn bound(input: &mut Input<'_>, upper: bool) -> Result<Bound, (&'static str, String)> {
    input.trim();
    if upper && input.take("$now") {
        return Ok(Bound::Now);
    }
    if input.rest.starts_with('"') {
        let text = input
            .string("a window instant")
            .map_err(|error| ("query_window_instant", error.to_string()))?;
        return instant(&text)
            .map(Bound::Instant)
            .map_err(|message| ("query_window_instant", message.to_owned()));
    }
    let length = input.rest.bytes().take_while(u8::is_ascii_digit).count();
    let (number, _) = input.rest.split_at(length);
    if number.is_empty() {
        return Err((
            "query_window_bound",
            "window takes unsigned versions or quoted RFC 3339 instants; $now is only to"
                .to_owned(),
        ));
    }
    let number = number.parse::<u64>().map_err(|_| {
        (
            "query_window_bound",
            "window version exceeds u64".to_owned(),
        )
    })?;
    input.advance(length);
    Ok(Bound::Version(number))
}

/// The stored clock is unsigned epoch nanoseconds; refuse unrepresentable instants.
pub(in crate::serve) fn instant(text: &str) -> Result<u64, &'static str> {
    let bytes = text.as_bytes();
    let invalid = "window instant must be RFC 3339 with a known offset and fit epoch nanoseconds";
    if bytes.len() < 20
        || bytes.get(4) != Some(&b'-')
        || bytes.get(7) != Some(&b'-')
        || !matches!(bytes.get(10), Some(b'T' | b't'))
        || bytes.get(13) != Some(&b':')
        || bytes.get(16) != Some(&b':')
    {
        return Err(invalid);
    }
    let year = digits(bytes.get(..4).ok_or(invalid)?)?;
    let month = digits(bytes.get(5..7).ok_or(invalid)?)?;
    let day = digits(bytes.get(8..10).ok_or(invalid)?)?;
    let hour = digits(bytes.get(11..13).ok_or(invalid)?)?;
    let minute = digits(bytes.get(14..16).ok_or(invalid)?)?;
    let second = digits(bytes.get(17..19).ok_or(invalid)?)?;
    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    let days = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if leap {
                29
            } else {
                28
            }
        }
        _ => return Err(invalid),
    };
    if day == 0 || day > days || hour > 23 || minute > 59 || second > 59 {
        return Err("window instant has an invalid date or an unrepresentable leap second");
    }
    let mut cursor = 19;
    let mut nanos = 0_u64;
    if bytes.get(cursor) == Some(&b'.') {
        cursor += 1;
        let start = cursor;
        while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
            let digit = u64::from(*bytes.get(cursor).ok_or(invalid)? - b'0');
            if cursor - start < 9 {
                nanos = nanos * 10 + digit;
            } else if digit != 0 {
                return Err("window instant precision exceeds stored nanoseconds");
            }
            cursor += 1;
        }
        if cursor == start {
            return Err(invalid);
        }
        for _ in cursor - start..9 {
            nanos *= 10;
        }
    }
    let offset = match bytes.get(cursor) {
        Some(b'Z' | b'z') if cursor + 1 == bytes.len() => 0,
        Some(sign @ (b'+' | b'-')) if cursor + 6 == bytes.len() => {
            if bytes.get(cursor + 3) != Some(&b':') {
                return Err(invalid);
            }
            let hours = digits(bytes.get(cursor + 1..cursor + 3).ok_or(invalid)?)?;
            let minutes = digits(bytes.get(cursor + 4..cursor + 6).ok_or(invalid)?)?;
            if hours > 23 || minutes > 59 || (*sign == b'-' && hours == 0 && minutes == 0) {
                return Err(invalid);
            }
            let seconds = hours * 3600 + minutes * 60;
            if *sign == b'-' { -seconds } else { seconds }
        }
        _ => return Err(invalid),
    };
    // March starts the year so leap days belong to its final month.
    let year = year - i64::from(month <= 2);
    let era = year.div_euclid(400);
    let within = year - era * 400;
    let shifted = month + if month > 2 { -3 } else { 9 };
    let ordinal = (153 * shifted + 2) / 5 + day - 1;
    let day = era * 146_097 + within * 365 + within / 4 - within / 100 + ordinal - 719_468;
    let seconds = day * 86_400 + hour * 3600 + minute * 60 + second - offset;
    u64::try_from(seconds)
        .ok()
        .and_then(|seconds| seconds.checked_mul(1_000_000_000))
        .and_then(|seconds| seconds.checked_add(nanos))
        .ok_or(invalid)
}

fn digits(bytes: &[u8]) -> Result<i64, &'static str> {
    if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
        return Err("window instant contains a nondecimal date or time");
    }
    Ok(bytes
        .iter()
        .fold(0, |number, digit| number * 10 + i64::from(*digit - b'0')))
}

#[cfg(test)]
#[path = "window_tests.rs"]
mod tests;