use crate::git_date::tm::date_overflows;
use crate::objects::ObjectKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureTimestamp {
Valid(i64),
Sentinel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSignatureTimes {
pub unix_seconds: i64,
pub tz_offset_secs: i64,
pub tz_hhmm_range: std::ops::Range<usize>,
}
fn scan_decimal_timestamp(bytes: &[u8], mut i: usize) -> Option<(u128, usize)> {
const MAX_DIGITS: usize = 21;
let start = i;
let mut count = 0usize;
while i < bytes.len() && bytes[i].is_ascii_digit() {
count += 1;
if count > MAX_DIGITS {
return Some((u128::MAX, i));
}
i += 1;
}
if count == 0 {
return None;
}
let s = std::str::from_utf8(&bytes[start..i]).ok()?;
let v: u128 = s.parse().ok()?;
Some((v, i))
}
fn skip_fsck_date_leading_ws(bytes: &[u8], mut i: usize) -> usize {
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
i
}
fn parse_tz_hhmm_offset(offset: &str) -> Option<i64> {
let b = offset.as_bytes();
if b.len() < 5 {
return None;
}
if !(b[0] == b'+' || b[0] == b'-') {
return None;
}
let sign = if b[0] == b'-' { -1i64 } else { 1i64 };
let hours: i64 = std::str::from_utf8(&b[1..3]).ok()?.parse().ok()?;
let minutes: i64 = std::str::from_utf8(&b[3..5]).ok()?.parse().ok()?;
Some(sign * (hours * 3600 + minutes * 60))
}
#[must_use]
pub fn parse_signature_times(ident: &str) -> Option<ParsedSignatureTimes> {
match parse_signature_tail(ident)? {
SignatureTail::Valid(p) => Some(p),
SignatureTail::Overflow | SignatureTail::NonNumeric => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignatureTail {
Valid(ParsedSignatureTimes),
Overflow,
NonNumeric,
}
#[must_use]
pub fn parse_signature_tail(ident: &str) -> Option<SignatureTail> {
let bytes = ident.as_bytes();
let gt = ident.rfind('>')?;
let mut i = skip_fsck_date_leading_ws(bytes, gt + 1);
if i >= bytes.len() || !bytes[i].is_ascii_digit() {
return Some(SignatureTail::NonNumeric);
}
let (raw, after_digits) = scan_decimal_timestamp(bytes, i)?;
if after_digits >= bytes.len() || bytes[after_digits] != b' ' {
return None;
}
i = after_digits + 1;
if i + 5 > bytes.len() {
return None;
}
let tz_slice = ident.get(i..i + 5)?;
let tz_offset_secs = parse_tz_hhmm_offset(tz_slice)?;
let tz_hhmm_range = i..i + 5;
if raw == u128::MAX || raw > u64::MAX as u128 || date_overflows(raw as u64) {
return Some(SignatureTail::Overflow);
}
let unix_seconds = i64::try_from(raw).ok()?;
Some(SignatureTail::Valid(ParsedSignatureTimes {
unix_seconds,
tz_offset_secs,
tz_hhmm_range,
}))
}
pub fn signature_timestamp_for_pretty(ident: &str) -> SignatureTimestamp {
match parse_signature_times(ident) {
Some(p) => SignatureTimestamp::Valid(p.unix_seconds),
None => SignatureTimestamp::Sentinel,
}
}
#[must_use]
pub fn committer_timestamp_for_until_filter(ident: &str) -> i64 {
match signature_timestamp_for_pretty(ident) {
SignatureTimestamp::Valid(ts) => ts,
SignatureTimestamp::Sentinel => 0,
}
}
#[must_use]
pub fn timestamp_for_at_ct(ts: SignatureTimestamp) -> Option<i64> {
match ts {
SignatureTimestamp::Valid(v) => Some(v),
SignatureTimestamp::Sentinel => None,
}
}
pub fn fsck_commit_idents(data: &[u8]) -> Result<(), String> {
crate::fsck_standalone::fsck_object(ObjectKind::Commit, data).map_err(|e| e.report_line())
}
#[must_use]
pub fn committer_unix_seconds_for_ordering(ident: &str) -> i64 {
match signature_timestamp_for_pretty(ident) {
SignatureTimestamp::Valid(ts) => ts,
SignatureTimestamp::Sentinel => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_numeric_author_date_is_non_numeric_tail() {
let ident = "A <e@x> totally_bogus -0700";
assert!(matches!(
parse_signature_tail(ident),
Some(SignatureTail::NonNumeric)
));
}
#[test]
fn u128_max_digit_count_is_overflow_tail() {
let ident = "A <e@x> 18446744073709551617 -0700";
assert!(matches!(
parse_signature_tail(ident),
Some(SignatureTail::Overflow)
));
}
}