From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <mira@nena.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 4A5B8936B9
 for <pmg-devel@lists.proxmox.com>; Tue, 20 Feb 2024 11:07:40 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id 2664B420A
 for <pmg-devel@lists.proxmox.com>; Tue, 20 Feb 2024 11:07:10 +0100 (CET)
Received: from nena.proxmox.com (unknown [94.136.29.99])
 by firstgate.proxmox.com (Proxmox) with ESMTP
 for <pmg-devel@lists.proxmox.com>; Tue, 20 Feb 2024 11:07:09 +0100 (CET)
Received: by nena.proxmox.com (Postfix, from userid 1000)
 id 3D717CC0BB; Tue, 20 Feb 2024 11:07:08 +0100 (CET)
From: Mira Limbeck <m.limbeck@proxmox.com>
To: pmg-devel@lists.proxmox.com
Date: Tue, 20 Feb 2024 11:06:44 +0100
Message-Id: <20240220100648.44119-1-m.limbeck@proxmox.com>
X-Mailer: git-send-email 2.39.2
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL -0.641 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
 KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery
 methods
 RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_NONE                0.001 SPF: sender does not publish an SPF Record
 T_SCC_BODY_TEXT_LINE    -0.01 -
 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See
 http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more
 information. [time.rs, main.rs]
Subject: [pmg-devel] [PATCH v3 pmg-log-tracker 1/5] rfc3339: move timezone
 offset compatibility code to old time parser
X-BeenThere: pmg-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Mail Gateway development discussion
 <pmg-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pmg-devel>, 
 <mailto:pmg-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pmg-devel/>
List-Post: <mailto:pmg-devel@lists.proxmox.com>
List-Help: <mailto:pmg-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel>, 
 <mailto:pmg-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Tue, 20 Feb 2024 10:07:40 -0000

The compatibility code was added to the new rfc3339 code path temporarily so
that the old code path would not be changed before the PMG 8 release.

Now move it to the old time format code to make sure the rfc3339 code path
works as expected. Since we have all the information we need (year, month,
day, hours, minutes, seconds, timezone), there's no need for a workaround in
this code path.

As a side effect of parsing the time format `YYYY-MM-DD HH:MM:SS` in
localtime, it's now possible to parse an rfc3339 compliant format
(except for fractional seconds).

The change needs to be accompanied by one in pmg-api MailTracker.pmg to
keep the time displayed in the GUI the same for the old time format, and
correct for the new rfc3339 format.

Signed-off-by: Mira Limbeck <m.limbeck@proxmox.com>
---
I've tested it locally with both old timestamp format and new one, but
running it on more logs would not hurt

Since we use only the start time to calculate the timezone offset, it
will not be reliable in all cases, especially when the end time offset
differs from the start time.

v3:
 - changed file in range of start/end time check to use the start time
   timezone offset, which allowed us to remove the timezone_offset
   member of the parser struct

v2:
 - removed TODO line
 - fixed start-/endtime offset mentioned by @stoiko on the rfc
 - added rfc3339 start-/endtime handling as suggested by @dominik since
   it eases parsing the YYYY-MM-DD HH:MM:SS format as localtime rather
   than UTC
 - added tests in another patch

 src/main.rs | 65 ++++++++++++++++++++++++++++++-----------------------
 src/time.rs | 23 ++++++++++++++++++-
 2 files changed, 59 insertions(+), 29 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index e7aeeb4..5d4f86e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use std::io::BufReader;
 use std::io::BufWriter;
 use std::io::Write;
 
-use anyhow::{bail, Error};
+use anyhow::{bail, Context, Error};
 use flate2::read;
 use libc::time_t;
 
