From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pbs-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id C11F81FF17F for <inbox@lore.proxmox.com>; Mon, 19 May 2025 13:47:49 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A1E00890F; Mon, 19 May 2025 13:47:39 +0200 (CEST) From: Christian Ebner <c.ebner@proxmox.com> To: pbs-devel@lists.proxmox.com Date: Mon, 19 May 2025 13:46:10 +0200 Message-Id: <20250519114640.303640-10-c.ebner@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250519114640.303640-1-c.ebner@proxmox.com> References: <20250519114640.303640-1-c.ebner@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC proxmox-backup 09/39] s3 client: add helper to parse http date headers X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion <pbs-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/> List-Post: <mailto:pbs-devel@lists.proxmox.com> List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Backup Server development discussion <pbs-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com> Add a helper to parse the preferred date/time format for http `Date` headers as specified in RFC 2616 [0], which is a fixed-length subset of the format specified in RFC 1123 [1], itself being a followup to RFC 822 [2]. Does not implement the format as described in the obsolete RFC 850 [3]. This allows to parse the `Date` and `Last-Modified` headers of S3 API responses. [0] https://datatracker.ietf.org/doc/html/rfc2616#section-3.3 [1] https://datatracker.ietf.org/doc/html/rfc1123#section-5.2.14 [2] https://datatracker.ietf.org/doc/html/rfc822#section-5 [3] https://datatracker.ietf.org/doc/html/rfc850 Signed-off-by: Christian Ebner <c.ebner@proxmox.com> --- pbs-s3-client/src/lib.rs | 97 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/pbs-s3-client/src/lib.rs b/pbs-s3-client/src/lib.rs index 308db64d8..00fa26455 100644 --- a/pbs-s3-client/src/lib.rs +++ b/pbs-s3-client/src/lib.rs @@ -6,7 +6,12 @@ pub use object_key::{S3ObjectKey, S3_CONTENT_PREFIX}; use std::time::Duration; -use anyhow::{bail, Error}; +use anyhow::{bail, Context, Error}; + +const VALID_DAYS_OF_WEEK: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +const VALID_MONTHS: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; #[derive(Debug)] pub struct LastModifiedTimestamp { @@ -121,3 +126,93 @@ impl std::str::FromStr for LastModifiedTimestamp { } serde_plain::derive_deserialize_from_fromstr!(LastModifiedTimestamp, "last modified timestamp"); + +/// Preferred date format specified by RFC2616, given as fixed-length +/// subset of RFC1123, which itself is a followup to RFC822. +/// +/// https://datatracker.ietf.org/doc/html/rfc2616#section-3.3 +/// https://datatracker.ietf.org/doc/html/rfc1123#section-5.2.14 +/// https://datatracker.ietf.org/doc/html/rfc822#section-5 +#[derive(Debug)] +pub struct HttpDate { + epoch: i64, +} + +impl HttpDate { + pub fn to_duration(&self) -> Result<Duration, Error> { + let seconds = u64::try_from(self.epoch)?; + Ok(Duration::from_secs(seconds)) + } +} + +impl std::str::FromStr for HttpDate { + type Err = Error; + + fn from_str(timestamp: &str) -> Result<Self, Self::Err> { + let input = timestamp.as_bytes(); + if input.len() != 29 { + bail!("unexpected length: got {}", input.len()); + } + + let expect = |pos: usize, c: u8| { + if input[pos] != c { + bail!("unexpected char at pos {pos}"); + } + Ok(()) + }; + + let digit = |pos: usize| -> Result<i32, Error> { + let digit = input[pos] as i32; + if !(48..=57).contains(&digit) { + bail!("unexpected char at pos {pos}"); + } + Ok(digit - 48) + }; + + fn check_max(i: i32, max: i32) -> Result<i32, Error> { + if i > max { + bail!("value too large ({i} > {max})"); + } + Ok(i) + } + + let mut tm = proxmox_time::TmEditor::new(true); + + if !VALID_DAYS_OF_WEEK + .iter() + .any(|valid| valid.as_bytes() == &input[0..3]) + { + bail!("unexpected day of week, got {:?}", &input[0..3]); + } + + expect(3, b',').context("unexpected separator after day of week")?; + expect(4, b' ').context("missing space after day of week separator")?; + tm.set_mday(check_max(digit(5)? * 10 + digit(6)?, 31)?)?; + expect(7, b' ').context("unexpected separator after day")?; + if let Some(month) = VALID_MONTHS + .iter() + .position(|month| month.as_bytes() == &input[8..11]) + { + // valid conversion to i32, position stems from fixed size array of 12 months. + tm.set_mon(check_max(month as i32 + 1, 12)?)?; + } else { + bail!("invalid month"); + } + expect(11, b' ').context("unexpected separator after month")?; + tm.set_year(digit(12)? * 1000 + digit(13)? * 100 + digit(14)? * 10 + digit(15)?)?; + expect(16, b' ').context("unexpected separator after year")?; + tm.set_hour(check_max(digit(17)? * 10 + digit(18)?, 23)?)?; + expect(19, b':').context("unexpected separator after hour")?; + tm.set_min(check_max(digit(20)? * 10 + digit(21)?, 59)?)?; + expect(22, b':').context("unexpected separator after minute")?; + tm.set_sec(check_max(digit(23)? * 10 + digit(24)?, 60)?)?; + expect(25, b' ').context("unexpected separator after second")?; + if !input.ends_with(b"GMT") { + bail!("unexpected timezone"); + } + + let epoch = tm.into_epoch()?; + + Ok(Self { epoch }) + } +} -- 2.39.5 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel