1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
//! `PalmTimestamp` type & conversion methods
//!
//! Palm OS has two epochs used for storing time - the "old Palm epoch" (seconds since 1904-01-01
//! 00:00:00), and the standard UNIX epoch (seconds since 1970-01-01 00:00:00). If the "old Palm
//! epoch" is used for storing the time, it is stored as an _unsigned_ 32-bit integer, but if the
//! UNIX epoch is used, it is stored as a _signed_ 32-bit integer.
//!
//! This module provides the [`PalmTimestamp`] helper type, which can be used within larger data
//! structures to provide automatic timestamp format conversion. This module also provides various
//! helper methods for timestamp format detection, and conversion between the timestamp formats.
use core::{
convert::TryFrom,
fmt::{self, Debug, Display},
};
use chrono::{TimeZone, Utc};
/// The number of seconds between the two Palm OS timestamp epochs.
///
/// Adding this constant to an "old Palm epoch" timestamp will give you a UNIX epoch timestamp.
///
/// The value of this constant differs from the value used in some other Palm OS utilities
/// (specifically, [palmdump](https://www.fourmilab.ch/palm/palmdump)). I am not entirely sure why
/// palmdump uses the value it does. For clarification's sake, here is how the value provided here
/// was calculated (as a Python 3 snippet):
///
/// ```python
/// from datetime import datetime
/// print("%d" % abs(datetime.timestamp(datetime(1904, 1, 1))))
/// ```
pub const SECONDS_BETWEEN_PALM_EPOCHS: u32 = 2082886200;
/// Type representing a Palm OS timestamp
///
/// The raw data contained within this struct can be _either_ of the Palm OS timestamp formats
/// (seconds since the UNIX epoch, or seconds since the "old Palm epoch," see the module
/// documentation for more info), so that this type can be embedded directly within raw data
/// structures.
///
/// To get a usable timestamp from this type, the [`as_unix_ts`][PalmTimestamp::as_unix_ts] method
/// can be called to return the number of seconds since the Unix epoch.
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(C, packed)]
pub struct PalmTimestamp(pub u32);
impl PalmTimestamp {
/// Return the timestamp as the seconds since the UNIX epoch
///
/// If the containing type contains an "old Palm epoch" timestamp, as may be the case if the
/// timestamp was loaded from a PRC/PDB file, this method will perform an implicit conversion
/// to the UNIX epoch.
pub fn as_unix_ts(&self) -> i32 {
if is_palm_epoch(self.0) {
return palm_ts_to_unix_ts(self.0);
}
self.0 as i32
}
/// Return the timestamp as a `strftime`-formatted string
///
/// This uses the formatting specifiers from [`chrono::format::strftime`].
pub fn strftime(&self, format: &str) -> String {
let datetime = Utc.timestamp_opt(self.as_unix_ts() as i64, 0).unwrap();
datetime.format(format).to_string()
}
}
impl Default for PalmTimestamp {
fn default() -> Self {
// 1970-01-01 00:00:00 as a UNIX epoch timestamp
Self(0u32)
}
}
impl Display for PalmTimestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PalmTimestamp({})", self.as_unix_ts())
}
}
/// Check if the given timestamp is using the "old Palm epoch"
///
/// This uses the incredibly simple heuristic of "is the top bit set?" -- this works because the
/// top bit will always be clear if we're dealing with a signed integer (which is the case if the
/// timestamp is using the UNIX epoch), and if the timestamp is using the old Palm epoch, the top
/// bit will always be set if the timestamp is a date occurring after some time in 1972.
///
/// Palm OS definitely wasn't around in 1972, making a Palm database containing a timestamp around
/// this time period extremely unlikely to occur naturally (I mean, bit flipping could happen?), so
/// this is a good enough measure of what timestamp format we're using.
///
/// This is the same heuristic that [palmdump](https://www.fourmilab.ch/palm/palmdump), and many
/// other Palm OS utilites, use for determining the timestamp format.
pub fn is_palm_epoch(timestamp: u32) -> bool {
timestamp & (1 << 31) != 0
}
/// Convert an "old Palm epoch" timestamp to a UNIX epoch timestamp
pub fn palm_ts_to_unix_ts(timestamp: u32) -> i32 {
i32::try_from(timestamp.wrapping_sub(SECONDS_BETWEEN_PALM_EPOCHS))
.expect("integer overflow during timestamp conversion")
}
/// Convert a UNIX epoch timestamp to an "old Palm epoch" timestamp
pub fn unix_ts_to_palm_ts(timestamp: i32) -> u32 {
(timestamp as u32).wrapping_add(SECONDS_BETWEEN_PALM_EPOCHS)
}
#[cfg(test)]
mod tests {
use test_env_log::test;
use super::*;
#[test]
fn is_palm_epoch_detects_unix() {
assert_eq!(is_palm_epoch(0), false);
assert_eq!(is_palm_epoch(0x613f3997), false);
}
#[test]
fn is_palm_epoch_detects_palm() {
assert_eq!(is_palm_epoch(0xb85898b0), true);
assert_eq!(is_palm_epoch(0xdd64ea17), true);
}
#[test]
fn unix_to_palm_to_unix() {
assert_eq!(
1009969200,
palm_ts_to_unix_ts(unix_ts_to_palm_ts(1009969200))
);
}
#[test]
fn palm_to_unix_to_palm() {
assert_eq!(
3092855400,
unix_ts_to_palm_ts(palm_ts_to_unix_ts(3092855400))
);
}
#[test]
fn palmtimestamp_decodes_palm_epoch() {
let ts = PalmTimestamp(3092855400);
assert_eq!(ts.as_unix_ts(), 1009969200);
}
#[test]
fn palmtimestamp_passthru_unix_epoch() {
let ts = PalmTimestamp(1009969200);
assert_eq!(ts.as_unix_ts(), 1009969200);
}
}