zesven 1.1.0

A pure Rust implementation of the 7z archive format
Documentation
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! High-precision timestamp handling.
//!
//! This module provides the [`Timestamp`] type for working with file timestamps
//! stored in 7z archives. Timestamps are stored as Windows FILETIME values,
//! which provide 100-nanosecond precision.
//!
//! # Precision
//!
//! The 7z archive format stores timestamps as Windows FILETIME values:
//! - 64-bit unsigned integer
//! - Counts 100-nanosecond intervals since January 1, 1601 (UTC)
//! - Maximum precision: 100 nanoseconds (not true nanoseconds)
//!
//! When converting to other time representations, be aware of precision loss:
//! - Unix timestamps (seconds): loses sub-second precision
//! - Unix timestamps with microseconds: loses 100ns precision
//! - Unix timestamps with nanoseconds: preserves full precision (as multiples of 100ns)
//!
//! # Example
//!
//! ```rust
//! use zesven::Timestamp;
//!
//! // Create from raw FILETIME
//! let ts = Timestamp::from_filetime(132456789012345678);
//!
//! // Convert to various formats
//! let unix_secs = ts.as_unix_secs();
//! println!("Unix seconds: {}", unix_secs);
//!
//! // Get sub-second components
//! println!("100-nanosecond intervals in second: {}", ts.sub_second_100ns());
//! ```

use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// Windows FILETIME epoch: January 1, 1601 (UTC)
/// Difference from Unix epoch (January 1, 1970) in 100-nanosecond intervals.
const FILETIME_UNIX_DIFF: u64 = 116444736000000000;

/// Number of 100-nanosecond intervals per second.
const INTERVALS_PER_SECOND: u64 = 10_000_000;

/// Number of 100-nanosecond intervals per millisecond.
const INTERVALS_PER_MILLI: u64 = 10_000;

/// Number of 100-nanosecond intervals per microsecond.
const INTERVALS_PER_MICRO: u64 = 10;

/// A high-precision timestamp from a 7z archive.
///
/// Wraps a Windows FILETIME value (100-nanosecond intervals since January 1, 1601)
/// and provides various conversion methods while preserving the original precision.
///
/// # Precision
///
/// FILETIME precision is 100 nanoseconds, not 1 nanosecond. When converting to
/// nanoseconds, values will always be multiples of 100.
///
/// # Example
///
/// ```rust
/// use zesven::Timestamp;
/// use std::time::SystemTime;
///
/// // Unix epoch in FILETIME format
/// let unix_epoch_filetime = 116444736000000000u64;
/// let ts = Timestamp::from_filetime(unix_epoch_filetime);
///
/// assert_eq!(ts.as_unix_secs(), 0);
/// assert_eq!(ts.as_system_time(), SystemTime::UNIX_EPOCH);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp {
    /// Raw FILETIME value (100-nanosecond intervals since 1601-01-01)
    filetime: u64,
}

impl Timestamp {
    /// Creates a timestamp from a raw Windows FILETIME value.
    ///
    /// FILETIME counts 100-nanosecond intervals since January 1, 1601 (UTC).
    #[inline]
    pub const fn from_filetime(filetime: u64) -> Self {
        Self { filetime }
    }

    /// Creates a timestamp from Unix seconds (since January 1, 1970).
    ///
    /// Returns `None` if the timestamp would overflow.
    pub fn from_unix_secs(secs: i64) -> Option<Self> {
        if secs < 0 {
            // Handle times before Unix epoch
            let neg_secs = (-secs) as u64;
            let neg_intervals = neg_secs.checked_mul(INTERVALS_PER_SECOND)?;
            FILETIME_UNIX_DIFF
                .checked_sub(neg_intervals)
                .map(Self::from_filetime)
        } else {
            let intervals = (secs as u64).checked_mul(INTERVALS_PER_SECOND)?;
            FILETIME_UNIX_DIFF
                .checked_add(intervals)
                .map(Self::from_filetime)
        }
    }

    /// Creates a timestamp from Unix seconds and nanoseconds.
    ///
    /// Note: Only 100-nanosecond precision is preserved. The nanoseconds value
    /// is rounded down to the nearest 100ns.
    pub fn from_unix_secs_nanos(secs: i64, nanos: u32) -> Option<Self> {
        // Convert nanos to 100ns intervals (truncating)
        let nano_intervals = (nanos as u64) / 100;

        if secs < 0 {
            let neg_secs = (-secs) as u64;
            let neg_intervals = neg_secs.checked_mul(INTERVALS_PER_SECOND)?;
            let base = FILETIME_UNIX_DIFF.checked_sub(neg_intervals)?;
            // For negative times, we subtract the remaining nanosecond portion
            if nano_intervals > 0 {
                base.checked_add(nano_intervals).map(Self::from_filetime)
            } else {
                Some(Self::from_filetime(base))
            }
        } else {
            let sec_intervals = (secs as u64).checked_mul(INTERVALS_PER_SECOND)?;
            let base = FILETIME_UNIX_DIFF.checked_add(sec_intervals)?;
            base.checked_add(nano_intervals).map(Self::from_filetime)
        }
    }

