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,
}
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))
}
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),
};
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;