1 //! TESID: Textualised Encrypted Sequential Identifiers
3 //! See <https://chrismorgan.info/tesid/> for a description of what it’s all about, and how to best
9 //! let secret_key = "000102030405060708090a0b0c0d0e0f";
10 //! let coder = tesid::TesidCoder::new(secret_key).unwrap();
11 //! assert_eq!(&*coder.encode(0), "w2ej");
12 //! assert_eq!(&*coder.encode(1), "w6um");
13 //! assert_eq!(&*coder.encode(2), "x45g");
14 //! assert_eq!(&*coder.encode(2_u64.pow(20) - 1), "atcw");
15 //! assert_eq!(&*coder.encode(2_u64.pow(20)), "8qwm6y");
16 //! assert_eq!(&*coder.encode(2_u64.pow(30) - 1), "3eipc7");
17 //! assert_eq!(&*coder.encode(2_u64.pow(30)), "n3md95r4");
18 //! assert_eq!(&*coder.encode_long(2_u128.pow(100) - 1).unwrap(), "ia2bvpjaiju7g5uaxn5t");
19 //! assert_eq!(coder.encode_long(2_u128.pow(100)), Err(tesid::ValueTooLarge));
21 //! assert_eq!(coder.decode("w2ej"), Ok(0));
24 #![cfg_attr(not(feature = "std" ), no_std)]
25 #![cfg_attr(docsrs, feature(doc_cfg))]
27 #![allow(clippy::eq_op, clippy::identity_op)] // `20 - 20` and `input >> 0`, good for symmetry.
29 // To run benchmarks: `RUSTFLAGS="--cfg bench" cargo +nightly bench`
30 #![cfg_attr(bench, feature(test))]
31 #[cfg(bench)] extern crate test;
34 use core
:: marker
:: PhantomData
;
36 use core
:: sync
:: atomic
;
42 pub use inline_string
:: InlineString
;
44 /// The error type when decoding of a TESID failed.
46 /// Decoding can fail for a number of reasons; most are things you can’t do anything about.
47 /// `WrongDiscriminant` is the only one that’s likely to be genuinely useful,
48 /// for enhancing error messages.
50 /// Because of the typical opaqueness of TESID decode errors,
51 /// The `fmt::Display` implementation doesn’t try to say anything useful, just “invalid TESID”.
53 #[cfg_attr(feature = "std" , doc = "Since <span class='stab portability'>**crate feature `std`**</span> is enabled, this type implements [`std::error::Error`].
54 (Though if you’re using `TypedTesidCoder`, you will need to make sure that your type discriminant type implements [`core::fmt::Debug`].)" )]
55 #[cfg_attr(not(feature = "std" ), doc = "If you want this type to implement `std::error::Error`, enable <span class='stab portability'>**crate feature `std`**</span>." )]
56 #[derive(Clone, Debug, PartialEq, Eq)]
57 pub enum DecodeError
< Discriminant
> {
58 /// The TESID was not 4, 6, 8, 10, 12, 14, 16, 18 or 20 characters long.
61 /// The TESID contained characters not in the defined base-32 alphabet.
64 /// The TESID was an improperly-encoded value, using the wrong length and cipher,
65 /// e.g. a ten-character string that decodes to 0, but 0 should be four characters.
66 /// This cannot happen normally and suggests the user is trying to enumerate IDs.
69 /// The value was not in the acceptable range for the type,
70 /// e.g. ≥2⁶⁴ in a u64, or <0 via discriminant.
72 /// Note that id must be u64 where sparsity or discrimination are used.
75 /// The TESID did not match the discriminant requested.
76 /// This assumes that the sparsity was correct.
77 /// The actual discriminant and ID found are provided.
79 /// This variant is basically the just-for-enhancing-error-reporting version of
80 /// [`TesidCoder::split_decode`] or [`TypedTesidCoder::split_decode`],
81 /// which you should use instead if you want to actively support diverse discriminants.
84 discriminant
: Discriminant
,
87 /// Via [`TypedTesidCoder`] only: the TESID’s discriminant wasn’t an acceptable value.
88 MalformedDiscriminant
{
94 impl < Discriminant
> fmt
:: Display
for DecodeError
< Discriminant
> {
95 fn fmt (& self , f
: & mut fmt
:: Formatter
) -> fmt
:: Result
{
96 f
. write_str ( "invalid TESID" )
100 #[cfg(feature = "std" )]
101 #[cfg_attr(docsrs, doc(cfg(feature = "std" )))]
102 impl < Discriminant
: fmt
:: Debug
> std
:: error
:: Error
for DecodeError
< Discriminant
> {}
104 /// The error type when encoding of a TESID failed.
106 /// Encoding can fail when `encode_long` is used with a value of 2¹²⁰ or more.
108 #[cfg_attr(feature = "std" , doc = "Since <span class='stab portability'>**crate feature `std`**</span> is enabled, this type implements [`std::error::Error`]." )]
109 #[cfg_attr(not(feature = "std" ), doc = "If you want this type to implement `std::error::Error`, enable <span class='stab portability'>**crate feature `std`**</span>." )]
110 #[derive(Clone, Debug, PartialEq, Eq)]
111 pub struct ValueTooLarge
;
113 impl fmt
:: Display
for ValueTooLarge
{
114 fn fmt (& self , f
: & mut fmt
:: Formatter
) -> fmt
:: Result
{
115 f
. write_str ( "cannot TESID-encode a value 2¹⁰⁰ or above" )
119 #[cfg(feature = "std" )]
120 #[cfg_attr(docsrs, doc(cfg(feature = "std" )))]
121 impl std
:: error
:: Error
for ValueTooLarge {}
125 pub struct TesidCoder
{
126 expanded_key
: [ u64 ; 30 ],
129 impl Drop
for TesidCoder
{
131 // Might as well zero the memory in which lies the expanded key, as good security practice.
132 // SAFETY: the pointer is trivially valid and aligned, and not accessed concurrently.
133 unsafe { ptr
:: write_volatile (& mut self . expanded_key
, Default
:: default ()); }
134 atomic
:: compiler_fence ( atomic
:: Ordering
:: SeqCst
);
139 /// Initialise a TESID coder with a key in hexadecimal string representation.
141 /// The key string must be made up of exactly 32 lowercase hexadecimal (0-9a-f) characters,
142 /// and should have been generated randomly.
143 /// Refer to external documentation for advice on key generation.
146 /// tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
148 pub fn new ( key
: & str ) -> Option
< Self > {
149 // from_str_radix plus checks to remove its undesired functionality (leading +, len != 16, A-F)
150 // won’t be quite as fast as possible, but the difference should be slight enough.
151 if key
. len () != 32 || key
. bytes (). any (| b
| ! matches! ( b
, b'0' ..= b'9' | b'a' ..= b'f' )) {
154 let key
= u128
:: from_str_radix ( key
, 16 ). ok () ?
;
156 expanded_key
: fpeck
:: expand ( key
),
163 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
164 /// assert_eq!(&*coder.encode(0), "w2ej");
166 pub fn encode (& self , id
: u64 ) -> InlineString
< 20 > {
167 self . encode_long ( id
as u128
). unwrap ()
170 /// Encode an ID, with sparsity and/or discrimination.
172 /// This will panic if `id * sparsity + discriminant` is 2¹⁰⁰ or higher, which can only happen
173 /// if `sparsity` is 2³⁶ or higher (which would be sparsity of one in 68 billion, adding around
174 /// 7.2 characters to the TESID, extreme overkill for all reasonable scenarios—and that’s why
175 /// this method panics rather than returning a Result, because panicking just about guarantees
176 /// that you’re doing the wrong thing; yet I haven’t artificially capped `sparsity`, as you
177 /// *can* correctly use it with a higher value, so long as `id` doesn’t get too high).
178 // (Well, actually I kinda have artificially limited all three parameters to u64.)
181 /// // Define constants for consistent use:
182 /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
183 /// const TYPE_A: u64 = 0;
184 /// const TYPE_B: u64 = 1;
185 /// const TYPE_C: u64 = 2;
187 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
189 /// // id 0 with three different discriminants/tags:
190 /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_A), "w2ej");
191 /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_B), "w6um");
192 /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_C), "x45g");
194 /// // id 1 with three different discriminants/tags:
195 /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_A), "dh2h");
196 /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_B), "a6xy");
197 /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_C), "7xgj");
199 pub fn encode_with_tag (& self , id
: u64 , sparsity
: u64 , discriminant
: u64 ) -> InlineString
< 20 > {
201 id
. checked_mul ( sparsity
as u128
)
202 . and_then (| id
| id
. checked_add ( discriminant
as u128
))
203 . and_then (| id
| self . encode_long ( id
). ok ())
204 . expect ( "TesidCoder::encode_with_tag used badly" )
207 /// Attempt to encode a u128 ID. This will return an error if the ID is 2¹⁰⁰ or greater.
210 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
211 /// assert_eq!(&*coder.encode_long(1<<70).unwrap(), "zk9d3setywjf7uwu");
212 /// assert_eq!(coder.encode_long(1<<100), Err(tesid::ValueTooLarge));
214 pub fn encode_long (& self , id
: u128
) -> Result
< InlineString
< 20 >, ValueTooLarge
> {
215 // Maybe in the future I’ll be able to do an inline const pattern, but for now—
216 const MIN_10
: u128
= 0 ; const MAX_10
: u128
= ( 1 << 20 ) - 1 ;
217 const MIN_15
: u128
= 1 << 20 ; const MAX_15
: u128
= ( 1 << 30 ) - 1 ;
218 const MIN_20
: u128
= 1 << 30 ; const MAX_20
: u128
= ( 1 << 40 ) - 1 ;
219 const MIN_25
: u128
= 1 << 40 ; const MAX_25
: u128
= ( 1 << 50 ) - 1 ;
220 const MIN_30
: u128
= 1 << 50 ; const MAX_30
: u128
= ( 1 << 60 ) - 1 ;
221 const MIN_35
: u128
= 1 << 60 ; const MAX_35
: u128
= ( 1 << 70 ) - 1 ;
222 const MIN_40
: u128
= 1 << 70 ; const MAX_40
: u128
= ( 1 << 80 ) - 1 ;
223 const MIN_45
: u128
= 1 << 80 ; const MAX_45
: u128
= ( 1 << 90 ) - 1 ;
224 const MIN_50
: u128
= 1 << 90 ; const MAX_50
: u128
= ( 1 << 100 ) - 1 ;
225 const MIN_TOO_LARGE
: u128
= 1 << 100 ;
226 let ( n
, start
) = match id
{
227 MIN_10
..= MAX_10
=> ( 10 , 20 - 4 ),
228 MIN_15
..= MAX_15
=> ( 15 , 20 - 6 ),
229 MIN_20
..= MAX_20
=> ( 20 , 20 - 8 ),
230 MIN_25
..= MAX_25
=> ( 25 , 20 - 10 ),
231 MIN_30
..= MAX_30
=> ( 30 , 20 - 12 ),
232 MIN_35
..= MAX_35
=> ( 35 , 20 - 14 ),
233 MIN_40
..= MAX_40
=> ( 40 , 20 - 16 ),
234 MIN_45
..= MAX_45
=> ( 45 , 20 - 18 ),
235 MIN_50
..= MAX_50
=> ( 50 , 20 - 20 ),
236 MIN_TOO_LARGE
.. => return Err ( ValueTooLarge
),
239 // SAFETY: base32::encode guarantees its output is ASCII; and start is less than buf.len().
240 // (start lets us take the padding we want and discard the rest.)
242 buf
: base32
:: encode ( fpeck
:: encrypt (& self . expanded_key
, n
, id
)),
247 /// Decode a 64-bit ID.
249 /// This is just `decode_long` followed by `u64::try_from`, but I wanted this for symmetry with
250 /// `encode`/`encode_long` which are separated because of the fallibility of the latter. By the
251 /// same token, I suppose if I just provided a u128-yielding method, then it’d be too tempting
252 /// for users to use the faulty `as u64`. Never mind that you might want an `i64` anyway (since
253 /// SQL databases don’t do unsigned integers very well) and thus will have to do the same
254 /// thing. Ah well, you can’t win them all. I got my symmetry.
255 // I thought briefly about a generic API so that you can .decode::<u128 | u64 | i64 | …>(…),
256 // but decided that would be too often too painful to use. .decode_u128(…) / .decode_u64(…) /
257 // .decode_i64(…) would probably be better if I were going that way.
260 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
261 /// assert_eq!(coder.decode("w2ej"), Ok(0));
263 pub fn decode (& self , tesid
: & str ) -> Result
< u64 , DecodeError
< u64 >> {
264 self . decode_long ( tesid
). and_then (| id
| id
. try_into (). map_err (| _
| DecodeError
:: OutOfRange
))
267 /// Decode a 128-bit ID.
270 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
271 /// assert_eq!(coder.decode_long("zk9d3setywjf7uwu"), Ok(1<<70));
273 pub fn decode_long (& self , tesid
: & str ) -> Result
< u128
, DecodeError
< u64 >> {
274 let ( n
, minimum
) = match tesid
. len () {
284 _
=> return Err ( DecodeError
:: BadLength
),
286 let number
= base32
:: decode ( tesid
). map_err (|()| DecodeError
:: BadCharacters
) ?
;
287 let id
= fpeck
:: decrypt (& self . expanded_key
, n
, number
);
291 Err ( DecodeError
:: OverlyLongEncoding
)
295 /// Decode an ID that was encoded with sparsity and/or discrimination.
297 /// See [`TesidCoder::encode_with_tag`] for a description of `sparsity` and `discriminant`.
299 /// If you want to decode supporting any discriminant, see [`TesidCoder::split_decode`].
302 /// // Define constants for consistent use:
303 /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
304 /// const TYPE_A: u64 = 0;
305 /// const TYPE_B: u64 = 1;
306 /// const TYPE_C: u64 = 2;
308 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
310 /// // id 0 with three different discriminants/tags:
311 /// assert_eq!(coder.decode_with_tag("w2ej", TYPE_SPARSITY, TYPE_A), Ok(0));
312 /// assert_eq!(coder.decode_with_tag("w6um", TYPE_SPARSITY, TYPE_B), Ok(0));
313 /// assert_eq!(coder.decode_with_tag("x45g", TYPE_SPARSITY, TYPE_C), Ok(0));
315 /// // id 1 with three different discriminants/tags:
316 /// assert_eq!(coder.decode_with_tag("dh2h", TYPE_SPARSITY, TYPE_A), Ok(1));
317 /// assert_eq!(coder.decode_with_tag("a6xy", TYPE_SPARSITY, TYPE_B), Ok(1));
318 /// assert_eq!(coder.decode_with_tag("7xgj", TYPE_SPARSITY, TYPE_C), Ok(1));
320 /// // And you can’t decode an ID with the wrong discriminant (presuming correct sparsity):
321 /// assert_eq!(coder.decode_with_tag("w2ej", TYPE_SPARSITY, TYPE_C),
322 /// Err(tesid::DecodeError::WrongDiscriminant { id: 0, discriminant: 0 }));
324 pub fn decode_with_tag (& self , tesid
: & str , sparsity
: u64 , discriminant
: u64 )
325 -> Result
< u64 , DecodeError
< u64 >> {
326 // This deliberately DOESN’T use split_decode, which requires sparsity > discriminant,
327 // which I don’t want to be required here.
328 let id
= self . decode_long ( tesid
) ?
;
329 if let Some ( id
) = id
. checked_sub ( discriminant
as u128
) {
330 if id
% sparsity
as u128
== 0 {
331 return u64 :: try_from ( id
/ sparsity
as u128
)
332 . map_err (| _
| DecodeError
:: OutOfRange
);
335 if sparsity
<= discriminant
{
336 // This suggests discriminant is just being used for an offset,
337 // not as a type tag or similar. In this situation, the discriminant is irrepairable,
338 // so we just declare it bad and wash our hands of it.
339 Err ( DecodeError
:: OutOfRange
)
341 Err ( DecodeError
:: WrongDiscriminant
{
342 id
: u64 :: try_from ( id
/ sparsity
as u128
). map_err (| _
| DecodeError
:: OutOfRange
) ?
,
343 discriminant
: id
as u64 % sparsity
,
348 /// Decode an ID that was encoded with certain sparsity,
349 /// separating the discriminant and returning it alongside the ID.
351 /// This is useful if you want to accept various discriminants;
352 /// one simple use case is better error reporting:
353 /// “that’s an ID for type A, but this takes IDs for type B”.
355 /// This allows *you* to identify the discriminant, but due to the encryption,
356 /// anyone who has only the ID cannot;
357 /// if you want users to be able to discern the discriminant,
358 /// consider adding a human-friendly prefix to the ID;
359 /// I like a single uppercase letter or a word followed by an underscore.
361 /// This requires that the discriminant be less than the sparsity,
362 /// or incorrect values will be produced.
363 // Note the use of u64 here. This also fits into the symmetry I was talking of. If you happen
364 // to want split_decode for IDs 2⁶⁴ or higher, well, here’s the implementation, it’s simple.
367 /// // Define constants for consistent use:
368 /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
369 /// const TYPE_A: u64 = 0;
370 /// const TYPE_B: u64 = 1;
371 /// const TYPE_C: u64 = 2;
373 /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
375 /// // id 0 with three different discriminants/tags:
376 /// assert_eq!(coder.split_decode("w2ej", TYPE_SPARSITY),
377 /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_A }));
378 /// assert_eq!(coder.split_decode("w6um", TYPE_SPARSITY),
379 /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_B }));
380 /// assert_eq!(coder.split_decode("x45g", TYPE_SPARSITY),
381 /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_C }));
383 /// // id 1 with three different discriminants/tags:
384 /// assert_eq!(coder.split_decode("dh2h", TYPE_SPARSITY),
385 /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_A }));
386 /// assert_eq!(coder.split_decode("a6xy", TYPE_SPARSITY),
387 /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_B }));
388 /// assert_eq!(coder.split_decode("7xgj", TYPE_SPARSITY),
389 /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_C }));
391 pub fn split_decode (& self , tesid
: & str , sparsity
: u64 )
392 -> Result
< SplitDecode
< u64 >, DecodeError
< u64 >> {
393 let id
= self . decode_long ( tesid
) ?
;
395 id
: u64 :: try_from ( id
/ sparsity
as u128
)
396 . map_err (| _
| DecodeError
:: OutOfRange
) ?
,
397 discriminant
: id
as u64 % sparsity
,
402 impl fmt
:: Debug
for TesidCoder
{
403 fn fmt (& self , f
: & mut fmt
:: Formatter
) -> fmt
:: Result
{
404 // Hide the expanded key.
405 f
. debug_struct ( "TesidCoder" ). finish_non_exhaustive ()
409 /// The output of [`TesidCoder::split_decode`], separating ID and discriminant.
410 #[derive(Clone, Debug, PartialEq, Eq)]
411 pub struct SplitDecode
< Discriminant
> {
413 pub discriminant
: Discriminant
,
416 /// A trait to implement for type enums for their use with [`TypedTesidCoder`].
418 /// See [`define_type_discriminant!`] for the usual way of defining types that implement this.
419 pub trait TypeDiscriminant
: Sized
{
420 /// The value to use for sparsity. This must exceed the highest discriminant,
421 /// or discriminants equal or greater will decode incorrectly.
423 fn into_discriminant ( self ) -> u64 ;
424 fn from_discriminant ( discriminant
: u64 ) -> Option
< Self >;
427 // It would have been nice to impl TypeDiscriminant for u64,
428 // but I would have had to shift sparsity to a runtime parameter,
429 // and I don’t want to lose its place as a const on the impl.
431 /// Define a type enum for use with `TypedTesidCoder`.
433 /// This is just shorthand for defining the C-style enum written,
434 /// and implementing [`TypeDiscriminant`] in the obvious way on it.
435 /// If it’s not to your liking, you can easily do that manually.
437 /// You might have expected a derive macro, `#[derive(tesid::TypeDiscriminant)]` with
438 /// `#[tesid(sparsity = 256)]` or similar, but I’ve stuck with a structural macro
439 /// for faster compilation and to keep zero dependencies.
444 /// tesid::define_type_discriminant!(sparsity=256,
445 /// #[non_exhaustive]
446 /// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
455 macro_rules
! define_type_discriminant
{
456 ( sparsity
= $sparsity
: expr
,
457 $
( #[$attr:meta])* $vis:vis enum $name:ident {
458 $
( $variant_name
: ident
= $variant_value
: expr
),* $
(,) ?
460 $
( #[$attr])* $vis enum $name {
461 $
( $variant_name
= $variant_value
),*
464 impl $
crate :: TypeDiscriminant
for $name
{
465 const SPARSITY
: u64 = $sparsity
;
467 fn into_discriminant ( self ) -> u64 {
471 fn from_discriminant ( discriminant
: u64 ) -> Option
< Self > {
473 $
( $variant_value
=> Some ( Self :: $variant_name
),)*
481 /// A TESID coder with type discrimination baked in.
483 /// All method examples will assume the following setup:
486 /// use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
488 /// define_type_discriminant!(sparsity=256,
489 /// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
490 /// #[non_exhaustive]
498 /// let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
499 /// let coder = TypedTesidCoder::<Type>::new(coder);
502 /// See [`define_type_discriminant!`] for the usual way of defining a type for `D`.
504 pub struct TypedTesidCoder
< D
: TypeDiscriminant
> {
506 marker
: PhantomData
< D
>,
509 impl < D
: TypeDiscriminant
> fmt
:: Debug
for TypedTesidCoder
< D
> {
510 fn fmt (& self , f
: & mut fmt
:: Formatter
) -> fmt
:: Result
{
511 f
. debug_struct ( "TypedTesidCoder" ). finish_non_exhaustive ()
515 impl < D
: TypeDiscriminant
> TypedTesidCoder
< D
> {
516 /// Initialise a typed TESID coder.
518 /// This takes a [`TesidCoder`] (rather than a key) so that you can clone an existing coder,
519 /// if you don’t always use the one sparsity and type enum.
520 pub fn new ( coder
: TesidCoder
) -> Self {
521 Self { coder
, marker
: PhantomData
}
524 /// Encode an ID with a specific type.
527 /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
528 /// # define_type_discriminant!(sparsity=256,
529 /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
530 /// # pub enum Type { A = 0, B = 1, C = 2, });
531 /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
532 /// # let coder = TypedTesidCoder::<Type>::new(coder);
533 /// assert_eq!(&*coder.encode(Type::A, 0), "w2ej");
534 /// assert_eq!(&*coder.encode(Type::B, 0), "w6um");
535 /// assert_eq!(&*coder.encode(Type::A, 1), "dh2h");
537 pub fn encode (& self , r
#type: D, id: u64) -> InlineString<20> {
538 self . coder
. encode_with_tag ( id
, D
:: SPARSITY
, r
#type.into_discriminant())
541 /// Decode an ID with a specific type.
544 /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
545 /// # define_type_discriminant!(sparsity=256,
546 /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
547 /// # pub enum Type { A = 0, B = 1, C = 2, });
548 /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
549 /// # let coder = TypedTesidCoder::<Type>::new(coder);
550 /// assert_eq!(coder.decode(Type::A, "w2ej"), Ok(0));
551 /// assert_eq!(coder.decode(Type::B, "w6um"), Ok(0));
552 /// assert_eq!(coder.decode(Type::A, "dh2h"), Ok(1));
553 /// assert_eq!(coder.decode(Type::A, "w6um"),
554 /// Err(DecodeError::WrongDiscriminant { id: 0, discriminant: Type::B }));
556 pub fn decode (& self , r
#type: D, tesid: &str) -> Result<u64, DecodeError<D>> {
557 self . coder
. decode_with_tag ( tesid
, D
:: SPARSITY
, r
#type.into_discriminant())
558 . map_err ( Self :: convert_decode_error
)
561 /// Decode an ID and type and return both.
564 /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
565 /// # define_type_discriminant!(sparsity=256,
566 /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
567 /// # pub enum Type { A = 0, B = 1, C = 2, });
568 /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
569 /// # let coder = TypedTesidCoder::<Type>::new(coder);
570 /// assert_eq!(coder.split_decode("w2ej"), Ok(SplitDecode { id: 0, discriminant: Type::A }));
571 /// assert_eq!(coder.split_decode("w6um"), Ok(SplitDecode { id: 0, discriminant: Type::B }));
572 /// assert_eq!(coder.split_decode("dh2h"), Ok(SplitDecode { id: 1, discriminant: Type::A }));
573 /// assert_eq!(coder.split_decode("6mqv"),
574 /// Err(DecodeError::MalformedDiscriminant { id: 0, discriminant: 3 }));
576 pub fn split_decode (& self , tesid
: & str ) -> Result
< SplitDecode
< D
>, DecodeError
< D
>> {
577 self . coder
. split_decode ( tesid
, D
:: SPARSITY
)
578 . and_then (| SplitDecode
{ id
, discriminant
}| Ok ( SplitDecode
{
580 discriminant
: D
:: from_discriminant ( discriminant
)
581 . ok_or ( DecodeError
:: MalformedDiscriminant
{ id
, discriminant
}) ?
,
583 . map_err ( Self :: convert_decode_error
)
586 /// `DecodeError<u64>` → `DecodeError<D>`, just redoing `WrongDiscriminant`.
587 fn convert_decode_error ( e
: DecodeError
< u64 >) -> DecodeError
< D
> {
589 DecodeError
:: BadLength
=> DecodeError
:: BadLength
,
590 DecodeError
:: BadCharacters
=> DecodeError
:: BadCharacters
,
591 DecodeError
:: OverlyLongEncoding
=> DecodeError
:: OverlyLongEncoding
,
592 DecodeError
:: OutOfRange
=> DecodeError
:: OutOfRange
,
593 DecodeError
:: WrongDiscriminant
{ id
, discriminant
} => {
594 match D
:: from_discriminant ( discriminant
) {
595 Some ( discriminant
) => DecodeError
:: WrongDiscriminant
{ id
, discriminant
},
596 None
=> DecodeError
:: MalformedDiscriminant
{ id
, discriminant
},
599 DecodeError
:: MalformedDiscriminant
{ id
, discriminant
} =>
600 DecodeError
:: MalformedDiscriminant
{ id
, discriminant
},
605 // I don’t want to use the README as the crate docs,
606 // but I can still run the examples in it!
607 #[cfg_attr(doctest, doc = include_str!( "../README.md" ))]
608 fn _readme_doctests () {}
611 #[allow(unused_parens)] // The constants, for consistency
615 // 19 known values, 12 u64 and 7 u128.
616 const FIRST_4
: u64 = 0 ;
617 const LAST_4
: u64 = ( 1 << 20 ) - 1 ;
618 const FIRST_6
: u64 = ( 1 << 20 ) ;
619 const LAST_6
: u64 = ( 1 << 30 ) - 1 ;
620 const FIRST_8
: u64 = ( 1 << 30 ) ;
621 const LAST_8
: u64 = ( 1 << 40 ) - 1 ;
622 const FIRST_10
: u64 = ( 1 << 40 ) ;
623 const LAST_10
: u64 = ( 1 << 50 ) - 1 ;
624 const FIRST_12
: u64 = ( 1 << 50 ) ;
625 const LAST_12
: u64 = ( 1 << 60 ) - 1 ;
626 const FIRST_14
: u64 = ( 1 << 60 ) ;
627 const MID_14
: u64 = u64 :: MAX
;
628 const LAST_14
: u128
= ( 1 << 70 ) - 1 ;
629 const FIRST_16
: u128
= ( 1 << 70 ) ;
630 const LAST_16
: u128
= ( 1 << 80 ) - 1 ;
631 const FIRST_18
: u128
= ( 1 << 80 ) ;
632 const LAST_18
: u128
= ( 1 << 90 ) - 1 ;
633 const FIRST_20
: u128
= ( 1 << 90 ) ;
634 const LAST_20
: u128
= ( 1 << 100 ) - 1 ;
639 ( $coder
: ident
, sparsity
= $sparsity
: expr
, discriminant
= $discriminant
: expr
, split
= $split
: ident
, id
= $number
: expr
, tesid
= $string
: expr
) => {
640 assert_eq! (&* $coder
. encode_with_tag ( $number
, $sparsity
, $discriminant
), $string
);
641 assert_eq! ( $coder
. decode_with_tag ( $string
, $sparsity
, $discriminant
), Ok ( $number
));
642 $
split! ( $coder
. split_decode ( $string
, $sparsity
), Ok ( SplitDecode
{ id
: $number
, discriminant
: $discriminant
}));
643 //println!("{}", $coder.encode_with_tag($number, $sparsity, $discriminant));
645 ( $coder
: ident
, u64 $number
: expr
, $string
: expr
) => {
646 // Test it with both the long and normal methods.
647 case! ( $coder
, $number
as u128
, $string
);
648 assert_eq! (&* $coder
. encode ( $number
), $string
);
649 assert_eq! ( $coder
. decode ( $string
), Ok ( $number
));
651 ( $coder
: ident
, $number
: expr
, $string
: expr
) => {
652 assert_eq! (&* $coder
. encode_long ( $number
). unwrap (), $string
);
653 assert_eq! ( $coder
. decode_long ( $string
), Ok ( $number
));
654 //println!("{}", $coder.encode_long($number).unwrap());
658 let coder
= TesidCoder
:: new ( "00000000000000000000000000000000" ). unwrap ();
659 case! ( coder
, u64 FIRST_4
, "4kcc" );
660 case! ( coder
, u64 LAST_4
, "3rck" );
661 case! ( coder
, u64 FIRST_6
, "ju2sgs" );
662 case! ( coder
, u64 LAST_6
, "zangyh" );
663 case! ( coder
, u64 FIRST_8
, "2aux4u3h" );
664 case! ( coder
, u64 LAST_8
, "3cd7rc4h" );
665 case! ( coder
, u64 FIRST_10
, "m8669y33k6" );
666 case! ( coder
, u64 LAST_10
, "45e9rbrvvu" );
667 case! ( coder
, u64 FIRST_12
, "t47yf553iv8t" );
668 case! ( coder
, u64 LAST_12
, "cwd8t75epzje" );
669 case! ( coder
, u64 FIRST_14
, "86hk4d8hj4yvcy" );
670 case! ( coder
, u64 MID_14
, "sirnf2k2d2m3bm" );
671 case! ( coder
, LAST_14
, "m77g4ezr3e8qay" );
672 case! ( coder
, FIRST_16
, "43xf2jj6r6qm8bw4" );
673 case! ( coder
, LAST_16
, "6h3wb7wytjr5tbrd" );
674 case! ( coder
, FIRST_18
, "4vumjq33d8iiwaharq" );
675 case! ( coder
, LAST_18
, "qd7s3csnc5yfrrud5t" );
676 case! ( coder
, FIRST_20
, "jd3vsipfn69ia72chuvx" );
677 case! ( coder
, LAST_20
, "628fg5kyid3z2vf2j4tf" );
679 let coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
680 case! ( coder
, u64 FIRST_4
, "w2ej" );
681 case! ( coder
, u64 LAST_4
, "atcw" );
682 case! ( coder
, u64 FIRST_6
, "8qwm6y" );
683 case! ( coder
, u64 LAST_6
, "3eipc7" );
684 case! ( coder
, u64 FIRST_8
, "n3md95r4" );
685 case! ( coder
, u64 LAST_8
, "nnz4z5qb" );
686 case! ( coder
, u64 FIRST_10
, "st9fvria97" );
687 case! ( coder
, u64 LAST_10
, "qt42fug7hq" );
688 case! ( coder
, u64 FIRST_12
, "dykqxtu2ieqi" );
689 case! ( coder
, u64 LAST_12
, "h7rhnw6tfhun" );
690 case! ( coder
, u64 FIRST_14
, "xb5c8isevin9i3" );
691 case! ( coder
, u64 MID_14
, "t62mijffzuvu4e" );
692 case! ( coder
, LAST_14
, "n6n8jq6ike9dnj" );
693 case! ( coder
, FIRST_16
, "zk9d3setywjf7uwu" );
694 case! ( coder
, LAST_16
, "bqqei5vmzkqjfru3" );
695 case! ( coder
, FIRST_18
, "z83vvq5u84sit9g7pd" );
696 case! ( coder
, LAST_18
, "cpawgn8snjvverxvmp" );
697 case! ( coder
, FIRST_20
, "397btwmkh5y7sjz2xu82" );
698 case! ( coder
, LAST_20
, "ia2bvpjaiju7g5uaxn5t" );
700 assert_eq! ( coder
. encode_long ( 1 << 100 ), Err ( ValueTooLarge
));
701 assert_eq! ( coder
. encode_long ( u128
:: MAX
), Err ( ValueTooLarge
));
703 // Test misencodings: 0 is w2ej, but if 0 were encrypted with n=15 instead of n=10,
704 // it’d be m2eig5—so that value isn’t allowed.
705 // Specific value found with:
706 // panic!("{}", InlineString { buf: base32::encode(fpeck::encrypt(&coder.expanded_key, 15, 0)), start: 20 - 6 });
707 assert_eq! ( coder
. decode ( "m2eig5" ), Err ( DecodeError
:: OverlyLongEncoding
));
709 // … but slightly changed values are probably valid (since only one in 2¹⁰ is invalid).
710 assert_eq! ( coder
. decode ( "m2eig6" ), Ok ( 473063752 ));
712 // Also a few more at the boundaries for confidence:
713 // LAST_4 (2²⁰−1) but encoded with n=15 instead of n=10
714 assert_eq! ( coder
. decode ( "vf5fem" ), Err ( DecodeError
:: OverlyLongEncoding
));
715 // LAST_6 (2³⁰−1) but encoded with n=20 instead of n=15
716 assert_eq! ( coder
. decode ( "ixs6h9ma" ), Err ( DecodeError
:: OverlyLongEncoding
));
717 // LAST_6 (2³⁰−1) but encoded with n=50 instead of n=10
718 assert_eq! ( coder
. decode ( "uhkprgrirp45pe54twsa" ), Err ( DecodeError
:: OverlyLongEncoding
));
720 // Bad string lengths
721 assert_eq! ( coder
. decode ( "" ), Err ( DecodeError
:: BadLength
));
722 assert_eq! ( coder
. decode ( "2" ), Err ( DecodeError
:: BadLength
));
723 assert_eq! ( coder
. decode ( "22" ), Err ( DecodeError
:: BadLength
));
724 assert_eq! ( coder
. decode ( "222" ), Err ( DecodeError
:: BadLength
));
725 assert_eq! ( coder
. decode ( "22222" ), Err ( DecodeError
:: BadLength
));
726 assert_eq! ( coder
. decode_long ( "2222222222222222222" ), Err ( DecodeError
:: BadLength
));
727 assert_eq! ( coder
. decode_long ( "222222222222222222222" ), Err ( DecodeError
:: BadLength
));
729 // … but just so it’s clear, ones are fine, it was just the lengths that were wrong.
730 case! ( coder
, u64 173734 , "2222" );
731 case! ( coder
, u64 592178178 , "222222" );
732 case! ( coder
, 111515659577240532774228475483 , "22222222222222222222" );
734 // Now time for some tagging.
735 case! ( coder
, sparsity
= 1 , discriminant
= 0 , split
= assert_eq
, id
= 0 , tesid
= "w2ej" );
736 case! ( coder
, sparsity
= 1 , discriminant
= 1 , split
= assert_ne
, id
= 0 , tesid
= "w6um" );
737 case! ( coder
, sparsity
= 1 , discriminant
= 2 , split
= assert_ne
, id
= 0 , tesid
= "x45g" );
739 case! ( coder
, sparsity
= 100 , discriminant
= 0 , split
= assert_eq
, id
= 0 , tesid
= "w2ej" );
740 case! ( coder
, sparsity
= 100 , discriminant
= 1 , split
= assert_eq
, id
= 0 , tesid
= "w6um" );
741 case! ( coder
, sparsity
= 100 , discriminant
= 2 , split
= assert_eq
, id
= 0 , tesid
= "x45g" );
742 case! ( coder
, sparsity
= 100 , discriminant
= 0 , split
= assert_eq
, id
= 1 , tesid
= "ypbn" );
743 case! ( coder
, sparsity
= 100 , discriminant
= 1 , split
= assert_eq
, id
= 1 , tesid
= "k9pw" );
744 case! ( coder
, sparsity
= 100 , discriminant
= 2 , split
= assert_eq
, id
= 1 , tesid
= "b7nc" );
745 case! ( coder
, sparsity
= 100 , discriminant
= 0 , split
= assert_eq
, id
= 2 , tesid
= "r9yc" );
746 case! ( coder
, sparsity
= 100 , discriminant
= 1 , split
= assert_eq
, id
= 2 , tesid
= "arf2" );
747 case! ( coder
, sparsity
= 100 , discriminant
= 2 , split
= assert_eq
, id
= 2 , tesid
= "z6wh" );
748 case! ( coder
, 000 , "w2ej" );
749 case! ( coder
, 001 , "w6um" );
750 case! ( coder
, 002 , "x45g" );
751 case! ( coder
, 100 , "ypbn" );
752 case! ( coder
, 101 , "k9pw" );
753 case! ( coder
, 102 , "b7nc" );
754 case! ( coder
, 200 , "r9yc" );
755 case! ( coder
, 201 , "arf2" );
756 case! ( coder
, 202 , "z6wh" );
757 // The highest sparsity that’s always valid: 2³⁶ − 1
758 case! ( coder
, sparsity
=( 1 << 36 ) - 1 , discriminant
= u64 :: MAX
, split
= assert_ne
, id
= u64 :: MAX
, tesid
= "fjwz5jk3p4gz9aqes22e" );
759 case! ( coder
, 1267650600228229401427983728640 , "fjwz5jk3p4gz9aqes22e" );
764 fn bench_init ( b
: & mut test
:: Bencher
) {
766 TesidCoder
:: new ( test
:: black_box ( "000102030405060708090a0b0c0d0e0f" )). unwrap ();
772 fn bench_encode ( b
: & mut test
:: Bencher
) {
773 let coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
775 // This benchmark is deliberately ordered to require a different expanded key every
776 // time, in order to mildly thwart caching and provide a *somewhat* more realistic
777 // result. When it used the same expanded key twice in a row (FIRST_4, LAST_4, FIRST_6,
778 // LAST_6, &c.) it took around 1000ns. Once I made it use a different expanded key each
779 // time, that increased to around 1770ns, which is under 100ns per TESID, hopefully not
780 // *too* unrealistic a general figure, but it is of course not completely realistic.
781 for i
in test
:: black_box ([
802 coder
. encode_long ( i
). unwrap ();
807 // Indicative figure: 82ms, meaning ~80ns per encode.
810 fn bench_encode_all_four_character_tesids ( b
: & mut test
:: Bencher
) {
811 let coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
812 b
. iter (|| for i
in 0 .. 0b100000000000000000000 {
813 test
:: black_box ( coder
. encode ( test
:: black_box ( i
)));
817 // Indicative figure: 210ms, meaning ~200ns per encode-and-decode, suggesting decode is slower
821 fn bench_encode_and_decode_all_four_character_tesids ( b
: & mut test
:: Bencher
) {
822 let coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
823 b
. iter (|| for i
in 0 .. 0b100000000000000000000 {
824 coder
. decode (& coder
. encode ( test
:: black_box ( i
))). unwrap ();
828 #[cfg(feature = "std" )]
830 #[cfg_attr(debug_assertions, ignore)] // Takes a few seconds in debug mode (<0.4s in release).
831 // Very unscientific approach: a single run of this takes under 0.4s, which is under 400ns per
832 // iteration, which includes an encode, a decode, and a HashSet insert.
833 fn show_uniqueness_of_four_character_tesids () {
834 let coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
835 let mut seen
= std
:: collections
:: HashSet
:: new ();
836 for i
in 0 .. 0b100000000000000000000 {
837 let string
= coder
. encode ( i
);
838 assert_eq! ( coder
. decode (& string
). unwrap (), i
);
839 assert! ( string
. len () == 4 );
840 assert! ( seen
. insert ( string
));
846 let mut coder
= TesidCoder
:: new ( "000102030405060708090a0b0c0d0e0f" ). unwrap ();
848 ptr
:: drop_in_place (& mut coder
);
850 assert_eq! ( coder
. expanded_key
, [ 0 ; 30 ]);