    /// Creates a timestamp from a `SystemTime`.
    pub fn from_system_time(time: SystemTime) -> Option<Self> {
        match time.duration_since(UNIX_EPOCH) {
            Ok(duration) => {
                Self::from_unix_secs_nanos(duration.as_secs() as i64, duration.subsec_nanos())
            }
            Err(e) => {
                let duration = e.duration();
                Self::from_unix_secs_nanos(-(duration.as_secs() as i64), duration.subsec_nanos())
            }
        }
    }

    /// Returns the raw Windows FILETIME value.
    ///
    /// This is the original value with maximum precision preserved.
    #[inline]
    pub const fn as_filetime(&self) -> u64 {
        self.filetime
    }

    /// Returns the timestamp as Unix seconds.
    ///
    /// Returns negative values for timestamps before January 1, 1970 (Unix epoch).
    /// Sub-second precision is truncated (rounded towards negative infinity for
    /// pre-epoch dates).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zesven::Timestamp;
    ///
    /// // Unix epoch
    /// let ts = Timestamp::from_unix_secs(0).unwrap();
    /// assert_eq!(ts.as_unix_secs(), 0);
    ///
    /// // Before Unix epoch
    /// let ts = Timestamp::from_unix_secs(-3600).unwrap();
    /// assert_eq!(ts.as_unix_secs(), -3600);
    /// ```
    pub fn as_unix_secs(&self) -> i64 {
        if self.filetime >= FILETIME_UNIX_DIFF {
            let intervals = self.filetime - FILETIME_UNIX_DIFF;
            (intervals / INTERVALS_PER_SECOND) as i64
        } else {
            // Before Unix epoch - return negative
            let intervals = FILETIME_UNIX_DIFF - self.filetime;
            let secs = intervals / INTERVALS_PER_SECOND;
            // Round up for negative values if there's a remainder
            let extra = if intervals % INTERVALS_PER_SECOND > 0 {
                1
            } else {
                0
            };
            -((secs + extra) as i64)
        }
    }

    /// Returns the timestamp as Unix milliseconds.
    ///
    /// Preserves millisecond precision. For times before Unix epoch,
    /// returns negative values.
    pub fn as_unix_millis(&self) -> i64 {
        if self.filetime >= FILETIME_UNIX_DIFF {
            let intervals = self.filetime - FILETIME_UNIX_DIFF;
            (intervals / INTERVALS_PER_MILLI) as i64
        } else {
            let intervals = FILETIME_UNIX_DIFF - self.filetime;
            -((intervals / INTERVALS_PER_MILLI) as i64)
        }
    }

    /// Returns the timestamp as Unix microseconds.
    ///
    /// Preserves microsecond precision. For times before Unix epoch,
    /// returns negative values.
    pub fn as_unix_micros(&self) -> i64 {
        if self.filetime >= FILETIME_UNIX_DIFF {
            let intervals = self.filetime - FILETIME_UNIX_DIFF;
            (intervals / INTERVALS_PER_MICRO) as i64
        } else {
            let intervals = FILETIME_UNIX_DIFF - self.filetime;
            -((intervals / INTERVALS_PER_MICRO) as i64)
        }
    }

    /// Returns the timestamp as Unix nanoseconds.
    ///
    /// Note: Values are always multiples of 100 due to FILETIME precision.
    /// For times before Unix epoch, returns negative values.
    pub fn as_unix_nanos(&self) -> i128 {
        if self.filetime >= FILETIME_UNIX_DIFF {
            let intervals = self.filetime - FILETIME_UNIX_DIFF;
            (intervals as i128) * 100
        } else {
            let intervals = FILETIME_UNIX_DIFF - self.filetime;
            -((intervals as i128) * 100)
        }
    }

    /// Converts to a `SystemTime`.
    ///
    /// Preserves full 100-nanosecond precision.
    pub fn as_system_time(&self) -> SystemTime {
        if self.filetime >= FILETIME_UNIX_DIFF {
            let intervals = self.filetime - FILETIME_UNIX_DIFF;
            let secs = intervals / INTERVALS_PER_SECOND;
            let sub_intervals = intervals % INTERVALS_PER_SECOND;
            let nanos = (sub_intervals * 100) as u32;
            UNIX_EPOCH + Duration::new(secs, nanos)
        } else {
            let intervals = FILETIME_UNIX_DIFF - self.filetime;
            let secs = intervals / INTERVALS_PER_SECOND;
            let sub_intervals = intervals % INTERVALS_PER_SECOND;
            let nanos = (sub_intervals * 100) as u32;
            UNIX_EPOCH - Duration::new(secs, nanos)
        }
    }

    /// Returns the sub-second portion as 100-nanosecond intervals (0-9999999).
    ///
    /// This provides the maximum precision available in the FILETIME format.
    #[inline]
    pub fn sub_second_100ns(&self) -> u32 {
        (self.filetime % INTERVALS_PER_SECOND) as u32
    }

    /// Returns the sub-second portion as nanoseconds (0-999999900).
    ///
    /// Note: Values are always multiples of 100 due to FILETIME precision.
    #[inline]
    pub fn sub_second_nanos(&self) -> u32 {
        ((self.filetime % INTERVALS_PER_SECOND) * 100) as u32
    }

