Skip to main content

ps_uuid/methods/
new_dcom.rs

1#![allow(clippy::cast_possible_truncation)]
2use std::{
3    ops::BitXor,
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7use crate::{UuidConstructionError, UUID};
8
9impl UUID {
10    /// Creates a new DCOM UUID using the specified timestamp and node ID.
11    ///
12    /// This generates a DCOM UUID following Microsoft's DCOM specification,
13    /// which predates RFC 4122 and uses its own format and algorithms.
14    ///
15    /// # Arguments
16    /// * `timestamp` - The system time to use for the UUID
17    /// * `node_id` - The node ID (6 bytes)
18    ///
19    /// # Errors
20    /// - [`UuidConstructionError`] is returned if `timestamp` is out of range.
21    pub fn new_dcom(
22        timestamp: SystemTime,
23        node_id: [u8; 6],
24    ) -> Result<Self, UuidConstructionError> {
25        // DCOM uses Windows FILETIME format: 100ns intervals since Jan 1, 1601
26        const FILETIME_EPOCH_OFFSET: u64 = 116_444_736_000_000_000;
27
28        let duration_since_unix = timestamp
29            .duration_since(UNIX_EPOCH)
30            .map_err(|_| UuidConstructionError::TimestampBeforeEpoch)?;
31
32        let filetime = u64::try_from(duration_since_unix.as_nanos() / 100)
33            .map_err(|_| UuidConstructionError::TimestampOverflow)?
34            .checked_add(FILETIME_EPOCH_OFFSET)
35            .ok_or(UuidConstructionError::TimestampOverflow)?;
36
37        // DCOM time layout (different from RFC 4122)
38        let time_low = (filetime & 0xFFFF_FFFF) as u32;
39        let time_mid = ((filetime >> 32) & 0xFFFF) as u16;
40        let time_hi_and_version = ((filetime >> 48) & 0xFFFF) as u16;
41
42        // DCOM clock sequence generation
43        let clock_seq = ((filetime & 0xFFFF) as u16)
44            .wrapping_mul(0x1234)
45            .bitxor(((filetime >> 16) & 0xFFFF) as u16);
46
47        Ok(Self::from_parts_dcom(
48            time_low,
49            time_mid,
50            time_hi_and_version,
51            clock_seq,
52            node_id,
53        ))
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    #![allow(clippy::expect_used)]
60    use std::{
61        ops::BitXor,
62        time::{Duration, UNIX_EPOCH},
63    };
64
65    use crate::{UuidConstructionError, Variant, UUID};
66
67    const FILETIME_EPOCH_OFFSET: u64 = 116_444_736_000_000_000;
68
69    const fn sample_node_id() -> [u8; 6] {
70        [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB]
71    }
72
73    #[test]
74    fn test_new_dcom_basic_functionality() {
75        let timestamp = UNIX_EPOCH + Duration::from_secs(1_000_000);
76        let node_id = sample_node_id();
77
78        let result = UUID::new_dcom(timestamp, node_id);
79        assert!(result.is_ok());
80
81        let uuid = result.expect("new_dcom should succeed for valid timestamp and node id");
82        assert_eq!(uuid.get_variant(), Variant::DCOM);
83    }
84
85    #[test]
86    fn test_new_dcom_unix_epoch() {
87        let timestamp = UNIX_EPOCH;
88        let node_id = sample_node_id();
89
90        let result = UUID::new_dcom(timestamp, node_id);
91        assert!(result.is_ok());
92
93        let uuid = result.expect("new_dcom should succeed for valid timestamp and node id");
94        assert_eq!(uuid.get_variant(), Variant::DCOM);
95    }
96
97    #[test]
98    fn test_new_dcom_before_unix_epoch() {
99        let timestamp = UNIX_EPOCH - Duration::from_secs(1);
100        let node_id = sample_node_id();
101
102        let result = UUID::new_dcom(timestamp, node_id);
103        assert!(matches!(
104            result,
105            Err(UuidConstructionError::TimestampBeforeEpoch)
106        ));
107    }
108
109    #[test]
110    fn test_new_dcom_filetime_calculation() {
111        let timestamp = UNIX_EPOCH + Duration::from_secs(1);
112        let node_id = sample_node_id();
113
114        let uuid = UUID::new_dcom(timestamp, node_id)
115            .expect("new_dcom should succeed for valid timestamp and node id");
116
117        // Expected FILETIME: 1 second * 10_000_000 (100ns units) + offset
118        let expected_filetime = 10_000_000 + FILETIME_EPOCH_OFFSET;
119
120        // Extract the timestamp components and verify
121        let time_low =
122            u32::from_le_bytes([uuid.bytes[0], uuid.bytes[1], uuid.bytes[2], uuid.bytes[3]]);
123        let time_mid = u16::from_le_bytes([uuid.bytes[4], uuid.bytes[5]]);
124        let time_hi = u16::from_le_bytes([uuid.bytes[6], uuid.bytes[7]]);
125
126        let reconstructed_filetime =
127            u64::from(time_low) | (u64::from(time_mid) << 32) | (u64::from(time_hi) << 48);
128
129        assert_eq!(reconstructed_filetime, expected_filetime);
130    }
131
132    #[test]
133    fn test_new_dcom_clock_sequence_algorithm() {
134        let timestamp = UNIX_EPOCH + Duration::from_secs(12345);
135        let node_id = sample_node_id();
136
137        let uuid = UUID::new_dcom(timestamp, node_id)
138            .expect("new_dcom should succeed for valid timestamp and node id");
139
140        // Calculate expected clock sequence
141        let filetime = 12345 * 10_000_000 + FILETIME_EPOCH_OFFSET;
142        let expected_clock_seq = ((filetime & 0xFFFF) as u16)
143            .wrapping_mul(0x1234)
144            .bitxor(((filetime >> 16) & 0xFFFF) as u16);
145
146        // Extract clock sequence from UUID (bytes 8-9, big-endian)
147        let actual_clock_seq = u16::from_be_bytes([uuid.bytes[8], uuid.bytes[9]]);
148
149        // Mask to 14 bits (DCOM variant sets upper 2 bits)
150        let actual_clock_seq_masked = actual_clock_seq & 0x3FFF;
151        let expected_clock_seq_masked = expected_clock_seq & 0x3FFF;
152
153        assert_eq!(actual_clock_seq_masked, expected_clock_seq_masked);
154    }
155
156    #[test]
157    fn test_new_dcom_deterministic() {
158        let timestamp = UNIX_EPOCH + Duration::from_millis(123_456_789);
159        let node_id = sample_node_id();
160
161        let uuid1 = UUID::new_dcom(timestamp, node_id)
162            .expect("new_dcom should succeed for valid timestamp and node id");
163        let uuid2 = UUID::new_dcom(timestamp, node_id)
164            .expect("new_dcom should succeed for valid timestamp and node id");
165
166        assert_eq!(uuid1, uuid2);
167    }
168
169    #[test]
170    fn test_new_dcom_different_timestamps() {
171        let node_id = sample_node_id();
172
173        let uuid1 = UUID::new_dcom(UNIX_EPOCH + Duration::from_secs(1), node_id)
174            .expect("new_dcom should succeed for valid timestamp and node id");
175        let uuid2 = UUID::new_dcom(UNIX_EPOCH + Duration::from_secs(2), node_id)
176            .expect("new_dcom should succeed for valid timestamp and node id");
177
178        assert_ne!(uuid1, uuid2);
179    }
180
181    #[test]
182    fn test_new_dcom_different_node_ids() {
183        let timestamp = UNIX_EPOCH + Duration::from_secs(1000);
184        let node_id1 = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
185        let node_id2 = [0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
186
187        let uuid1 = UUID::new_dcom(timestamp, node_id1)
188            .expect("new_dcom should succeed for valid timestamp and node id");
189        let uuid2 = UUID::new_dcom(timestamp, node_id2)
190            .expect("new_dcom should succeed for valid timestamp and node id");
191
192        assert_ne!(uuid1, uuid2);
193
194        // Verify node IDs are correctly set (bytes 10-15)
195        assert_eq!(&uuid1.bytes[10..16], &node_id1);
196        assert_eq!(&uuid2.bytes[10..16], &node_id2);
197    }
198
199    #[test]
200    fn test_new_dcom_nanosecond_precision() {
201        let node_id = sample_node_id();
202
203        // Test with nanosecond precision
204        let timestamp1 = UNIX_EPOCH + Duration::new(1000, 100); // 100 ns
205        let timestamp2 = UNIX_EPOCH + Duration::new(1000, 199); // 199 ns
206
207        let uuid1 = UUID::new_dcom(timestamp1, node_id)
208            .expect("new_dcom should succeed for valid timestamp and node id");
209        let uuid2 = UUID::new_dcom(timestamp2, node_id)
210            .expect("new_dcom should succeed for valid timestamp and node id");
211
212        // Should be the same due to 100ns precision truncation
213        assert_eq!(uuid1, uuid2);
214
215        // But 1000ns difference should be different
216        let timestamp3 = UNIX_EPOCH + Duration::new(1000, 1000); // 1000 ns
217        let uuid3 = UUID::new_dcom(timestamp3, node_id)
218            .expect("new_dcom should succeed for valid timestamp and node id");
219
220        assert_ne!(uuid1, uuid3);
221    }
222
223    #[test]
224    fn test_new_dcom_far_future() {
225        let node_id = sample_node_id();
226
227        // Test with a far future timestamp (year 2100)
228        let far_future = UNIX_EPOCH + Duration::from_secs(365 * 24 * 3600 * 130); // ~130 years
229
230        let result = UUID::new_dcom(far_future, node_id);
231        assert!(result.is_ok());
232
233        let uuid = result.expect("new_dcom should succeed for valid timestamp and node id");
234        assert_eq!(uuid.get_variant(), Variant::DCOM);
235    }
236
237    #[test]
238    fn test_new_dcom_timestamp_overflow() {
239        let node_id = sample_node_id();
240
241        // Create a timestamp that would cause overflow when converted to nanoseconds
242        // This is near the limit of what Duration can represent
243        let max_duration = Duration::new(u64::MAX / 10_000_000, 999_999_999);
244        let overflow_timestamp = UNIX_EPOCH + max_duration;
245
246        let result = UUID::new_dcom(overflow_timestamp, node_id);
247
248        assert_eq!(result, Err(UuidConstructionError::TimestampOverflow));
249    }
250
251    #[test]
252    fn test_new_dcom_all_zero_node_id() {
253        let timestamp = UNIX_EPOCH + Duration::from_secs(1000);
254        let node_id = [0x00; 6];
255
256        let result = UUID::new_dcom(timestamp, node_id);
257        assert!(result.is_ok());
258
259        let uuid = result.expect("new_dcom should succeed for valid timestamp and node id");
260        assert_eq!(&uuid.bytes[10..16], &node_id);
261    }
262
263    #[test]
264    fn test_new_dcom_all_ff_node_id() {
265        let timestamp = UNIX_EPOCH + Duration::from_secs(1000);
266        let node_id = [0xFF; 6];
267
268        let result = UUID::new_dcom(timestamp, node_id);
269        assert!(result.is_ok());
270
271        let uuid = result.expect("new_dcom should succeed for valid timestamp and node id");
272        assert_eq!(&uuid.bytes[10..16], &node_id);
273    }
274
275    #[test]
276    fn test_new_dcom_endianness() {
277        let timestamp = UNIX_EPOCH + Duration::from_secs(0x1234_5678);
278        let node_id = sample_node_id();
279
280        let uuid = UUID::new_dcom(timestamp, node_id)
281            .expect("new_dcom should succeed for valid timestamp and node id");
282
283        // Verify little-endian encoding for time fields
284        let expected_filetime = 0x1234_5678u64 * 10_000_000 + FILETIME_EPOCH_OFFSET;
285
286        // time_low should be little-endian in bytes 0-3
287        let time_low_bytes = (expected_filetime as u32).to_le_bytes();
288        assert_eq!(&uuid.bytes[0..4], &time_low_bytes);
289
290        // time_mid should be little-endian in bytes 4-5
291        let time_mid_bytes = (((expected_filetime >> 32) & 0xFFFF) as u16).to_le_bytes();
292        assert_eq!(&uuid.bytes[4..6], &time_mid_bytes);
293
294        // time_hi should be little-endian in bytes 6-7
295        let time_hi_bytes = (((expected_filetime >> 48) & 0xFFFF) as u16).to_le_bytes();
296        assert_eq!(&uuid.bytes[6..8], &time_hi_bytes);
297    }
298
299    #[test]
300    fn test_new_dcom_variant_bits() {
301        let timestamp = UNIX_EPOCH + Duration::from_secs(1000);
302        let node_id = sample_node_id();
303
304        let uuid = UUID::new_dcom(timestamp, node_id)
305            .expect("new_dcom should succeed for valid timestamp and node id");
306
307        // Verify DCOM variant bits (110) are set in byte 8, bits 7-5
308        let byte_8 = uuid.bytes[8];
309        let variant_bits = (byte_8 & 0xE0) >> 5; // Extract bits 7-5
310        assert_eq!(variant_bits, 0b110); // DCOM variant
311    }
312}