@@ -90,12 +90,12 @@ fn main() -> Result<(), Error> {
 
     println!(
         "# Start: {} ({})",
-        time::strftime(c_str!("%F %T"), &parser.start_tm)?,
+        proxmox_time::strftime_local("%F %T", parser.options.start)?,
         parser.options.start
     );
     println!(
         "# End: {} ({})",
-        time::strftime(c_str!("%F %T"), &parser.end_tm)?,
+        proxmox_time::strftime_local("%F %T", parser.options.end)?,
         parser.options.end
     );
 
@@ -1681,8 +1681,6 @@ struct Parser {
     string_match: bool,
 
     lines: u64,
-
-    timezone_offset: time_t,
 }
 
 impl Parser {
@@ -1708,7 +1706,6 @@ impl Parser {
             ctime: 0,
             string_match: false,
             lines: 0,
-            timezone_offset: ltime.tm_gmtoff,
         })
     }
 
@@ -1787,7 +1784,11 @@ impl Parser {
                 line,
                 self.current_year,
                 self.current_month,
-                self.timezone_offset,
+                // use start time for timezone offset in parse_time_no_year rather than the
+                // timezone offset of the current time
+                // this is required for cases where current time is in standard time, while start
+                // time is in summer time or the other way around
+                self.start_tm.tm_gmtoff,
             ) {
                 Some(t) => t,
                 None => continue,
@@ -1876,7 +1877,7 @@ impl Parser {
                             &buffer[0..size],
                             self.current_year,
                             self.current_month,
-                            self.timezone_offset,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             // found the earliest file in the time frame
                             if time < self.options.start {
@@ -1896,7 +1897,7 @@ impl Parser {
                             &buffer[0..size],
                             self.current_year,
                             self.current_month,
-                            self.timezone_offset,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             if time < self.options.start {
                                 break;
@@ -1920,12 +1921,14 @@ impl Parser {
         }
 
         if let Some(start) = args.opt_value_from_str::<_, String>(["-s", "--starttime"])? {
-            if let Ok(res) = time::strptime(&start, c_str!("%F %T")) {
-                self.options.start = res.as_utc_to_epoch();
-                self.start_tm = res;
-            } else if let Ok(res) = time::strptime(&start, c_str!("%s")) {
-                self.options.start = res.as_utc_to_epoch();
-                self.start_tm = res;
+            if let Ok(epoch) = proxmox_time::parse_rfc3339(&start).or_else(|_| {
+                time::date_to_rfc3339(&start).and_then(|s| proxmox_time::parse_rfc3339(&s))
+            }) {
+                self.options.start = epoch;
+                self.start_tm = time::Tm::at_local(epoch).context("failed to parse start time")?;
+            } else if let Ok(epoch) = start.parse::<time_t>() {
+                self.options.start = epoch;
+                self.start_tm = time::Tm::at_local(epoch).context("failed to parse start time")?;
             } else {
                 bail!("failed to parse start time");
             }
@@ -1939,12 +1942,14 @@ impl Parser {
         }
 
         if let Some(end) = args.opt_value_from_str::<_, String>(["-e", "--endtime"])? {
-            if let Ok(res) = time::strptime(&end, c_str!("%F %T")) {
-                self.options.end = res.as_utc_to_epoch();
-                self.end_tm = res;
-            } else if let Ok(res) = time::strptime(&end, c_str!("%s")) {
-                self.options.end = res.as_utc_to_epoch();
-                self.end_tm = res;
+            if let Ok(epoch) = proxmox_time::parse_rfc3339(&end).or_else(|_| {
+                time::date_to_rfc3339(&end).and_then(|s| proxmox_time::parse_rfc3339(&s))
+            }) {
+                self.options.end = epoch;
+                self.end_tm = time::Tm::at_local(epoch).context("failed to parse end time")?;
+            } else if let Ok(epoch) = end.parse::<time_t>() {
+                self.options.end = epoch;
+                self.end_tm = time::Tm::at_local(epoch).context("failed to parse end time")?;
             } else {
                 bail!("failed to parse end time");
             }
@@ -2196,11 +2201,11 @@ fn parse_time(
     cur_month: i64,
     timezone_offset: time_t,
 ) -> Option<(time_t, &'_ [u8])> {
-    parse_time_with_year(data, timezone_offset)
-        .or_else(|| parse_time_no_year(data, cur_year, cur_month))
+    parse_time_with_year(data)
+        .or_else(|| parse_time_no_year(data, cur_year, cur_month, timezone_offset))
 }
 
-fn parse_time_with_year(data: &'_ [u8], timezone_offset: time_t) -> Option<(time_t, &'_ [u8])> {
+fn parse_time_with_year(data: &'_ [u8]) -> Option<(time_t, &'_ [u8])> {
     let mut timestamp_buffer = [0u8; 25];
 
     let count = data.iter().take_while(|b| **b != b' ').count();
@@ -2227,13 +2232,17 @@ fn parse_time_with_year(data: &'_ [u8], timezone_offset: time_t) -> Option<(time
     match proxmox_time::parse_rfc3339(unsafe {
         std::str::from_utf8_unchecked(&timestamp_buffer[0..timestamp_len])
     }) {
-        // TODO handle timezone offset in old code path instead
-        Ok(ltime) => Some((ltime + timezone_offset, data)),
+        Ok(ltime) => Some((ltime, data)),
         Err(_err) => None,
     }
 }
 
-fn parse_time_no_year(data: &'_ [u8], cur_year: i64, cur_month: i64) -> Option<(time_t, &'_ [u8])> {
+fn parse_time_no_year(
+    data: &'_ [u8],
+    cur_year: i64,
+    cur_month: i64,
+    timezone_offset: time_t,
+) -> Option<(time_t, &'_ [u8])> {
     if data.len() < 15 {
         return None;
     }
@@ -2338,7 +2347,7 @@ fn parse_time_no_year(data: &'_ [u8], cur_year: i64, cur_month: i64) -> Option<(
         _ => &data[1..],
     };
 
-    Some((ltime, data))
+    Some((ltime - timezone_offset, data))
 }
 
 type ByteSlice<'a> = &'a [u8];
diff --git a/src/time.rs b/src/time.rs
index 5851496..9a2ce73 100644
--- a/src/time.rs
+++ b/src/time.rs
@@ -149,7 +149,8 @@ pub fn strftime(format: &CStr, tm: &Tm) -> Result<String, Error> {
 
 /// Wrapper around `strptime(3)` to parse time strings.
 pub fn strptime(time: &str, format: &CStr) -> Result<Tm, Error> {
-    let mut out = MaybeUninit::<libc::tm>::uninit();
+    // zero memory because strptime does not necessarily initialize tm_isdst, tm_gmtoff and tm_zone
+    let mut out = MaybeUninit::<libc::tm>::zeroed();
 
     let time = CString::new(time).map_err(|_| format_err!("time string contains nul bytes"))?;
 
@@ -167,3 +168,23 @@ pub fn strptime(time: &str, format: &CStr) -> Result<Tm, Error> {
 
     Ok(Tm(unsafe { out.assume_init() }))
 }
+
+pub fn date_to_rfc3339(date: &str) -> Result<String, Error> {
+    // parse the YYYY-MM-DD HH:MM:SS format for the timezone info
+    let ltime = strptime(date, c_str!("%F %T"))?;
+
+    // strptime assume it is in UTC, but we want to interpret it as local time and get the timezone
+    // offset (tm_gmtoff) which we can then append to be able to parse it as rfc3339
+    let ltime = Tm::at_local(ltime.as_utc_to_epoch())?;
+
+    let mut s = date.replacen(' ', "T", 1);
+    if ltime.tm_gmtoff != 0 {
+        let sign = if ltime.tm_gmtoff < 0 { '-' } else { '+' };
+        let minutes = (ltime.tm_gmtoff / 60) % 60;
+        let hours = ltime.tm_gmtoff / (60 * 60);
+        s.push_str(&format!("{}{:02}:{:02}", sign, hours, minutes));
+    } else {
+        s.push('Z');
+    }
+    Ok(s)
+}
-- 
2.39.2