tnid/lib.rs
1//! # TNID
2//!
3//! **T**ype-checked, **N**amed **ID**s that are fully compatible with UUIDs.
4//!
5//! TNID ("Typed Named ID", pronounced "tee-nid") embeds a human-readable name directly into
6//! a UUID-compatible 128-bit identifier. Each TNID carries a 1-4 character type name (like
7//! "user" or "post") that's validated at compile time, preventing you from accidentally
8//! mixing up IDs for different entity types.
9//!
10//! # Quick Start
11//!
12//! ```rust
13//! use tnid::{Tnid, TnidName, NameStr};
14//!
15//! // Define a name type for your entity
16//! struct User;
17//! impl TnidName for User {
18//! const ID_NAME: NameStr<'static> = NameStr::new_const("user");
19//! }
20//!
21//! // Generate IDs
22//! let id = Tnid::<User>::new_v0(); // Time-ordered (like UUIDv7)
23//!
24//! // Display as a TNID string
25//! println!("{}", id); // e.g., "user.Br2flcNDfF6LYICnT"
26//!
27//! // Or as a standard UUID for database storage
28//! let uuid_str = id.to_uuid_string_cased(false);
29//! // e.g., "cab1952a-f09d-86d9-928e-96ea03dc6af3"
30//! ```
31//!
32//! # Why TNIDs?
33//!
34//! TNIDs solve several common problems with plain UUIDs:
35//!
36//! - **Type Safety**: `Tnid<User>` and `Tnid<Post>` are different types. You can't accidentally
37//! pass a post ID where a user ID is expected.
38//! - **Human Readable**: The TNID string format (`user.Br2flcNDfF6LYICnT`) tells you at a
39//! glance what kind of entity the ID refers to.
40//! - **UUID Compatible**: Every TNID is a valid UUIDv8, so it works with existing UUID columns
41//! in databases and UUID-based APIs.
42//!
43//! # TNID Variants
44//!
45//! Like UUID versions, TNIDs come in different variants:
46//!
47//! - **V0** ([`Tnid::new_v0`]) - Time-ordered with millisecond precision (like UUIDv7).
48//! Use when you want IDs to sort chronologically.
49//! - **V1** ([`Tnid::new_v1`]) - High-entropy random (like UUIDv4).
50//! Use when you want maximum randomness without time information.
51//!
52//! See [`TnidVariant`] for details.
53//!
54//! # String Representations
55//!
56//! TNIDs support two string formats:
57//!
58//! - **TNID format** (`user.Br2flcNDfF6LYICnT`): Human-readable, sortable, unambiguous
59//! - **UUID format** (`cab1952a-f09d-86d9-928e-96ea03dc6af3`): Standard UUID hex for compatibility
60//!
61//! ```rust
62//! # use tnid::{Tnid, TnidName, NameStr};
63//! # struct User;
64//! # impl TnidName for User {
65//! # const ID_NAME: NameStr<'static> = NameStr::new_const("user");
66//! # }
67//! let id = Tnid::<User>::new_v1();
68//!
69//! // TNID string (for APIs, logs, user-facing contexts)
70//! let tnid_str = id.as_tnid_string();
71//!
72//! // UUID string (for databases, UUID-based systems)
73//! let uuid_str = id.to_uuid_string_cased(false);
74//!
75//! // Parse back
76//! let from_tnid = Tnid::<User>::parse_tnid_string(&tnid_str);
77//! let from_uuid = Tnid::<User>::parse_uuid_string(&uuid_str);
78//! ```
79//!
80//! # Feature Flags
81//!
82//! | Feature | Default | Description |
83//! |---------|---------|-------------|
84//! | `time` | ✓ | Enables [`Tnid::new_v0`] with automatic timestamps |
85//! | `rand` | ✓ | Enables [`Tnid::new_v0`] and [`Tnid::new_v1`] with automatic randomness |
86//! | `encryption` | | Enables [`Tnid::encrypt_v0_to_v1`] and [`Tnid::decrypt_v1_to_v0`] for hiding timestamps |
87//! | `uuid` | | Integration with the [`uuid`](https://docs.rs/uuid) crate |
88//!
89//! Without the default features, you can still create TNIDs using explicit components:
90//! - [`Tnid::new_v0_with_parts`] - Provide your own timestamp and random bits
91//! - [`Tnid::new_v1_with_random`] - Provide your own random bits
92//!
93//! # Reliability
94//!
95//! This crate is designed to never panic in normal use. All functions that could potentially
96//! panic are fuzz tested with randomized inputs to verify they don't. Extensive unit tests
97//! verify correctness of encoding, sorting, and round-trip conversions.
98
99#![cfg_attr(docsrs, feature(doc_cfg))]
100#![deny(unsafe_code)]
101#![deny(clippy::unwrap_used)]
102#![deny(clippy::indexing_slicing)]
103#![deny(rustdoc::broken_intra_doc_links)]
104#![warn(missing_docs)]
105
106use std::marker::PhantomData;
107
108mod data_encoding;
109#[cfg(feature = "encryption")]
110pub mod encryption;
111mod name_encoding;
112mod tnid_variant;
113mod utils;
114#[cfg(feature = "uuid")]
115mod uuid;
116mod uuidlike;
117mod v0;
118mod v1;
119
120pub use name_encoding::NameStr;
121pub use tnid_variant::TnidVariant;
122pub use uuidlike::UUIDLike;
123
124/// Intended to be used on empty structs to create type checked TNID names.
125///
126/// ```rust
127/// # use tnid::TnidName;
128/// # use tnid::Tnid;
129/// # use tnid::NameStr;
130///
131/// struct ExampleName;
132/// impl TnidName for ExampleName {
133/// const ID_NAME: NameStr<'static> = NameStr::new_const("exna");
134/// }
135///
136/// # let _ = Tnid::<ExampleName>::new_v0();
137/// ```
138///
139/// [`NameStr::new_const`] validates the name at compile time and is the only way to create
140/// a `NameStr<'static>`, ensuring all [`Tnid`] names are valid.
141/// ```rust,compile_fail
142/// # use tnid::TnidName;
143/// # use tnid::Tnid;
144/// # use tnid::NameStr;
145///
146/// struct InvalidName;
147/// impl TnidName for InvalidName {
148/// const ID_NAME: NameStr<'static> = NameStr::new_const("2long");
149/// }
150///
151/// # let _ = Tnid::<InvalidName>::new_v0();
152/// ```
153pub trait TnidName {
154 /// Must be overridden with the name of your ID
155 const ID_NAME: NameStr<'static>;
156}
157
158/// A type-safe TNID parameterized by name.
159///
160/// The type parameter uses the [`TnidName`] trait to enforce compile-time checking of names.
161/// `Tnid<User>` and `Tnid<Post>` are distinct types that cannot be mixed.
162///
163/// All validation happens at construction time, so any `Tnid<Name>` instance is guaranteed
164/// to be valid. If you need to work with potentially invalid 128-bit values, use [`UUIDLike`]
165/// for inspection without validation.
166#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
167pub struct Tnid<Name: TnidName> {
168 id_name: PhantomData<Name>,
169 id: u128,
170}
171
172impl<Name: TnidName> Copy for Tnid<Name> {}
173
174impl<Name: TnidName> Clone for Tnid<Name> {
175 fn clone(&self) -> Self {
176 *self
177 }
178}
179
180impl<Name: TnidName> Tnid<Name> {
181 /// Returns the name associated with this TNID type.
182 ///
183 /// The name comes from the [`TnidName`] implementation for this type and is
184 /// used in the TNID string representation (`<name>.<data>`).
185 ///
186 /// # Examples
187 ///
188 /// ```rust
189 /// use tnid::{Tnid, TnidName, NameStr};
190 ///
191 /// struct User;
192 /// impl TnidName for User {
193 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
194 /// }
195 ///
196 /// let id = Tnid::<User>::new_v0();
197 /// assert_eq!(id.name(), "user");
198 /// ```
199 pub fn name(&self) -> &'static str {
200 Name::ID_NAME.as_str()
201 }
202
203 /// Returns the hex representation of the name field (20 bits as 5 hex characters).
204 ///
205 /// The ASCII representation of a name (like "test") is different from the hex
206 /// representation of those bits when viewing a TNID in hex format. This method shows
207 /// what the name looks like as hex, which is how it appears in TNID hex strings.
208 ///
209 /// This is useful for understanding what the name portion looks like in the hex
210 /// representation without needing a specific TNID instance.
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use tnid::{Tnid, TnidName, NameStr};
216 ///
217 /// struct Test;
218 /// impl TnidName for Test {
219 /// const ID_NAME: NameStr<'static> = NameStr::new_const("test");
220 /// }
221 ///
222 /// // Check what "test" looks like in hex (any TNID instance works)
223 /// let id = Tnid::<Test>::new_v1();
224 /// assert_eq!(id.name_hex(), "cab19");
225 /// ```
226 pub fn name_hex(&self) -> String {
227 let hex = format!("{:05x}", self.id >> 108);
228
229 debug_assert_eq!(hex.len(), 5);
230
231 hex
232 }
233
234 /// Returns the raw 128-bit UUIDv8-compatible representation of this TNID.
235 ///
236 /// This returns the complete bit representation including the name, UUID version/variant
237 /// bits, TNID variant, and all data bits.
238 ///
239 /// # Endianness
240 ///
241 /// The UUID specification dictates that [UUIDs are stored in big-endian](https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-format) byte order.
242 /// When storing or transmitting this `u128` value as bytes, you may need to convert
243 /// to big-endian format using methods like [`u128::to_be_bytes()`] since `u128` uses
244 /// the platform's native endianness.
245 ///
246 /// # Examples
247 ///
248 /// ```rust
249 /// use tnid::{Tnid, TnidName, NameStr};
250 ///
251 /// struct User;
252 /// impl TnidName for User {
253 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
254 /// }
255 ///
256 /// let id = Tnid::<User>::new_v0();
257 /// let as_u128 = id.as_u128();
258 ///
259 /// // Convert to big-endian bytes for storage/transmission
260 /// let bytes = as_u128.to_be_bytes();
261 /// ```
262 pub fn as_u128(&self) -> u128 {
263 self.id
264 }
265
266 /// Generates a new time-ordered TNID (alias for [`Self::new_v0`]).
267 ///
268 /// This variant is sortable by creation time, similar to UUIDv7. TNIDs created earlier
269 /// will sort before those created later in all representations (u128, UUID hex, TNID string).
270 ///
271 /// Use this when you need time-based sorting, similar to choosing UUIDv7 over UUIDv4.
272 pub fn new_time_ordered() -> Self {
273 Self::new_v0()
274 }
275
276 /// Generates a new v0 TNID.
277 ///
278 /// This variant is time-ordered with millisecond precision, similar to UUIDv7.
279 /// TNIDs created earlier will sort before those created later in all representations
280 /// (u128, UUID hex, and TNID string). The remaining bits are filled with random data.
281 ///
282 /// Use this when you need time-based sorting and want IDs to be roughly chronological,
283 /// similar to choosing UUIDv7 over UUIDv4.
284 #[cfg(feature = "time")]
285 pub fn new_v0() -> Self {
286 Self::new_v0_with_time(time::OffsetDateTime::now_utc())
287 }
288
289 /// Generates a new TNID with maximum randomness (alias for [`Self::new_v1`]).
290 ///
291 /// This variant maximizes entropy with 100 bits of random data, similar to UUIDv4
292 /// but with slightly less entropy due to the 20-bit name field. This is almost
293 /// certainly sufficient for most use cases.
294 ///
295 /// Use this when you don't need time-based sorting and want maximum randomness,
296 /// similar to choosing UUIDv4 over UUIDv7.
297 ///
298 /// # Examples
299 ///
300 /// ```rust
301 /// use tnid::{Tnid, TnidName, NameStr};
302 ///
303 /// struct User;
304 /// impl TnidName for User {
305 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
306 /// }
307 ///
308 /// let id = Tnid::<User>::new_high_entropy();
309 /// ```
310 #[cfg(feature = "rand")]
311 pub fn new_high_entropy() -> Self {
312 Self::new_v1()
313 }
314
315 /// Generates a new v1 TNID.
316 ///
317 /// This variant maximizes entropy with 100 bits of random data, similar to UUIDv4.
318 /// This is almost certainly sufficient for most use cases.
319 ///
320 /// Use this when you don't need time-based sorting and want maximum randomness,
321 /// similar to choosing UUIDv4 over UUIDv7.
322 #[cfg(feature = "rand")]
323 pub fn new_v1() -> Self {
324 Self::new_v1_with_random(rand::random())
325 }
326
327 /// Generates a new high-entropy TNID (v1) from explicit random bits.
328 ///
329 /// This creates a v1 TNID without requiring the `rand` feature dependency,
330 /// allowing you to provide your own randomness source. This is useful in
331 /// environments where the `rand` library may not be suitable (embedded systems,
332 /// WASM, or when you need a custom random source).
333 ///
334 /// # Parameters
335 ///
336 /// - `random_bits`: Random bits for the TNID. Only 100 bits are used, but
337 /// accepting a `u128` makes it easier to provide randomness.
338 ///
339 /// # Examples
340 ///
341 /// ```rust
342 /// use tnid::{Tnid, TnidName, NameStr};
343 ///
344 /// struct User;
345 /// impl TnidName for User {
346 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
347 /// }
348 ///
349 /// // Provide your own randomness
350 /// let random_bits = 0x0123456789ABCDEF0123456789ABCDEF;
351 ///
352 /// let id = Tnid::<User>::new_v1_with_random(random_bits);
353 /// ```
354 pub fn new_v1_with_random(random_bits: u128) -> Self {
355 let id_name = Name::ID_NAME;
356
357 let id = v1::make_from_parts(id_name, random_bits);
358
359 Self {
360 id_name: PhantomData,
361 id,
362 }
363 }
364
365 /// Generates a new time-ordered TNID (v0) with a specific timestamp.
366 ///
367 /// This creates the same time-sortable TNID as [`Self::new_v0`], but allows you to
368 /// provide a specific timestamp instead of using the current time. The timestamp is
369 /// converted to milliseconds since the Unix epoch and encoded into the TNID.
370 ///
371 /// TNIDs created with earlier timestamps will sort before those with later timestamps
372 /// in all representations (u128, UUID hex, and TNID string).
373 ///
374 /// # Examples
375 ///
376 /// ```rust
377 /// use tnid::{Tnid, TnidName, NameStr};
378 /// use time::OffsetDateTime;
379 ///
380 /// struct User;
381 /// impl TnidName for User {
382 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
383 /// }
384 ///
385 /// let timestamp = OffsetDateTime::now_utc();
386 /// let id = Tnid::<User>::new_v0_with_time(timestamp);
387 /// ```
388 ///
389 /// Demonstrating time-based sorting:
390 /// ```rust
391 /// use tnid::{Tnid, TnidName, NameStr};
392 /// use time::{OffsetDateTime, Duration};
393 ///
394 /// struct User;
395 /// impl TnidName for User {
396 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
397 /// }
398 ///
399 /// let now = OffsetDateTime::now_utc();
400 /// let earlier = now - Duration::hours(1);
401 /// let later = now + Duration::hours(1);
402 ///
403 /// let id1 = Tnid::<User>::new_v0_with_time(earlier);
404 /// let id2 = Tnid::<User>::new_v0_with_time(now);
405 /// let id3 = Tnid::<User>::new_v0_with_time(later);
406 ///
407 /// // Earlier times sort before later times
408 /// assert!(id1.as_u128() < id2.as_u128());
409 /// assert!(id2.as_u128() < id3.as_u128());
410 /// ```
411 #[cfg(all(feature = "rand", feature = "time"))]
412 pub fn new_v0_with_time(time: time::OffsetDateTime) -> Self {
413 let id_name = Name::ID_NAME;
414
415 let epoch_millis = (time.unix_timestamp_nanos() / 1000 / 1000) as u64;
416
417 let random_bits: u64 = rand::random();
418
419 let id = v0::make_from_parts(id_name, epoch_millis, random_bits);
420
421 Self {
422 id_name: PhantomData,
423 id,
424 }
425 }
426
427 /// Generates a new time-ordered TNID (v0) from explicit components.
428 ///
429 /// This creates a v0 TNID without requiring the `time` or `rand` feature dependencies,
430 /// allowing you to provide your own timestamp and randomness sources. This is useful
431 /// in environments where those libraries may not be suitable (embedded systems, WASM,
432 /// or when you need custom time/random sources).
433 ///
434 /// # Parameters
435 ///
436 /// - `epoch_millis`: Milliseconds since the Unix epoch (January 1, 1970 UTC)
437 /// - `random`: Random bits for the TNID (57 bits will be used)
438 ///
439 /// # Examples
440 ///
441 /// ```rust
442 /// use tnid::{Tnid, TnidName, NameStr};
443 ///
444 /// struct User;
445 /// impl TnidName for User {
446 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
447 /// }
448 ///
449 /// // Provide your own timestamp and randomness
450 /// let timestamp_ms = 1750118400000;
451 /// let random_bits = 42;
452 ///
453 /// let id = Tnid::<User>::new_v0_with_parts(timestamp_ms, random_bits);
454 /// ```
455 pub fn new_v0_with_parts(epoch_millis: u64, random: u64) -> Self {
456 Self {
457 id_name: PhantomData,
458 id: v0::make_from_parts(Name::ID_NAME, epoch_millis, random),
459 }
460 }
461
462 /// Returns the TNID string representation.
463 ///
464 /// This representation has several advantages over the UUID hex format:
465 /// - **Unambiguous**: Unlike UUID hex strings which are case-insensitive, TNID strings
466 /// are case-sensitive with exactly one valid representation
467 /// - **Sortable**: For v0 TNIDs, the string representation maintains time-ordering
468 /// - **Human-readable name**: The name prefix makes it easy to identify the ID type
469 ///
470 /// The format is `<name>.<encoded-data>`, where the data is encoded using the TNID
471 /// Data Encoding that preserves these sortability and uniqueness properties.
472 ///
473 /// # Examples
474 ///
475 /// ```rust
476 /// use tnid::{Tnid, TnidName, NameStr};
477 ///
478 /// struct User;
479 /// impl TnidName for User {
480 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
481 /// }
482 ///
483 /// let id = Tnid::<User>::new_v0();
484 /// let tnid_string = id.as_tnid_string();
485 ///
486 /// // Format: <name>.<encoded-data>
487 /// // Example: "user.Br2flcNDfF6LYICnT"
488 /// assert!(tnid_string.starts_with("user."));
489 /// ```
490 pub fn as_tnid_string(&self) -> String {
491 format!(
492 "{}.{}",
493 self.name(),
494 data_encoding::id_data_to_string(self.id)
495 )
496 }
497
498 /// Returns the TNID variant.
499 ///
500 /// # Examples
501 ///
502 /// ```rust
503 /// use tnid::{Tnid, TnidName, NameStr, TnidVariant};
504 ///
505 /// struct User;
506 /// impl TnidName for User {
507 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
508 /// }
509 ///
510 /// let id_v0 = Tnid::<User>::new_v0();
511 /// assert_eq!(id_v0.variant(), TnidVariant::V0);
512 ///
513 /// let id_v1 = Tnid::<User>::new_v1();
514 /// assert_eq!(id_v1.variant(), TnidVariant::V1);
515 /// ```
516 pub fn variant(&self) -> TnidVariant {
517 let variant_bits = (self.id >> 60) as u8;
518
519 TnidVariant::from_u8(variant_bits)
520 }
521
522 /// Converts the TNID to UUID hex string format.
523 ///
524 /// This is useful for UUID compatibility and interoperability with systems that expect
525 /// standard UUID format, or any other case where you need the common UUID hex representation.
526 ///
527 /// # Parameters
528 ///
529 /// - `uppercase`: If `true`, uses uppercase hex digits (A-F). If `false`, uses lowercase (a-f).
530 ///
531 /// # Examples
532 ///
533 /// ```rust
534 /// use tnid::{Tnid, TnidName, NameStr};
535 ///
536 /// struct User;
537 /// impl TnidName for User {
538 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
539 /// }
540 ///
541 /// let id = Tnid::<User>::new_v1();
542 ///
543 /// let uuid_lower = id.to_uuid_string_cased(false);
544 /// // "cab1952a-f09d-86d9-928e-96ea03dc6af3"
545 ///
546 /// let uuid_upper = id.to_uuid_string_cased(true);
547 /// // "CAB1952A-F09D-86D9-928E-96EA03DC6AF3"
548 /// ```
549 pub fn to_uuid_string_cased(&self, uppercase: bool) -> String {
550 utils::u128_to_uuid_string(self.id, uppercase)
551 }
552
553 /// Parses a TNID from UUID hex string format.
554 ///
555 /// This is the inverse of [`Self::to_uuid_string_cased`].
556 ///
557 /// The parser accepts both uppercase and lowercase hex digits (A-F or a-f).
558 ///
559 /// Returns `None` if:
560 /// - The string is not valid UUID format
561 /// - The UUID is not a valid TNID (wrong version/variant bits or name mismatch)
562 ///
563 /// For inspecting why a UUID might not be a valid TNID, see [`UUIDLike`].
564 ///
565 /// # Examples
566 ///
567 /// ```rust
568 /// use tnid::{Tnid, TnidName, NameStr};
569 ///
570 /// struct User;
571 /// impl TnidName for User {
572 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
573 /// }
574 ///
575 /// // Create a TNID and convert to UUID string
576 /// let original = Tnid::<User>::new_v1();
577 /// let uuid_string = original.to_uuid_string_cased(false);
578 ///
579 /// // Parse it back
580 /// let parsed = Tnid::<User>::parse_uuid_string(&uuid_string);
581 /// assert!(parsed.is_some());
582 /// assert_eq!(parsed.unwrap().as_u128(), original.as_u128());
583 ///
584 /// // Also accepts uppercase
585 /// let uuid_upper = original.to_uuid_string_cased(true);
586 /// let parsed_upper = Tnid::<User>::parse_uuid_string(&uuid_upper);
587 /// assert!(parsed_upper.is_some());
588 ///
589 /// // Invalid: not a valid UUID format
590 /// assert!(Tnid::<User>::parse_uuid_string("not-a-uuid").is_none());
591 /// ```
592 pub fn parse_uuid_string(uuid_string: &str) -> Option<Self> {
593 let id = UUIDLike::parse_uuid_string(uuid_string)?.as_u128();
594
595 Self::from_u128(id)
596 }
597
598 /// Parses a TNID from its string representation.
599 ///
600 /// This is the inverse of [`Self::as_tnid_string`]. See that method for details
601 /// on the TNID string format.
602 ///
603 /// Returns `None` if the string is invalid. Validation includes:
604 /// - Correct format (`<name>.<encoded-data>`)
605 /// - Name matches the expected name for this TNID type
606 /// - Valid TNID Data Encoding
607 /// - Correct UUIDv8 version and variant bits
608 ///
609 /// If you need to inspect non-compliant IDs or understand why parsing failed,
610 /// consider using [`UUIDLike`] which provides lower-level access.
611 ///
612 /// # Examples
613 ///
614 /// ```rust
615 /// use tnid::{Tnid, TnidName, NameStr};
616 ///
617 /// struct User;
618 /// impl TnidName for User {
619 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
620 /// }
621 ///
622 /// // Successful parsing
623 /// let parsed = Tnid::<User>::parse_tnid_string("user.Br2flcNDfF6LYICnT");
624 /// assert!(parsed.is_some());
625 ///
626 /// // Failed parsing - wrong name
627 /// assert!(Tnid::<User>::parse_tnid_string("post.Br2flcNDfF6LYICnT").is_none());
628 ///
629 /// // Failed parsing - invalid format
630 /// assert!(Tnid::<User>::parse_tnid_string("not-a-tnid").is_none());
631 /// ```
632 pub fn parse_tnid_string(tnid_string: &str) -> Option<Self> {
633 // Split on dot separator
634 let (name, data_str) = tnid_string.split_once('.')?;
635
636 // Validate name matches expected name
637 if name != Name::ID_NAME.as_str() {
638 return None;
639 }
640
641 // Decode data string to compact 102 bits
642 let compact_data = data_encoding::string_to_id_data(data_str)?;
643
644 // Expand to proper bit positions
645 let data_bits = data_encoding::expand_data_bits(compact_data);
646
647 // Get name bits
648 let name_bits = name_encoding::name_mask(Name::ID_NAME);
649
650 // Combine: name + UUID metadata + data
651 let id = name_bits | utils::UUID_V8_MASK | data_bits;
652
653 // Validate and construct (this checks UUID bits and name encoding)
654 Self::from_u128(id)
655 }
656
657 /// Creates a TNID from a raw 128-bit value.
658 ///
659 /// This is the inverse of [`Self::as_u128`] and is useful for loading TNIDs from
660 /// databases that store UUIDs as u128/binary, interoperating with UUID-based systems,
661 /// or deserializing.
662 ///
663 /// Returns `None` if the value is not a valid TNID. Validation includes:
664 /// - Correct UUIDv8 version and variant bits
665 /// - Name encoding matches the expected name for this TNID type
666 ///
667 /// # Endianness
668 ///
669 /// When loading from bytes, you'll almost certainly want to parse a `[u8; 16]` to a
670 /// `u128` using big-endian byte order with [`u128::from_be_bytes()`], as per the
671 /// UUID specification.
672 pub fn from_u128(id: u128) -> Option<Self> {
673 // check UUIDv8 version and variant bits
674 if (id & utils::UUID_V8_MASK) != utils::UUID_V8_MASK {
675 return None;
676 }
677
678 // check name encoding matches expected name
679 let name_bits_mask = 0xFFFFF_u128 << 108; // top 20 bits
680 let actual_name_bits = id & name_bits_mask;
681 let expected_name_bits = name_encoding::name_mask(Name::ID_NAME);
682 if actual_name_bits != expected_name_bits {
683 return None;
684 }
685
686 Some(Self {
687 id,
688 id_name: PhantomData,
689 })
690 }
691
692 /// Encrypts a V0 TNID to a V1 TNID, hiding timestamp information.
693 ///
694 /// V0 TNIDs contain a timestamp (like UUIDv7), which may leak information when exposed
695 /// publicly. This method encrypts the data bits to produce a valid V1 TNID that hides
696 /// the timestamp while remaining decryptable with [`Self::decrypt_v1_to_v0`].
697 ///
698 /// See the [`encryption`] module for more details on why and how this works.
699 ///
700 /// # Parameters
701 ///
702 /// - `secret`: 128-bit (16 bytes) encryption key
703 ///
704 /// # Returns
705 ///
706 /// - `Ok(encrypted)` for V0 input (encrypts and converts to V1)
707 /// - `Ok(self)` for V1 input (already encrypted, returns unchanged)
708 /// - `Err(())` for V2/V3 input (unsupported variants)
709 ///
710 /// # Example
711 ///
712 /// ```rust
713 /// use tnid::{Tnid, TnidName, NameStr, TNIDVariant};
714 ///
715 /// struct User;
716 /// impl TnidName for User {
717 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
718 /// }
719 ///
720 /// let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
721 ///
722 /// let original = Tnid::<User>::new_v0();
723 /// let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
724 /// assert_eq!(encrypted.variant(), TnidVariant::V1);
725 ///
726 /// let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
727 /// assert_eq!(decrypted.as_u128(), original.as_u128());
728 /// ```
729 #[cfg(feature = "encryption")]
730 pub fn encrypt_v0_to_v1(&self, key: impl Into<encryption::EncryptionKey>) -> Result<Self, ()> {
731 match self.variant() {
732 TnidVariant::V0 => {}
733 TnidVariant::V1 => return Ok(*self),
734 TnidVariant::V2 => return Err(()),
735 TnidVariant::V3 => return Err(()),
736 }
737
738 // Extract only the secret data bits (100 bits, excludes TNID variant)
739 let secret_data = encryption::extract_secret_data_bits(self.id);
740
741 // Encrypt the secret data
742 let encrypted_data = encryption::encrypt(secret_data, &key.into());
743
744 // Expand back to proper bit positions
745 let expanded = encryption::expand_secret_data_bits(encrypted_data);
746
747 // Preserve name and UUID metadata, replace data bits with encrypted version
748 let id = (self.id & !encryption::COMPLETE_SECRET_DATA_MASK) | expanded;
749
750 // Change variant from V0 to V1
751 let id = utils::change_variant(id, TnidVariant::V1);
752
753 Ok(Self {
754 id_name: PhantomData,
755 id,
756 })
757 }
758
759 /// Decrypts a V1 TNID back to a V0 TNID, recovering timestamp information.
760 ///
761 /// This is the inverse of [`Self::encrypt_v0_to_v1`]. See the [`encryption`] module
762 /// for more details.
763 ///
764 /// # Parameters
765 ///
766 /// - `secret`: 128-bit (16 bytes) encryption key (must match the key used for encryption)
767 ///
768 /// # Returns
769 ///
770 /// - `Ok(decrypted)` for V1 input (decrypts and converts to V0)
771 /// - `Ok(self)` for V0 input (already decrypted, returns unchanged)
772 /// - `Err(())` for V2/V3 input (unsupported variants)
773 ///
774 /// # Example
775 ///
776 /// ```rust
777 /// use tnid::{Tnid, TnidName, NameStr, TNIDVariant};
778 ///
779 /// struct User;
780 /// impl TnidName for User {
781 /// const ID_NAME: NameStr<'static> = NameStr::new_const("user");
782 /// }
783 ///
784 /// let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
785 ///
786 /// let original = Tnid::<User>::new_v0();
787 /// let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
788 ///
789 /// let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
790 /// assert_eq!(decrypted.variant(), TnidVariant::V0);
791 /// assert_eq!(decrypted.as_u128(), original.as_u128());
792 /// ```
793 #[cfg(feature = "encryption")]
794 pub fn decrypt_v1_to_v0(&self, key: impl Into<encryption::EncryptionKey>) -> Result<Self, ()> {
795 match self.variant() {
796 TnidVariant::V0 => return Ok(*self),
797 TnidVariant::V1 => {}
798 TnidVariant::V2 => return Err(()),
799 TnidVariant::V3 => return Err(()),
800 }
801
802 // Extract only the secret data bits (100 bits, excludes TNID variant)
803 let encrypted_data = encryption::extract_secret_data_bits(self.id);
804
805 // Decrypt the secret data
806 let decrypted_data = encryption::decrypt(encrypted_data, &key.into());
807
808 // Expand back to proper bit positions
809 let expanded = encryption::expand_secret_data_bits(decrypted_data);
810
811 // Preserve name and UUID metadata, replace data bits with decrypted version
812 let id = (self.id & !encryption::COMPLETE_SECRET_DATA_MASK) | expanded;
813
814 // Change variant from V1 to V0
815 let id = utils::change_variant(id, TnidVariant::V0);
816
817 Ok(Self {
818 id_name: PhantomData,
819 id,
820 })
821 }
822}
823
824impl<Name: TnidName> std::fmt::Display for Tnid<Name> {
825 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826 write!(f, "{}", self.as_tnid_string())
827 }
828}
829
830impl<Name: TnidName> std::fmt::Debug for Tnid<Name> {
831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832 write!(f, "{}", self.as_tnid_string())
833 }
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839
840 struct TestId;
841 impl TnidName for TestId {
842 const ID_NAME: NameStr<'static> = NameStr::new_const("test");
843 }
844
845 #[test]
846 fn variant0_is_k_sortable() {
847 use time::Duration;
848
849 let mut test_time = time::OffsetDateTime::now_utc();
850 let mut last_id: Tnid<TestId> = Tnid::new_v0_with_time(test_time);
851
852 for _ in 1..10_000 {
853 test_time += Duration::milliseconds(1);
854 let id: Tnid<TestId> = Tnid::new_v0_with_time(test_time);
855
856 assert!(last_id.as_u128() < id.as_u128());
857 assert!(last_id.as_tnid_string() < id.as_tnid_string());
858
859 last_id = id;
860 }
861 }
862
863 #[test]
864 fn tnid_variant_returns_v0() {
865 let id: Tnid<TestId> = Tnid::new_v0();
866 assert_eq!(id.variant(), TnidVariant::V0);
867 }
868
869 #[cfg(feature = "encryption")]
870 #[test]
871 fn encryption_bidirectional() {
872 let secret = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
873
874 let original: Tnid<TestId> = Tnid::new_v0();
875 assert_eq!(original.variant(), TnidVariant::V0);
876
877 let encrypted = original.encrypt_v0_to_v1(secret).unwrap();
878 assert_eq!(encrypted.variant(), TnidVariant::V1);
879
880 dbg!(encrypted, original);
881
882 let decrypted = encrypted.decrypt_v1_to_v0(secret).unwrap();
883 assert_eq!(decrypted.variant(), TnidVariant::V0);
884
885 assert_eq!(decrypted.as_u128(), original.as_u128());
886 }
887
888 #[test]
889 fn parse_tnid_string_roundtrip() {
890 let original: Tnid<TestId> = Tnid::new_v0();
891 let tnid_string = original.as_tnid_string();
892 let parsed = Tnid::<TestId>::parse_tnid_string(&tnid_string).unwrap();
893 assert_eq!(parsed.as_u128(), original.as_u128());
894 }
895
896 #[test]
897 fn parse_tnid_string_invalid_name() {
898 let result = Tnid::<TestId>::parse_tnid_string("wrong.abc123xyz");
899 assert!(result.is_none());
900 }
901
902 #[test]
903 fn parse_tnid_string_no_separator() {
904 let result = Tnid::<TestId>::parse_tnid_string("testabc123xyz");
905 assert!(result.is_none());
906 }
907}