Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gix-actor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde = ["dep:serde", "bstr/serde", "gix-date/serde"]
gix-date = { version = "^0.12.1", path = "../gix-date" }
gix-utils = { version = "^0.3.1", path = "../gix-utils" }

exn = "0.2.1"
thiserror = "2.0.17"
bstr = { version = "1.12.0", default-features = false, features = [
"std",
Expand Down
4 changes: 2 additions & 2 deletions gix-actor/src/signature/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod _ref {
}

/// Try to parse the timestamp and create an owned instance from this shared one.
pub fn to_owned(&self) -> Result<Signature, gix_date::parse::Error> {
pub fn to_owned(&self) -> Result<Signature, gix_date::parse::ParseError> {
Ok(Signature {
name: self.name.to_owned(),
email: self.email.to_owned(),
Expand Down Expand Up @@ -58,7 +58,7 @@ mod _ref {

/// Parse the `time` field for access to the passed time since unix epoch, and the time offset.
/// The format is expected to be [raw](gix_date::parse_header()).
pub fn time(&self) -> Result<gix_date::Time, gix_date::parse::Error> {
pub fn time(&self) -> Result<gix_date::Time, gix_date::parse::ParseError> {
self.time.parse()
}
}
Expand Down
2 changes: 1 addition & 1 deletion gix-date/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ bstr = { version = "1.12.0", default-features = false, features = ["std"] }
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
itoa = "1.0.17"
jiff = "0.2.17"
thiserror = "2.0.17"
exn = "0.2.1"
# TODO: used for quick and easy `TimeBacking: std::io::Write` implementation, but could make that `Copy`
# and remove this dep with custom impl
smallvec = { version = "1.15.1", features = ["write"] }
Expand Down
8 changes: 4 additions & 4 deletions gix-date/src/parse/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ use crate::{
/// If `now` is October 27, 2023 at 10:00:00 UTC:
/// * `2 minutes ago` (October 27, 2023 at 09:58:00 UTC)
/// * `3 hours ago` (October 27, 2023 at 07:00:00 UTC)
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
pub fn parse(input: &str, now: Option<SystemTime>) -> exn::Result<Time, Error> {
Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
let val = val
.to_zoned(TimeZone::UTC)
Expand All @@ -97,7 +97,7 @@ pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
// Format::Raw
val
} else {
return Err(Error::InvalidDateString { input: input.into() });
exn::bail!(Error::InvalidDateString { input: input.into() })
})
}

Expand Down Expand Up @@ -168,15 +168,15 @@ pub fn parse_header(input: &str) -> Option<Time> {
/// whose weekdays are inconsistent with the date. While the day-of-week
/// still must be parsed, it is otherwise ignored. This seems to be
/// consistent with how `git` behaves.
fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
fn strptime_relaxed(fmt: &str, input: &str) -> std::result::Result<Zoned, jiff::Error> {
let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
tm.set_weekday(None);
tm.to_zoned()
}

/// This is just like strptime_relaxed, except for RFC 2822 parsing.
/// Namely, it permits the weekday to be inconsistent with the date.
fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
fn rfc2822_relaxed(input: &str) -> std::result::Result<Zoned, jiff::Error> {
static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
P.parse_zoned(input)
}
74 changes: 65 additions & 9 deletions gix-date/src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,73 @@ use smallvec::SmallVec;

use crate::Time;

#[derive(thiserror::Error, Debug, Clone)]
/// Errors that can occur when parsing dates.
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub enum Error {
#[error("Could not convert a duration into a date")]
RelativeTimeConversion,
#[error("Date string can not be parsed")]
InvalidDateString { input: String },
#[error("The heat-death of the universe happens before this date")]
InvalidDate(#[from] std::num::TryFromIntError),
#[error("Current time is missing but required to handle relative dates.")]
InvalidDate(std::num::TryFromIntError),
MissingCurrentTime,
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::RelativeTimeConversion => write!(f, "Could not convert a duration into a date"),
Error::InvalidDateString { input } => write!(f, "Date string can not be parsed: {input}"),
Error::InvalidDate(err) => write!(f, "The heat-death of the universe happens before this date: {err}"),
Error::MissingCurrentTime => write!(f, "Current time is missing but required to handle relative dates."),
}
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::InvalidDate(err) => Some(err),
_ => None,
}
}
}

/// A wrapper around `exn::Exn<Error>` that implements `std::error::Error`.
///
/// This type is returned by functions that integrate with external APIs requiring `std::error::Error`,
/// while internally using exn for context-aware error tracking.
#[derive(Debug)]
pub struct ParseError(exn::Exn<Error>);

impl ParseError {
/// Create a ParseError from an exn error.
pub fn from_exn(exn: exn::Exn<Error>) -> Self {
Self(exn)
}

/// Get a reference to the underlying error.
pub fn as_error(&self) -> &Error {
self.0.as_error()
}
}

impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}

impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_error())
}
}

impl From<exn::Exn<Error>> for ParseError {
fn from(exn: exn::Exn<Error>) -> Self {
Self(exn)
}
}

/// A container for just enough bytes to hold the largest-possible [`time`](Time) instance.
/// It's used in conjunction with
#[derive(Default, Clone)]
Expand Down Expand Up @@ -55,10 +109,12 @@ impl Time {
}

impl FromStr for Time {
type Err = Error;
type Err = ParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
crate::parse_header(s).ok_or_else(|| Error::InvalidDateString { input: s.into() })
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
crate::parse_header(s)
.ok_or_else(|| Error::InvalidDateString { input: s.into() })
.map_err(|e| ParseError::from_exn(exn::Exn::from(e)))
}
}

