oletools_rs 0.1.0

Rust port of oletools — analysis tools for Microsoft Office files (VBA macros, DDE, OLE objects, RTF exploits)
Documentation
//! OLE timestamp extraction and formatting.
//!
//! Extracts creation and modification timestamps from OLE2 container entries
//! and converts Windows FILETIME values to human-readable strings.

use crate::error::Result;
use crate::ole::container::{OleEntryWithTimestamp, OleFile};

/// A timestamp entry extracted from an OLE file.
#[derive(Debug, Clone)]
pub struct TimestampEntry {
    /// Path within the OLE container.
    pub path: String,
    /// Whether this is a stream (data) or storage (directory).
    pub is_stream: bool,
    /// Size in bytes.
    pub size: u64,
    /// Creation time as formatted string, if available.
    pub created: Option<String>,
    /// Modification time as formatted string, if available.
    pub modified: Option<String>,
}

/// Windows epoch (1601-01-01) to Unix epoch (1970-01-01) offset in seconds.
const FILETIME_UNIX_DIFF: u64 = 11_644_473_600;

/// Format a `SystemTime` as "YYYY-MM-DD HH:MM:SS".
///
/// Uses arithmetic conversion — no external date/time dependency.
fn format_systemtime(st: &std::time::SystemTime) -> String {
    // Convert to seconds since Unix epoch
    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}")
}

/// Convert Unix timestamp (seconds) to (year, month, day, hour, minute, second).
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;

    // Days since Unix epoch
    let mut days = secs / 86400;

    // Compute year
    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;
    }

    // Compute month
    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)
}

/// Check if a year is a leap year.
fn is_leap_year(year: u64) -> bool {
    (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}

/// Convert a Windows FILETIME value (100-nanosecond intervals since 1601-01-01)
/// to a formatted string.
pub fn filetime_to_string(filetime: u64) -> String {
    if filetime == 0 {
        return "(not set)".to_string();
    }

    // Convert 100-ns intervals to seconds, then subtract epoch diff
    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}")
}

/// Extract timestamps from all entries in an OLE file.
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()
}

/// Extract timestamps from raw OLE file data.
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() {
        // A FILETIME value before 1970-01-01
        let ft = 100_000_000u64; // very early date
        assert_eq!(filetime_to_string(ft), "(before Unix epoch)");
    }

    #[test]
    fn test_filetime_known_date() {
        // 2020-01-01 00:00:00 UTC
        // Unix timestamp: 1577836800
        // FILETIME = (1577836800 + 11644473600) * 10_000_000
        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};
        // 2023-06-15 12:30:45 UTC
        // Verify by computing: 2023-01-01 = 1672531200
        // + 31(Jan)+28(Feb)+31(Mar)+30(Apr)+31(May)+14(Jun) = 165 days = 14256000
        // + 12*3600 + 30*60 + 45 = 45045
        // = 1672531200 + 14256000 + 45045 = 1686832245
        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)");
    }
}