Skip to main content

ps_uuid/methods/
get_timestamp.rs

1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3use crate::{Gregorian, UUID};
4
5// FILETIME epoch: 1601-01-01T00:00:00Z
6const FILETIME_EPOCH_OFFSET: u64 = 116_444_736_000_000_000;
7
8// NCS epoch: 1980-01-01T00:00:00Z
9const NCS_EPOCH: Duration = Duration::from_secs(315_532_800);
10
11impl UUID {
12    /// Extract the embedded timestamp as a `SystemTime`, if present.
13    ///
14    /// Returns `None` if the UUID does not encode a timestamp.
15    #[must_use]
16    pub fn get_timestamp(&self) -> Option<SystemTime> {
17        match (self.get_version(), self.get_variant()) {
18            // v1/v2: 60-bit timestamp, 100ns intervals since 1582-10-15
19            (Some(1 | 2), crate::Variant::OSF) => {
20                let time_low = u32::from_be_bytes([
21                    self.bytes[0],
22                    self.bytes[1],
23                    self.bytes[2],
24                    self.bytes[3],
25                ]);
26                let time_mid = u16::from_be_bytes([self.bytes[4], self.bytes[5]]);
27                let time_hi = u16::from_be_bytes([self.bytes[6], self.bytes[7]]) & 0x0FFF;
28                let timestamp: u64 =
29                    (u64::from(time_hi) << 48) | (u64::from(time_mid) << 32) | u64::from(time_low);
30
31                #[allow(clippy::cast_possible_truncation)]
32                Some(
33                    Gregorian::epoch()
34                        + Duration::new(
35                            timestamp / 10_000_000,
36                            ((timestamp % 10_000_000) * 100) as u32,
37                        ),
38                )
39            }
40            // v6: 60-bit timestamp, 100ns intervals since 1582-10-15, reordered
41            (Some(6), crate::Variant::OSF) => {
42                let time_high = u32::from_be_bytes([
43                    self.bytes[0],
44                    self.bytes[1],
45                    self.bytes[2],
46                    self.bytes[3],
47                ]);
48                let time_mid = u16::from_be_bytes([self.bytes[4], self.bytes[5]]);
49                let time_low = u16::from_be_bytes([self.bytes[6], self.bytes[7]]) & 0x0FFF;
50                let timestamp: u64 = (u64::from(time_high) << 28)
51                    | (u64::from(time_mid) << 12)
52                    | u64::from(time_low);
53                Some(Gregorian::epoch() + Duration::from_nanos(timestamp * 100))
54            }
55            // v7: 48-bit Unix ms timestamp, bytes 0..6
56            (Some(7), crate::Variant::OSF) => {
57                let mut ms_bytes = [0u8; 8];
58                ms_bytes[2..8].copy_from_slice(&self.bytes[0..6]);
59                let ms = u64::from_be_bytes(ms_bytes);
60                Some(UNIX_EPOCH + Duration::from_millis(ms))
61            }
62            // DCOM: FILETIME, 100ns since 1601-01-01, little-endian
63            (_, crate::Variant::DCOM) => {
64                // time_low: bytes 0..4 (LE), time_mid: 4..6 (LE), time_hi: 6..8 (LE)
65                let time_low = u32::from_le_bytes([
66                    self.bytes[0],
67                    self.bytes[1],
68                    self.bytes[2],
69                    self.bytes[3],
70                ]);
71                let time_mid = u16::from_le_bytes([self.bytes[4], self.bytes[5]]);
72                let time_hi = u16::from_le_bytes([self.bytes[6], self.bytes[7]]);
73                let filetime: u64 =
74                    (u64::from(time_hi) << 48) | (u64::from(time_mid) << 32) | u64::from(time_low);
75
76                #[allow(clippy::cast_possible_truncation)]
77                if filetime < FILETIME_EPOCH_OFFSET {
78                    let unix_100ns = FILETIME_EPOCH_OFFSET - filetime;
79
80                    Some(
81                        UNIX_EPOCH
82                            - Duration::new(
83                                unix_100ns / 10_000_000,
84                                (unix_100ns % 10_000_000) as u32 * 100,
85                            ),
86                    )
87                } else {
88                    let unix_100ns = filetime - FILETIME_EPOCH_OFFSET;
89
90                    Some(
91                        UNIX_EPOCH
92                            + Duration::new(
93                                unix_100ns / 10_000_000,
94                                (unix_100ns % 10_000_000) as u32 * 100,
95                            ),
96                    )
97                }
98            }
99            // NCS: 48-bit timestamp, 4μs units since 1980-01-01, big-endian
100            (_, crate::Variant::NCS) => {
101                // timestamp: bytes 0..6 (BE)
102                let mut ts_bytes = [0u8; 8];
103                ts_bytes[2..8].copy_from_slice(&self.bytes[0..6]);
104                let ts = u64::from_be_bytes(ts_bytes);
105
106                let ncs_epoch = UNIX_EPOCH + NCS_EPOCH;
107                Some(ncs_epoch + Duration::from_micros(ts * 4))
108            }
109            _ => None,
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    #![allow(clippy::cast_possible_truncation, clippy::expect_used)]
117    use crate::Variant;
118
119    use super::*;
120    use std::time::{Duration, UNIX_EPOCH};
121
122    #[test]
123    fn v1_and_v2_timestamp_roundtrip() {
124        let t = Gregorian::epoch() + Duration::from_secs(1_000_000_000);
125        let uuid = UUID::from_parts_v1(0x6fc1_0000, 0x86f2, 0x23, 0x1234, [1, 2, 3, 4, 5, 6]);
126        let ts = uuid
127            .get_timestamp()
128            .expect("timestamp should be present for time-based UUID");
129        assert_eq!(ts, t, "v1 timestamp roundtrip failed");
130    }
131
132    #[test]
133    fn v6_timestamp_roundtrip() {
134        let t = Gregorian::epoch() + Duration::from_secs(1_000_000_000);
135        let uuid = UUID::from_parts_v6(0x0238_6f26, 0xfc10, 0x6000, 0x1234, [1, 2, 3, 4, 5, 6]);
136        let ts = uuid
137            .get_timestamp()
138            .expect("timestamp should be present for time-based UUID");
139        assert_eq!(ts, t, "v6 timestamp roundtrip failed");
140    }
141
142    #[test]
143    fn v7_timestamp_roundtrip() {
144        let ms = 1_700_000_000_000u64;
145        let uuid = UUID::from_parts_v7(ms, 0, 0);
146        let ts = uuid
147            .get_timestamp()
148            .expect("timestamp should be present for time-based UUID");
149        let expected = UNIX_EPOCH + Duration::from_millis(ms);
150        assert_eq!(ts, expected, "v7 timestamp roundtrip failed");
151    }
152
153    #[test]
154    fn dcom_timestamp_roundtrip() {
155        // FILETIME: 100ns since 1601-01-01
156        let t = UNIX_EPOCH + Duration::from_secs(1_000_000_000);
157        let node = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
158        let uuid = UUID::new_dcom(t, node).expect("new_dcom should succeed for valid test inputs");
159        let ts = uuid
160            .get_timestamp()
161            .expect("timestamp should be present for time-based UUID");
162        assert_eq!(ts, t, "DCOM timestamp roundtrip failed");
163    }
164
165    #[test]
166    fn ncs_timestamp_roundtrip() {
167        // NCS epoch: 1980-01-01
168        let ncs_epoch = UNIX_EPOCH + Duration::from_secs(315_532_800);
169        let t = ncs_epoch + Duration::from_secs(1_000_000);
170        let address = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
171        let uuid =
172            UUID::new_ncs(t, 1, &address).expect("new_ncs should succeed for valid test inputs");
173        let ts = uuid
174            .get_timestamp()
175            .expect("timestamp should be present for time-based UUID");
176        assert_eq!(ts, t, "NCS timestamp roundtrip failed");
177    }
178
179    #[test]
180    fn non_time_based_versions_return_none() {
181        for v in [3, 4, 5, 8] {
182            let mut bytes = [0u8; 16];
183            bytes[6] = v << 4;
184            let uuid = UUID::from_parts_v8(bytes);
185            assert!(
186                uuid.get_timestamp().is_none(),
187                "Non-time-based v{v} should return None"
188            );
189        }
190    }
191
192    #[test]
193    fn v1_from_bytes_and_timestamp() {
194        let mut bytes = [0u8; 16];
195        bytes[6] = 0x10; // version 1 (time-based)
196        bytes[8] = 0x80; // RFC 4122 variant (10xx xxxx)
197        let uuid = UUID::from_bytes(bytes);
198        assert_eq!(uuid.as_bytes(), &bytes);
199        assert_eq!(uuid.get_version(), Some(1));
200        assert!(uuid.get_timestamp().is_some());
201    }
202
203    #[test]
204    fn v2_from_bytes_and_timestamp() {
205        let mut bytes = [0u8; 16];
206        bytes[6] = 0x20; // version 2 (DCE security)
207        bytes[8] = 0x80; // RFC 4122 variant
208        let uuid = UUID::from_bytes(bytes);
209        assert_eq!(uuid.as_bytes(), &bytes);
210        assert_eq!(uuid.get_version(), Some(2));
211        assert!(uuid.get_timestamp().is_some());
212    }
213
214    #[test]
215    fn v3_from_bytes_and_timestamp() {
216        let mut bytes = [0u8; 16];
217        bytes[6] = 0x30; // version 3 (name-based MD5)
218        bytes[8] = 0x80; // RFC 4122 variant
219        let uuid = UUID::from_bytes(bytes);
220        assert_eq!(uuid.as_bytes(), &bytes);
221        assert_eq!(uuid.get_version(), Some(3));
222        assert_eq!(uuid.get_timestamp(), None);
223    }
224
225    #[test]
226    fn v4_from_bytes_and_timestamp() {
227        let mut bytes = [0u8; 16];
228        bytes[6] = 0x40; // version 4 (random)
229        bytes[8] = 0x80; // RFC 4122 variant
230        let uuid = UUID::from_bytes(bytes);
231        assert_eq!(uuid.as_bytes(), &bytes);
232        assert_eq!(uuid.get_version(), Some(4));
233        assert_eq!(uuid.get_timestamp(), None);
234    }
235
236    #[test]
237    fn v5_from_bytes_and_timestamp() {
238        let mut bytes = [0u8; 16];
239        bytes[6] = 0x50; // version 5 (name-based SHA-1)
240        bytes[8] = 0x80; // RFC 4122 variant
241        let uuid: UUID = UUID::from_bytes(bytes);
242        assert_eq!(uuid.as_bytes(), &bytes);
243        assert_eq!(uuid.get_version(), Some(5));
244        assert_eq!(uuid.get_timestamp(), None);
245    }
246
247    // ---------- variant tests -------------------------------------------------
248
249    #[test]
250    fn variant_ncs() {
251        let mut bytes = [0u8; 16];
252        bytes[6] = 0x40; // any version; keep v4 for consistency
253        bytes[8] = 0x00; // NCS variant (0xxx xxxx)
254        let uuid = UUID::from_bytes(bytes);
255        assert_eq!(uuid.as_bytes(), &bytes);
256        assert_eq!(uuid.get_variant(), Variant::NCS);
257    }
258
259    #[test]
260    fn variant_rfc4122() {
261        let mut bytes = [0u8; 16];
262        bytes[6] = 0x40; // version 4
263        bytes[8] = 0x80; // RFC 4122 variant (10xx xxxx)
264        let uuid = UUID::from_bytes(bytes);
265        assert_eq!(uuid.as_bytes(), &bytes);
266        assert_eq!(uuid.get_variant(), Variant::OSF);
267    }
268
269    #[test]
270    fn variant_microsoft() {
271        let mut bytes = [0u8; 16];
272        bytes[6] = 0x40; // version 4
273        bytes[8] = 0xC0; // Microsoft variant (110x xxxx)
274        let uuid = UUID::from_bytes(bytes);
275        assert_eq!(uuid.as_bytes(), &bytes);
276        assert_eq!(uuid.get_variant(), Variant::DCOM);
277    }
278
279    #[test]
280    fn variant_future() {
281        let mut bytes = [0u8; 16];
282        bytes[6] = 0x40; // version 4
283        bytes[8] = 0xE0; // Future variant (111x xxxx)
284        let uuid = UUID::from_bytes(bytes);
285        assert_eq!(uuid.as_bytes(), &bytes);
286        assert_eq!(uuid.get_variant(), Variant::Reserved);
287    }
288
289    // UUID (and Microsoft file-time) tick = 100 ns
290    const HUNDRED_NS_PER_SEC: u64 = 10_000_000;
291
292    // Difference 1582-10-15 → 1970-01-01 in 100 ns ticks
293    const UUID_UNIX_TICKS: u64 = 0x01B2_1DD2_1381_4000; // 122192928000000000
294
295    // Difference 1970-01-01 → 1980-01-01 in whole seconds
296    const SECS_1970_TO_1980: u64 = 315_532_800;
297
298    // ---------- version 1, 2, and 6 (100 ns since Gregorian epoch) ----------
299
300    #[test]
301    fn v1_timestamp_exact_unix_epoch() {
302        let ticks = UUID_UNIX_TICKS;
303        let time_low = (ticks & 0xFFFF_FFFF) as u32;
304        let time_mid = ((ticks >> 32) & 0xFFFF) as u16;
305        let time_hi_ver = (((ticks >> 48) & 0x0FFF) as u16) | 0x1000; // ver = 1
306
307        let mut b = [0u8; 16];
308        b[0] = (time_low >> 24) as u8;
309        b[1] = (time_low >> 16) as u8;
310        b[2] = (time_low >> 8) as u8;
311        b[3] = time_low as u8;
312        b[4] = (time_mid >> 8) as u8;
313        b[5] = time_mid as u8;
314        b[6] = (time_hi_ver >> 8) as u8;
315        b[7] = time_hi_ver as u8;
316        b[8] = 0x80; // RFC-4122 variant
317        let uuid = UUID::from_bytes(b);
318        assert_eq!(uuid.get_timestamp(), Some(SystemTime::UNIX_EPOCH));
319    }
320
321    #[test]
322    fn v2_timestamp_exact_unix_epoch() {
323        let ticks = UUID_UNIX_TICKS;
324        let time_low = (ticks & 0xFFFF_FFFF) as u32;
325        let time_mid = ((ticks >> 32) & 0xFFFF) as u16;
326        let time_hi_ver = (((ticks >> 48) & 0x0FFF) as u16) | 0x2000; // ver = 2
327
328        let mut b = [0u8; 16];
329        b[0] = (time_low >> 24) as u8;
330        b[1] = (time_low >> 16) as u8;
331        b[2] = (time_low >> 8) as u8;
332        b[3] = time_low as u8;
333        b[4] = (time_mid >> 8) as u8;
334        b[5] = time_mid as u8;
335        b[6] = (time_hi_ver >> 8) as u8;
336        b[7] = time_hi_ver as u8;
337        b[8] = 0x80;
338        let uuid = UUID::from_bytes(b);
339        assert_eq!(uuid.get_timestamp(), Some(SystemTime::UNIX_EPOCH));
340    }
341
342    #[test]
343    fn v6_timestamp_exact_unix_epoch() {
344        let ticks = UUID_UNIX_TICKS;
345        let hi = (ticks >> 28) as u32;
346        let mid = ((ticks >> 12) & 0xFFFF) as u16;
347        let lo_ver = ((ticks & 0x0FFF) as u16) | 0x6000; // ver = 6
348
349        let mut b = [0u8; 16];
350        b[0] = (hi >> 24) as u8;
351        b[1] = (hi >> 16) as u8;
352        b[2] = (hi >> 8) as u8;
353        b[3] = hi as u8;
354        b[4] = (mid >> 8) as u8;
355        b[5] = mid as u8;
356        b[6] = (lo_ver >> 8) as u8;
357        b[7] = lo_ver as u8;
358        b[8] = 0x80;
359        let uuid = UUID::from_bytes(b);
360        assert_eq!(uuid.get_timestamp(), Some(SystemTime::UNIX_EPOCH));
361    }
362
363    // ------------------------- version 7 (msec since UNIX) -------------------
364
365    #[test]
366    fn v7_timestamp_zero() {
367        // first 48 bits (bytes 0-5 & low nibble of byte 6) are zero
368        let mut b = [0u8; 16];
369        b[6] = 0x70; // set version 7
370        b[8] = 0x80; // RFC-4122 variant
371        let uuid = UUID::from_bytes(b);
372        assert_eq!(uuid.get_timestamp(), Some(SystemTime::UNIX_EPOCH));
373    }
374
375    // ------------------------- NCS variant (4 µs since 1980) ------------------
376
377    #[test]
378    fn ncs_timestamp_epoch() {
379        // ticks = 0 → 1980-01-01
380        let uuid = UUID::from_bytes([0u8; 16]); // variant bit = 0
381        let expected = SystemTime::UNIX_EPOCH + Duration::from_secs(SECS_1970_TO_1980);
382        assert_eq!(uuid.get_timestamp(), Some(expected));
383    }
384
385    // --------------------- Microsoft / DCOM variant (file-time) --------------
386
387    #[test]
388    fn dcom_timestamp_exact_unix_epoch() {
389        // 100 ns ticks from 1601-01-01 to 1970-01-01
390        let ticks = 11_644_473_600u64 * HUNDRED_NS_PER_SEC; // = 116444736000000000
391        let lo = (ticks & 0xFFFF_FFFF) as u32;
392        let mid = ((ticks >> 32) & 0xFFFF) as u16;
393        let hi = ((ticks >> 48) & 0xFFFF) as u16;
394
395        let mut b = [0u8; 16];
396        // Little-endian encoding for DCOM UUIDs
397        b[0] = lo as u8;
398        b[1] = (lo >> 8) as u8;
399        b[2] = (lo >> 16) as u8;
400        b[3] = (lo >> 24) as u8;
401        b[4] = mid as u8;
402        b[5] = (mid >> 8) as u8;
403        b[6] = hi as u8;
404        b[7] = (hi >> 8) as u8;
405        b[8] = 0xC0; // Microsoft variant (110x xxxx)
406        let uuid = UUID::from_bytes(b);
407        assert_eq!(uuid.get_timestamp(), Some(SystemTime::UNIX_EPOCH));
408    }
409}