    /// Returns the sub-second portion as microseconds (0-999999).
    #[inline]
    pub fn sub_second_micros(&self) -> u32 {
        ((self.filetime % INTERVALS_PER_SECOND) / INTERVALS_PER_MICRO) as u32
    }

    /// Returns the sub-second portion as milliseconds (0-999).
    #[inline]
    pub fn sub_second_millis(&self) -> u32 {
        ((self.filetime % INTERVALS_PER_SECOND) / INTERVALS_PER_MILLI) as u32
    }

    /// Returns true if this timestamp is before the Unix epoch.
    #[inline]
    pub fn is_before_unix_epoch(&self) -> bool {
        self.filetime < FILETIME_UNIX_DIFF
    }

    /// Returns true if this timestamp is at or after the Unix epoch.
    #[inline]
    pub fn is_at_or_after_unix_epoch(&self) -> bool {
        self.filetime >= FILETIME_UNIX_DIFF
    }
}

impl Default for Timestamp {
    /// Returns the Unix epoch (January 1, 1970).
    fn default() -> Self {
        Self::from_filetime(FILETIME_UNIX_DIFF)
    }
}

impl From<u64> for Timestamp {
    fn from(filetime: u64) -> Self {
        Self::from_filetime(filetime)
    }
}

impl From<Timestamp> for u64 {
    fn from(ts: Timestamp) -> u64 {
        ts.filetime
    }
}

impl From<Timestamp> for SystemTime {
    fn from(ts: Timestamp) -> SystemTime {
        ts.as_system_time()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_unix_epoch() {
        let ts = Timestamp::from_filetime(FILETIME_UNIX_DIFF);
        assert_eq!(ts.as_unix_secs(), 0);
        assert_eq!(ts.as_unix_millis(), 0);
        assert_eq!(ts.as_unix_micros(), 0);
        assert_eq!(ts.as_unix_nanos(), 0);
        assert_eq!(ts.as_system_time(), UNIX_EPOCH);
        assert!(!ts.is_before_unix_epoch());
        assert!(ts.is_at_or_after_unix_epoch());
    }

    #[test]
    fn test_sub_second_precision() {
        // Unix epoch + 1.5 seconds (5000000 100-ns intervals)
        let ts = Timestamp::from_filetime(FILETIME_UNIX_DIFF + 15_000_000);

        assert_eq!(ts.as_unix_secs(), 1);
        assert_eq!(ts.sub_second_100ns(), 5_000_000);
        assert_eq!(ts.sub_second_nanos(), 500_000_000);
        assert_eq!(ts.sub_second_micros(), 500_000);
        assert_eq!(ts.sub_second_millis(), 500);
    }

    #[test]
    fn test_from_unix_secs() {
        let ts = Timestamp::from_unix_secs(0).unwrap();
        assert_eq!(ts.as_filetime(), FILETIME_UNIX_DIFF);

        let ts = Timestamp::from_unix_secs(1).unwrap();
        assert_eq!(ts.as_filetime(), FILETIME_UNIX_DIFF + INTERVALS_PER_SECOND);

        // Negative (before Unix epoch)
        let ts = Timestamp::from_unix_secs(-1).unwrap();
        assert_eq!(ts.as_filetime(), FILETIME_UNIX_DIFF - INTERVALS_PER_SECOND);
    }

    #[test]
    fn test_from_unix_secs_nanos() {
        // 1 second + 500ms
        let ts = Timestamp::from_unix_secs_nanos(1, 500_000_000).unwrap();
        assert_eq!(ts.as_unix_secs(), 1);
        assert_eq!(ts.sub_second_millis(), 500);
    }

    #[test]
    fn test_roundtrip_system_time() {
        let original = UNIX_EPOCH + Duration::new(1234567890, 123_456_700);
        let ts = Timestamp::from_system_time(original).unwrap();
        let recovered = ts.as_system_time();
        assert_eq!(original, recovered);
    }

    #[test]
    fn test_100ns_precision() {
        // Create timestamp with specific 100ns value
        let ts = Timestamp::from_filetime(FILETIME_UNIX_DIFF + 123);

        // Should preserve the 100ns precision
        assert_eq!(ts.sub_second_100ns(), 123);
        assert_eq!(ts.sub_second_nanos(), 12300);
    }

    #[test]
    fn test_before_unix_epoch() {
        // 1 day before Unix epoch
        let day_in_intervals = 24 * 60 * 60 * INTERVALS_PER_SECOND;
        let ts = Timestamp::from_filetime(FILETIME_UNIX_DIFF - day_in_intervals);

        assert!(ts.is_before_unix_epoch());
        assert_eq!(ts.as_unix_secs(), -86400);
    }

    #[test]
    fn test_conversions() {
        let ts: Timestamp = 132456789012345678u64.into();
        let back: u64 = ts.into();
        assert_eq!(back, 132456789012345678);
    }

    #[test]
    fn test_default() {
        let ts = Timestamp::default();
        assert_eq!(ts.as_unix_secs(), 0);
    }
}