Skip to content
Merged
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
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ anyhow = "1"
async-trait = "0.1"
base64 = "0.22"
bytes = "1"
chrono = "0.4.35"
criterion = { version = "0.7.0", features = ["async_tokio", "html_reports"] }
dotenv = "0.15"
env_logger = "0.11"
form_urlencoded = "1"
hex = "0.4"
hmac = "0.12"
home = "0.5"
http = "1"
jiff = "0.2"
log = "0.4"
macro_rules_attribute = "0.2.0"
once_cell = "1"
Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
chrono.workspace = true
form_urlencoded.workspace = true
hex.workspace = true
hmac.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
percent-encoding.workspace = true
sha1.workspace = true
Expand Down
96 changes: 31 additions & 65 deletions core/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,68 +18,25 @@
//! Time related utils.

use crate::Error;
use chrono::format::Fixed;
use chrono::format::Item;
use chrono::format::Numeric;
use chrono::format::Pad;
use chrono::SecondsFormat;
use chrono::Utc;

/// DateTime is the alias for chrono::DateTime<Utc>.
pub type DateTime = chrono::DateTime<Utc>;
/// DateTime is the alias for `jiff::Timestamp`.
pub type Timestamp = jiff::Timestamp;

/// Create datetime of now.
pub fn now() -> DateTime {
Utc::now()
pub fn now() -> Timestamp {
Timestamp::now()
}

/// DATE is a time format like `20220301`
const DATE: &[Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Numeric(Numeric::Day, Pad::Zero),
];

/// Format time into date: `20220301`
pub fn format_date(t: DateTime) -> String {
t.format_with_items(DATE.iter()).to_string()
pub fn format_date(t: Timestamp) -> String {
t.strftime("%Y%m%d").to_string()
}

/// ISO8601 is a time format like `20220313T072004Z`.
const ISO8601: &[Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal("T"),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Numeric(Numeric::Second, Pad::Zero),
Item::Literal("Z"),
];

/// Format time into ISO8601: `20220313T072004Z`
pub fn format_iso8601(t: DateTime) -> String {
t.format_with_items(ISO8601.iter()).to_string()
pub fn format_iso8601(t: Timestamp) -> String {
t.strftime("%Y%m%dT%H%M%SZ").to_string()
}

/// HTTP_DATE is a time format like `Sun, 06 Nov 1994 08:49:37 GMT`.
const HTTP_DATE: &[Item<'static>] = &[
Item::Fixed(Fixed::ShortWeekdayName),
Item::Literal(", "),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal(" "),
Item::Fixed(Fixed::ShortMonthName),
Item::Literal(" "),
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal(" "),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero),
Item::Literal(" GMT"),
];

/// Format time into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
///
/// ## Note
Expand All @@ -88,13 +45,13 @@ const HTTP_DATE: &[Item<'static>] = &[
///
/// - Timezone is fixed to GMT.
/// - Day must be 2 digit.
pub fn format_http_date(t: DateTime) -> String {
t.format_with_items(HTTP_DATE.iter()).to_string()
pub fn format_http_date(t: Timestamp) -> String {
t.strftime("%a, %d %b %Y %T GMT").to_string()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use an explicit RFC 9110 printer instead?

Note that I can't immediately spot anything obviously wrong with what you have here. Probably the dedicated printer is faster, but I'm not sure if that matters given the String alloc here anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know. Updating.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a closer look, I'd prefer the current consistent usage of strftime as it's correct.

Would you elaborate a bit on how switching to the RFC 9110 printer can benefit?

@tisonkun tisonkun Sep 25, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the dedicated printer is faster, but I'm not sure if that matters given the String alloc here anyway

Yeah .. I'd prefer code consistency for understandability unless a break has a strong reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly perf. And I think it makes the code clearer to use a dedicated printer. But that's your call.

}

/// Format time into RFC3339: `2022-03-13T07:20:04Z`
pub fn format_rfc3339(t: DateTime) -> String {
t.to_rfc3339_opts(SecondsFormat::Secs, true)
pub fn format_rfc3339(t: Timestamp) -> String {
t.strftime("%FT%TZ").to_string()
}

/// Parse time from RFC3339.
Expand All @@ -104,22 +61,31 @@ pub fn format_rfc3339(t: DateTime) -> String {
/// - `2022-03-13T07:20:04Z`
/// - `2022-03-01T08:12:34+00:00`
/// - `2022-03-01T08:12:34.00+00:00`
pub fn parse_rfc3339(s: &str) -> crate::Result<DateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(s)
.map_err(|err| {
Error::unexpected(format!("parse '{s}' into rfc3339 failed")).with_source(err)
})?
.with_timezone(&Utc))
pub fn parse_rfc3339(s: &str) -> crate::Result<Timestamp> {
s.parse().map_err(|err| {
Error::unexpected(format!("parse '{s}' into rfc3339 failed")).with_source(err)
})
}

/// Parse time from RFC2822.
///
/// All of them are valid time:
///
/// - `Sat, 13 Jul 2024 15:09:59 -0400`
/// - `Mon, 15 Aug 2022 16:50:12 GMT`
pub fn parse_rfc2822(s: &str) -> crate::Result<Timestamp> {
let zoned = jiff::fmt::rfc2822::parse(s).map_err(|err| {
Error::unexpected(format!("parse '{s}' into rfc2822 failed")).with_source(err)
})?;
Ok(zoned.timestamp())
}

#[cfg(test)]
mod tests {
use chrono::TimeZone;

use super::*;

fn test_time() -> DateTime {
Utc.with_ymd_and_hms(2022, 3, 1, 8, 12, 34).unwrap()
fn test_time() -> Timestamp {
"2022-03-01T08:12:34Z".parse().unwrap()
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion licenserc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@

headerPath = "Apache-2.0-ASF.txt"

includes = ['**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml']
includes = ['**/*.rs', '**/*.yml', '**/*.yaml', '**/*.toml']
2 changes: 1 addition & 1 deletion services/aliyun-oss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
once_cell.workspace = true
percent-encoding.workspace = true
Expand Down
6 changes: 3 additions & 3 deletions services/aliyun-oss/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.

use reqsign_core::time::{now, DateTime};
use reqsign_core::time::{now, Timestamp};
use reqsign_core::utils::Redact;
use reqsign_core::SigningCredential;
use std::fmt::{Debug, Formatter};
Expand All @@ -30,7 +30,7 @@ pub struct Credential {
/// Security token for aliyun services.
pub security_token: Option<String>,
/// Expiration time for this credential.
pub expires_in: Option<DateTime>,
pub expires_in: Option<Timestamp>,
}

impl Debug for Credential {
Expand All @@ -54,7 +54,7 @@ impl SigningCredential for Credential {
// Take 120s as buffer to avoid edge cases.
if let Some(valid) = self
.expires_in
.map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds"))
.map(|v| v > now() + jiff::SignedDuration::from_mins(2))
{
return valid;
}
Expand Down
22 changes: 11 additions & 11 deletions services/aliyun-oss/src/sign_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use http::HeaderValue;
use once_cell::sync::Lazy;
use percent_encoding::utf8_percent_encode;
use reqsign_core::hash::base64_hmac_sha1;
use reqsign_core::time::{format_http_date, now, DateTime};
use reqsign_core::time::{format_http_date, now, Timestamp};
use reqsign_core::Result;
use reqsign_core::{Context, SignRequest};
use std::collections::HashSet;
Expand All @@ -35,7 +35,7 @@ const CONTENT_MD5: &str = "content-md5";
#[derive(Debug)]
pub struct RequestSigner {
bucket: String,
time: Option<DateTime>,
time: Option<Timestamp>,
}

impl RequestSigner {
Expand All @@ -54,12 +54,12 @@ impl RequestSigner {
/// We should always take current time to sign requests.
/// Only use this function for testing.
#[cfg(test)]
pub fn with_time(mut self, time: DateTime) -> Self {
pub fn with_time(mut self, time: Timestamp) -> Self {
self.time = Some(time);
self
}

fn get_time(&self) -> DateTime {
fn get_time(&self) -> Timestamp {
self.time.unwrap_or_else(now)
}
}
Expand Down Expand Up @@ -97,7 +97,7 @@ impl RequestSigner {
&self,
req: &mut http::request::Parts,
cred: &Credential,
signing_time: DateTime,
signing_time: Timestamp,
) -> Result<()> {
let string_to_sign = self.build_string_to_sign(req, cred, signing_time, None)?;
let signature =
Expand Down Expand Up @@ -125,11 +125,11 @@ impl RequestSigner {
&self,
req: &mut http::request::Parts,
cred: &Credential,
signing_time: DateTime,
signing_time: Timestamp,
expires: Duration,
) -> Result<()> {
let expiration_time = signing_time
+ chrono::TimeDelta::from_std(expires).map_err(|e| {
+ jiff::SignedDuration::try_from(expires).map_err(|e| {
reqsign_core::Error::request_invalid(format!("Invalid expiration duration: {e}"))
})?;
let string_to_sign = self.build_string_to_sign(req, cred, signing_time, Some(expires))?;
Expand All @@ -154,7 +154,7 @@ impl RequestSigner {
query_pairs.push(("OSSAccessKeyId".to_string(), cred.access_key_id.clone()));
query_pairs.push((
"Expires".to_string(),
expiration_time.timestamp().to_string(),
expiration_time.as_second().to_string(),
));
query_pairs.push((
"Signature".to_string(),
Expand Down Expand Up @@ -200,7 +200,7 @@ impl RequestSigner {
&self,
req: &http::request::Parts,
cred: &Credential,
signing_time: DateTime,
signing_time: Timestamp,
expires: Option<Duration>,
) -> Result<String> {
let mut s = String::new();
Expand Down Expand Up @@ -229,12 +229,12 @@ impl RequestSigner {
match expires {
Some(expires_duration) => {
let expiration_time = signing_time
+ chrono::TimeDelta::from_std(expires_duration).map_err(|e| {
+ jiff::SignedDuration::try_from(expires_duration).map_err(|e| {
reqsign_core::Error::request_invalid(format!(
"Invalid expiration duration: {e}"
))
})?;
writeln!(&mut s, "{}", expiration_time.timestamp())?;
writeln!(&mut s, "{}", expiration_time.as_second())?;
}
None => {
writeln!(&mut s, "{}", format_http_date(signing_time))?;
Expand Down
2 changes: 1 addition & 1 deletion services/aws-v4/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ name = "aws"
anyhow.workspace = true
async-trait.workspace = true
bytes = "1.7.2"
chrono.workspace = true
form_urlencoded.workspace = true
http.workspace = true
jiff.workspace = true
log.workspace = true
percent-encoding.workspace = true
quick-xml.workspace = true
Expand Down
6 changes: 3 additions & 3 deletions services/aws-v4/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.

use reqsign_core::time::{now, DateTime};
use reqsign_core::time::{now, Timestamp};
use reqsign_core::utils::Redact;
use reqsign_core::SigningCredential;
use std::fmt::{Debug, Formatter};
Expand All @@ -30,7 +30,7 @@ pub struct Credential {
/// Session token for aws services.
pub session_token: Option<String>,
/// Expiration time for this credential.
pub expires_in: Option<DateTime>,
pub expires_in: Option<Timestamp>,
}

impl Debug for Credential {
Expand All @@ -54,7 +54,7 @@ impl SigningCredential for Credential {
// Take 120s as buffer to avoid edge cases.
if let Some(valid) = self
.expires_in
.map(|v| v > now() + chrono::TimeDelta::try_minutes(2).expect("in bounds"))
.map(|v| v > now() + jiff::SignedDuration::from_mins(2))
{
return valid;
}
Expand Down
6 changes: 3 additions & 3 deletions services/aws-v4/src/provide_credential/cognito.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

use crate::Credential;
use async_trait::async_trait;
use chrono::DateTime;
use http::{Method, Request, StatusCode};
use log::debug;
use reqsign_core::time::Timestamp;
use reqsign_core::{Context, Error, ProvideCredential, Result};
use serde::Deserialize;
use serde_json::json;
Expand Down Expand Up @@ -224,8 +224,8 @@ impl CognitoIdentityCredentialProvider {
.map_err(|e| Error::unexpected(format!("failed to parse credentials response: {e}")))?;

let creds = result.credentials;
let expires_in = DateTime::from_timestamp(creds.expiration, 0)
.ok_or_else(|| Error::unexpected("invalid expiration timestamp".to_string()))?;
let expires_in = Timestamp::from_second(creds.expiration)
.map_err(|e| Error::unexpected(format!("invalid expiration date: {e}")))?;

Ok(Credential {
access_key_id: creds.access_key_id,
Expand Down
10 changes: 5 additions & 5 deletions services/aws-v4/src/provide_credential/imds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ use async_trait::async_trait;
use bytes::Bytes;
use http::header::CONTENT_LENGTH;
use http::Method;
use reqsign_core::time::{now, parse_rfc3339, DateTime};
use reqsign_core::time::{now, parse_rfc3339, Timestamp};
use reqsign_core::{Context, Error, ProvideCredential, Result};
use serde::Deserialize;
use std::sync::{Arc, Mutex};

#[derive(Debug, Clone)]
pub struct IMDSv2CredentialProvider {
endpoint: Option<String>,
token: Arc<Mutex<(String, DateTime)>>,
token: Arc<Mutex<(String, Timestamp)>>,
}

impl Default for IMDSv2CredentialProvider {
fn default() -> Self {
Self {
endpoint: None,
token: Arc::new(Mutex::new((String::new(), DateTime::default()))),
token: Arc::new(Mutex::new((String::new(), Timestamp::default()))),
}
}
}
Expand Down Expand Up @@ -105,8 +105,8 @@ impl IMDSv2CredentialProvider {
}
let ec2_token = resp.into_body();
// Set expires_in to 10 minutes to enforce re-read.
let expires_in = now() + chrono::TimeDelta::try_seconds(21600).expect("in bounds")
- chrono::TimeDelta::try_seconds(600).expect("in bounds");
let expires_in =
now() + jiff::SignedDuration::from_secs(21600) - jiff::SignedDuration::from_secs(600);

{
*self.token.lock().expect("lock poisoned") = (ec2_token.clone(), expires_in);
Expand Down
Loading
Loading