Skip to content

Commit 57366d3

Browse files
committed
fix!: turn SignatureRef::time field into &str.
We also add a `gix_date::Time::to_str()` method, along with related utilities, to be able to turn a parsed time back into a raw buffer conveniently. Further, remove `Time::to_bstring()` in favor of a `Display` implementation.
1 parent 545edf5 commit 57366d3

File tree

11 files changed

+258
-81
lines changed

11 files changed

+258
-81
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-actor/src/lib.rs

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
doc = ::document_features::document_features!()
77
)]
88
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
9-
#![deny(missing_docs, rust_2018_idioms)]
10-
#![forbid(unsafe_code)]
9+
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
1110

1211
/// The re-exported `bstr` crate.
1312
///
@@ -85,6 +84,6 @@ pub struct SignatureRef<'a> {
8584
///
8685
/// Use [SignatureRef::trim()] or trim manually to be able to clean it up.
8786
pub email: &'a BStr,
88-
/// The time stamp at which the signature was performed.
89-
pub time: &'a BStr,
87+
/// The timestamp at which the signature was performed.
88+
pub time: &'a str,
9089
}

gix-actor/src/signature/decode.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ pub(crate) mod function {
2323
))
2424
.map(|maybe_bytes| {
2525
if let Some((bytes,)) = maybe_bytes {
26-
bytes.into()
26+
// SAFETY: The parser validated that there are only ASCII characters.
27+
#[allow(unsafe_code)]
28+
unsafe {
29+
std::str::from_utf8_unchecked(bytes)
30+
}
2731
} else {
28-
b"".into()
32+
""
2933
}
3034
}),
3135
)
@@ -82,7 +86,6 @@ pub use function::identity;
8286
mod tests {
8387
mod parse_signature {
8488
use crate::{signature, SignatureRef};
85-
use bstr::ByteSlice;
8689
use gix_date::time::Sign;
8790
use gix_testtools::to_bstr_err;
8891
use winnow::prelude::*;
@@ -95,9 +98,9 @@ mod tests {
9598

9699
fn signature(name: &'static str, email: &'static str, time: &'static str) -> SignatureRef<'static> {
97100
SignatureRef {
98-
name: name.as_bytes().as_bstr(),
99-
email: email.as_bytes().as_bstr(),
100-
time: time.as_bytes().as_bstr(),
101+
name: name.into(),
102+
email: email.into(),
103+
time,
101104
}
102105
}
103106

gix-actor/src/signature/mod.rs

+19-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
mod _ref {
22
use bstr::ByteSlice;
3-
use gix_date::Time;
43
use winnow::{error::StrContext, prelude::*};
54

65
use crate::{signature::decode, IdentityRef, Signature, SignatureRef};
@@ -32,7 +31,7 @@ mod _ref {
3231
SignatureRef {
3332
name: self.name.trim().as_bstr(),
3433
email: self.email.trim().as_bstr(),
35-
time: self.time.trim().as_bstr(),
34+
time: self.time.trim(),
3635
}
3736
}
3837

@@ -49,49 +48,52 @@ mod _ref {
4948
///
5049
/// For a fallible and more complete, but slower version, use [`time()`](Self::time).
5150
pub fn seconds(&self) -> gix_date::SecondsSinceUnixEpoch {
52-
use winnow::stream::AsChar;
5351
self.time
5452
.trim()
55-
.split(|b| b.is_space())
53+
.split(' ')
5654
.next()
57-
.and_then(|i| i.to_str().ok()?.parse().ok())
55+
.and_then(|i| i.parse().ok())
5856
.unwrap_or_default()
5957
}
6058

6159
/// Parse the `time` field for access to the passed time since unix epoch, and the time offset.
60+
/// The format is expected to be [raw](gix_date::parse_header()).
6261
pub fn time(&self) -> Result<gix_date::Time, gix_date::parse::Error> {
63-
Time::from_bytes(self.time)
62+
self.time.parse()
6463
}
6564
}
6665
}
6766

6867
mod convert {
6968
use crate::{Signature, SignatureRef};
69+
use gix_date::parse::TimeBuf;
7070

7171
impl Signature {
7272
/// Borrow this instance as immutable, serializing the `time` field into `buf`.
73-
pub fn to_ref<'a>(&'a self, buf: &'a mut Vec<u8>) -> SignatureRef<'a> {
73+
pub fn to_ref<'a>(&'a self, time_buf: &'a mut TimeBuf) -> SignatureRef<'a> {
7474
SignatureRef {
7575
name: self.name.as_ref(),
7676
email: self.email.as_ref(),
77-
time: self.time.to_ref(buf),
77+
time: self.time.to_str(time_buf),
7878
}
7979
}
8080
}
8181

82-
impl TryFrom<SignatureRef<'_>> for Signature {
83-
type Error = gix_date::parse::Error;
84-
85-
fn try_from(other: SignatureRef<'_>) -> Result<Signature, Self::Error> {
86-
other.to_owned()
82+
impl From<SignatureRef<'_>> for Signature {
83+
fn from(other: SignatureRef<'_>) -> Signature {
84+
Signature {
85+
name: other.name.to_owned(),
86+
email: other.email.to_owned(),
87+
time: other.time().unwrap_or_default(),
88+
}
8789
}
8890
}
8991
}
9092

9193
pub(crate) mod write {
92-
use bstr::{BStr, ByteSlice};
93-
9494
use crate::{Signature, SignatureRef};
95+
use bstr::{BStr, ByteSlice};
96+
use gix_date::parse::TimeBuf;
9597

9698
/// The Error produced by [`Signature::write_to()`].
9799
#[derive(Debug, thiserror::Error)]
@@ -111,7 +113,7 @@ pub(crate) mod write {
111113
impl Signature {
112114
/// Serialize this instance to `out` in the git serialization format for actors.
113115
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
114-
let mut buf = Vec::<u8>::new();
116+
let mut buf = TimeBuf::default();
115117
self.to_ref(&mut buf).write_to(out)
116118
}
117119
/// Computes the number of bytes necessary to serialize this signature
@@ -128,7 +130,7 @@ pub(crate) mod write {
128130
out.write_all(b"<")?;
129131
out.write_all(validated_token(self.email)?)?;
130132
out.write_all(b"> ")?;
131-
out.write_all(validated_token(self.time)?)
133+
out.write_all(validated_token(self.time.into())?)
132134
}
133135
/// Computes the number of bytes necessary to serialize this signature
134136
pub fn size(&self) -> usize {

gix-date/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur
2424
itoa = "1.0.1"
2525
jiff = "0.2.10"
2626
thiserror = "2.0.0"
27+
# TODO: used for quick and easy `TimeBacking: std::io::Write` implementation, but could make that `Copy`
28+
# and remove this dep with custom impl
29+
smallvec = { version = "1.15.0", features = ["write"] }
2730

2831
document-features = { version = "0.2.0", optional = true }
2932

gix-date/src/lib.rs

+4-27
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,28 @@
77
doc = ::document_features::document_features!()
88
)]
99
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
10-
#![deny(missing_docs, rust_2018_idioms)]
11-
#![forbid(unsafe_code)]
12-
10+
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
1311
///
1412
pub mod time;
1513

1614
///
1715
pub mod parse;
18-
use bstr::{BStr, ByteSlice};
1916
pub use parse::function::parse;
20-
pub use parse::function::parse_raw;
21-
use parse::Error;
22-
use std::time::SystemTime;
17+
pub use parse::function::parse_header;
2318

2419
/// A timestamp with timezone.
2520
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
2621
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2722
pub struct Time {
28-
/// The seconds that passed since UNIX epoch. This makes it UTC, or `<seconds>+0000`.
23+
/// The seconds that have passed since UNIX epoch. This makes it UTC, or `<seconds>+0000`.
2924
pub seconds: SecondsSinceUnixEpoch,
3025
/// The time's offset in seconds, which may be negative to match the `sign` field.
3126
pub offset: OffsetInSeconds,
3227
/// the sign of `offset`, used to encode `-0000` which would otherwise lose sign information.
3328
pub sign: time::Sign,
3429
}
3530

36-
impl Time {
37-
/// Parse date in config format into a Time
38-
pub fn from_config(i: &BStr) -> Result<Self, Error> {
39-
let s = i.as_bstr().to_str().expect("Input must be ascii");
40-
parse(s, Some(SystemTime::now()))
41-
}
42-
/// Parse raw bytes into a Time
43-
pub fn from_bytes(i: &BStr) -> Result<Self, Error> {
44-
let s = i.as_bstr().to_str().expect("Input must be ascii");
45-
parse_raw(s).ok_or(Error::InvalidDateString { input: s.into() })
46-
}
47-
/// Write time into buffer
48-
pub fn to_ref<'a>(&self, buf: &'a mut Vec<u8>) -> &'a BStr {
49-
self.write_to(buf).expect("write to memory cannot fail");
50-
buf.as_bstr()
51-
}
52-
}
53-
54-
/// The amount of seconds since unix epoch.
31+
/// The number of seconds since unix epoch.
5532
///
5633
/// Note that negative dates represent times before the unix epoch.
5734
///

gix-date/src/parse.rs

+124-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use crate::Time;
2+
use smallvec::SmallVec;
3+
use std::str::FromStr;
4+
15
#[derive(thiserror::Error, Debug, Clone)]
26
#[allow(missing_docs)]
37
pub enum Error {
@@ -11,6 +15,61 @@ pub enum Error {
1115
MissingCurrentTime,
1216
}
1317

18+
/// A container for just enough bytes to hold the largest-possible [`time`](Time) instance.
19+
/// It's used in conjunction with
20+
#[derive(Default, Clone)]
21+
pub struct TimeBuf {
22+
buf: SmallVec<[u8; Time::MAX.size()]>,
23+
}
24+
25+
impl TimeBuf {
26+
/// Represent this instance as standard string, serialized in a format compatible with
27+
/// signature fields in Git commits, also known as anything parseable as [raw format](function::parse_header()).
28+
pub fn as_str(&self) -> &str {
29+
// SAFETY: We know that serialized times are pure ASCII, a subset of UTF-8.
30+
// `buf` and `len` are written only by time-serialization code.
31+
let time_bytes = self.buf.as_slice();
32+
#[allow(unsafe_code)]
33+
unsafe {
34+
std::str::from_utf8_unchecked(time_bytes)
35+
}
36+
}
37+
38+
/// Clear the previous content.
39+
pub fn clear(&mut self) {
40+
self.buf.clear();
41+
}
42+
}
43+
44+
impl std::io::Write for TimeBuf {
45+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
46+
self.buf.write(buf)
47+
}
48+
49+
fn flush(&mut self) -> std::io::Result<()> {
50+
self.buf.flush()
51+
}
52+
}
53+
54+
impl Time {
55+
/// Serialize this instance into `buf`, exactly as it would appear in the header of a Git commit,
56+
/// and return `buf` as `&str` for easy consumption.
57+
pub fn to_str<'a>(&self, buf: &'a mut TimeBuf) -> &'a str {
58+
buf.clear();
59+
self.write_to(buf)
60+
.expect("write to memory of just the right size cannot fail");
61+
buf.as_str()
62+
}
63+
}
64+
65+
impl FromStr for Time {
66+
type Err = Error;
67+
68+
fn from_str(s: &str) -> Result<Self, Self::Err> {
69+
crate::parse_header(s).ok_or_else(|| Error::InvalidDateString { input: s.into() })
70+
}
71+
}
72+
1473
pub(crate) mod function {
1574
use std::{str::FromStr, time::SystemTime};
1675

@@ -25,7 +84,67 @@ pub(crate) mod function {
2584
SecondsSinceUnixEpoch, Time,
2685
};
2786

28-
#[allow(missing_docs)]
87+
/// Parse `input` as any time that Git can parse when inputting a date.
88+
///
89+
/// ## Examples
90+
///
91+
/// ### 1. SHORT Format
92+
///
93+
/// * `2018-12-24`
94+
/// * `1970-01-01`
95+
/// * `1950-12-31`
96+
/// * `2024-12-31`
97+
///
98+
/// ### 2. RFC2822 Format
99+
///
100+
/// * `Thu, 18 Aug 2022 12:45:06 +0800`
101+
/// * `Mon Oct 27 10:30:00 2023 -0800`
102+
///
103+
/// ### 3. GIT_RFC2822 Format
104+
///
105+
/// * `Thu, 8 Aug 2022 12:45:06 +0800`
106+
/// * `Mon Oct 27 10:30:00 2023 -0800` (Note the single-digit day)
107+
///
108+
/// ### 4. ISO8601 Format
109+
///
110+
/// * `2022-08-17 22:04:58 +0200`
111+
/// * `1970-01-01 00:00:00 -0500`
112+
///
113+
/// ### 5. ISO8601_STRICT Format
114+
///
115+
/// * `2022-08-17T21:43:13+08:00`
116+
///
117+
/// ### 6. UNIX Timestamp (Seconds Since Epoch)
118+
///
119+
/// * `123456789`
120+
/// * `0` (January 1, 1970 UTC)
121+
/// * `-1000`
122+
/// * `1700000000`
123+
///
124+
/// ### 7. Commit Header Format
125+
///
126+
/// * `1745582210 +0200`
127+
/// * `1660874655 +0800`
128+
/// * `-1660874655 +0800`
129+
///
130+
/// See also the [`parse_header()`].
131+
///
132+
/// ### 8. GITOXIDE Format
133+
///
134+
/// * `Thu Sep 04 2022 10:45:06 -0400`
135+
/// * `Mon Oct 27 2023 10:30:00 +0000`
136+
///
137+
/// ### 9. DEFAULT Format
138+
///
139+
/// * `Thu Sep 4 10:45:06 2022 -0400`
140+
/// * `Mon Oct 27 10:30:00 2023 +0000`
141+
///
142+
/// ### 10. Relative Dates (e.g., "2 minutes ago", "1 hour from now")
143+
///
144+
/// These dates are parsed *relative to a `now` timestamp*. The examples depend entirely on the value of `now`.
145+
/// If `now` is October 27, 2023 at 10:00:00 UTC:
146+
/// * `2 minutes ago` (October 27, 2023 at 09:58:00 UTC)
147+
/// * `3 hours ago` (October 27, 2023 at 07:00:00 UTC)
29148
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
30149
// TODO: actual implementation, this is just to not constantly fail
31150
if input == "1979-02-26 18:30:00" {
@@ -50,7 +169,7 @@ pub(crate) mod function {
50169
} else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
51170
// Format::Unix
52171
Time::new(val, 0)
53-
} else if let Some(val) = parse_raw(input) {
172+
} else if let Some(val) = parse_header(input) {
54173
// Format::Raw
55174
val
56175
} else if let Some(val) = relative::parse(input, now).transpose()? {
@@ -60,8 +179,9 @@ pub(crate) mod function {
60179
})
61180
}
62181

63-
#[allow(missing_docs)]
64-
pub fn parse_raw(input: &str) -> Option<Time> {
182+
/// Unlike [`parse()`] which handles all kinds of input, this function only parses the commit-header format
183+
/// like `1745582210 +0200`.
184+
pub fn parse_header(input: &str) -> Option<Time> {
65185
let mut split = input.split_whitespace();
66186
let seconds: SecondsSinceUnixEpoch = split.next()?.parse().ok()?;
67187
let offset = split.next()?;

gix-date/src/time/format.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl Time {
4444
match format {
4545
Format::Custom(CustomFormat(format)) => self.to_time().strftime(format).to_string(),
4646
Format::Unix => self.seconds.to_string(),
47-
Format::Raw => self.to_bstring().to_string(),
47+
Format::Raw => self.to_string(),
4848
}
4949
}
5050
}

0 commit comments

Comments
 (0)