Expand Down
11 changes: 6 additions & 5 deletions gix-date/tests/time/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,23 @@ fn git_rfc2822() {
#[test]
fn raw() -> gix_testtools::Result {
assert_eq!(
gix_date::parse("1660874655 +0800", None)?,
gix_date::parse("1660874655 +0800", None).map_err(gix_date::parse::ParseError::from)?,
Time {
seconds: 1660874655,
offset: 28800,
},
);

assert_eq!(
gix_date::parse("1112911993 +0100", None)?,
gix_date::parse("1112911993 +0100", None).map_err(gix_date::parse::ParseError::from)?,
Time {
seconds: 1112911993,
offset: 3600,
},
);

assert_eq!(
gix_date::parse("1313584730 +051500", None)?,
gix_date::parse("1313584730 +051500", None).map_err(gix_date::parse::ParseError::from)?,
Time {
seconds: 1313584730,
offset: 18900,
Expand All @@ -90,7 +90,7 @@ fn raw() -> gix_testtools::Result {
);

assert_eq!(
gix_date::parse("1313584730 -0230", None)?,
gix_date::parse("1313584730 -0230", None).map_err(gix_date::parse::ParseError::from)?,
Time {
seconds: 1313584730,
offset: -150 * 60,
Expand Down Expand Up @@ -177,8 +177,9 @@ fn git_default() {

#[test]
fn invalid_dates_can_be_produced_without_current_time() {
let err = gix_date::parse("foobar", None).unwrap_err();
assert!(matches!(
gix_date::parse("foobar", None).unwrap_err(),
err.as_error(),
gix_date::parse::Error::InvalidDateString { input } if input == "foobar"
));
}
Expand Down
3 changes: 2 additions & 1 deletion gix-date/tests/time/parse/relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ fn large_offsets() {

#[test]
fn large_offsets_do_not_panic() {
let result = gix_date::parse("9999999999 weeks ago", Some(std::time::UNIX_EPOCH));
assert!(matches!(
gix_date::parse("9999999999 weeks ago", Some(std::time::UNIX_EPOCH)),
result.as_ref().map_err(|e| e.as_error()),
Err(gix_date::parse::Error::RelativeTimeConversion)
));
}
Expand Down
1 change: 1 addition & 0 deletions gix-revision/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gix-trace = { version = "^0.1.17", path = "../gix-trace", optional = true }

bstr = { version = "1.12.0", default-features = false, features = ["std"] }
bitflags = { version = "2", optional = true }
exn = "0.2.1"
thiserror = "2.0.17"
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
document-features = { version = "0.2.1", optional = true }
Expand Down
Loading