use crate::error::Result;
use crate::ole::container::{OleEntryWithTimestamp, OleFile};
#[derive(Debug, Clone)]
pub struct TimestampEntry {
pub path: String,
pub is_stream: bool,
pub size: u64,
pub created: Option<String>,
pub modified: Option<String>,
}
const FILETIME_UNIX_DIFF: u64 = 11_644_473_600;
fn format_systemtime(st: &std::time::SystemTime) -> String {
let secs = match st.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_) => return "(before epoch)".to_string(),
};
let (year, month, day, hour, min, sec) = unix_secs_to_datetime(secs);
format!("{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}")
}
fn unix_secs_to_datetime(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
let sec = secs % 60;
let min = (secs / 60) % 60;
let hour = (secs / 3600) % 24;
let mut days = secs / 86400;
let mut year = 1970u64;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let month_days = if is_leap_year(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 0u64;
for (i, &md) in month_days.iter().enumerate() {
if days < md {
month = i as u64 + 1;
break;
}
days -= md;
}
if month == 0 {
month = 12;
}
let day = days + 1;
(year, month, day, hour, min, sec)
}
fn is_leap_year(year: u64) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
pub fn filetime_to_string(filetime: u64) -> String {
if filetime == 0 {
return "(not set)".to_string();
}
let secs_since_1601 = filetime / 10_000_000;
if secs_since_1601 < FILETIME_UNIX_DIFF {
return "(before Unix epoch)".to_string();
}
let unix_secs = secs_since_1601 - FILETIME_UNIX_DIFF;
let (year, month, day, hour, min, sec) = unix_secs_to_datetime(unix_secs);
format!("{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}")
}
pub fn extract_timestamps(ole: &OleFile) -> Vec<TimestampEntry> {
let entries = ole.list_entries_with_timestamps();
entries
.into_iter()
.map(|e: OleEntryWithTimestamp| TimestampEntry {
path: e.path,
is_stream: e.is_stream,
size: e.size,
created: e.created.map(|t| format_systemtime(&t)),
modified: e.modified.map(|t| format_systemtime(&t)),
})
.collect()
}
pub fn extract_timestamps_from_bytes(data: &[u8]) -> Result<Vec<TimestampEntry>> {
let ole = OleFile::from_bytes(data)?;
Ok(extract_timestamps(&ole))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filetime_zero() {
assert_eq!(filetime_to_string(0), "(not set)");
}
#[test]
fn test_filetime_before_unix_epoch() {
let ft = 100_000_000u64; assert_eq!(filetime_to_string(ft), "(before Unix epoch)");
}
#[test]
fn test_filetime_known_date() {
let unix_ts: u64 = 1_577_836_800;
let ft = (unix_ts + FILETIME_UNIX_DIFF) * 10_000_000;
let result = filetime_to_string(ft);
assert_eq!(result, "2020-01-01 00:00:00");
}
#[test]
fn test_format_systemtime() {
use std::time::{Duration, UNIX_EPOCH};
let st = UNIX_EPOCH + Duration::from_secs(1_686_832_245);
let result = format_systemtime(&st);
assert_eq!(result, "2023-06-15 12:30:45");
}
#[test]
fn test_unix_epoch() {
let (y, m, d, h, min, s) = unix_secs_to_datetime(0);
assert_eq!((y, m, d, h, min, s), (1970, 1, 1, 0, 0, 0));
}
#[test]
fn test_leap_year() {
assert!(is_leap_year(2000));
assert!(is_leap_year(2024));
assert!(!is_leap_year(1900));
assert!(!is_leap_year(2023));
}
#[test]
fn test_invalid_ole_data() {
let result = extract_timestamps_from_bytes(&[0x00, 0x01, 0x02]);
assert!(result.is_err());
}
#[test]
fn test_empty_data() {
let result = extract_timestamps_from_bytes(&[]);
assert!(result.is_err());
}
#[test]
fn test_format_systemtime_before_epoch() {
use std::time::{Duration, UNIX_EPOCH};
let st = UNIX_EPOCH - Duration::from_secs(1);
let result = format_systemtime(&st);
assert_eq!(result, "(before epoch)");
}
}