+//! TESID: Textualised Encrypted Sequential Identifiers
+//!
+//! See <https://chrismorgan.info/tesid/> for a description of what it’s all about, and how to best
+//! use it.
+//!
+//! # Example
+//!
+//! ```rust
+//! let secret_key = "000102030405060708090a0b0c0d0e0f";
+//! let coder = tesid::TesidCoder::new(secret_key).unwrap();
+//! assert_eq!(&*coder.encode(0), "w2ej");
+//! assert_eq!(&*coder.encode(1), "w6um");
+//! assert_eq!(&*coder.encode(2), "x45g");
+//! assert_eq!(&*coder.encode(2_u64.pow(20) - 1), "atcw");
+//! assert_eq!(&*coder.encode(2_u64.pow(20)), "8qwm6y");
+//! assert_eq!(&*coder.encode(2_u64.pow(30) - 1), "3eipc7");
+//! assert_eq!(&*coder.encode(2_u64.pow(30)), "n3md95r4");
+//! assert_eq!(&*coder.encode_long(2_u128.pow(100) - 1).unwrap(), "ia2bvpjaiju7g5uaxn5t");
+//! assert_eq!(coder.encode_long(2_u128.pow(100)), Err(tesid::ValueTooLarge));
+//!
+//! assert_eq!(coder.decode("w2ej"), Ok(0));
+//! ```
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
+#![allow(clippy::eq_op, clippy::identity_op)] // `20 - 20` and `input >> 0`, good for symmetry.
+
+// To run benchmarks: `RUSTFLAGS="--cfg bench" cargo +nightly bench`
+#![cfg_attr(bench, feature(test))]
+#[cfg(bench)] extern crate test;
+
+use core::fmt;
+use core::marker::PhantomData;
+use core::ptr;
+use core::sync::atomic;
+
+mod base32;
+mod fpeck;
+mod inline_string;
+
+pub use inline_string::InlineString;
+
+/// The error type when decoding of a TESID failed.
+///
+/// Decoding can fail for a number of reasons; most are things you can’t do anything about.
+/// `WrongDiscriminant` is the only one that’s likely to be genuinely useful,
+/// for enhancing error messages.
+///
+/// Because of the typical opaqueness of TESID decode errors,
+/// The `fmt::Display` implementation doesn’t try to say anything useful, just “invalid TESID”.
+///
+#[cfg_attr(feature = "std", doc = "Since <span class='stab portability'>**crate feature `std`**</span> is enabled, this type implements [`std::error::Error`].
+(Though if you’re using `TypedTesidCoder`, you will need to make sure that your type discriminant type implements [`core::fmt::Debug`].)")]
+#[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>.")]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DecodeError<Discriminant> {
+ /// The TESID was not 4, 6, 8, 10, 12, 14, 16, 18 or 20 characters long.
+ BadLength,
+
+ /// The TESID contained characters not in the defined base-32 alphabet.
+ BadCharacters,
+
+ /// The TESID was an improperly-encoded value, using the wrong length and cipher,
+ /// e.g. a ten-character string that decodes to 0, but 0 should be four characters.
+ /// This cannot happen normally and suggests the user is trying to enumerate IDs.
+ OverlyLongEncoding,
+
+ /// The value was not in the acceptable range for the type,
+ /// e.g. ≥2⁶⁴ in a u64, or <0 via discriminant.
+ ///
+ /// Note that id must be u64 where sparsity or discrimination are used.
+ OutOfRange,
+
+ /// The TESID did not match the discriminant requested.
+ /// This assumes that the sparsity was correct.
+ /// The actual discriminant and ID found are provided.
+ ///
+ /// This variant is basically the just-for-enhancing-error-reporting version of
+ /// [`TesidCoder::split_decode`] or [`TypedTesidCoder::split_decode`],
+ /// which you should use instead if you want to actively support diverse discriminants.
+ WrongDiscriminant {
+ id: u64,
+ discriminant: Discriminant,
+ },
+
+ /// Via [`TypedTesidCoder`] only: the TESID’s discriminant wasn’t an acceptable value.
+ MalformedDiscriminant {
+ id: u64,
+ discriminant: u64,
+ },
+}
+
+impl<Discriminant> fmt::Display for DecodeError<Discriminant> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("invalid TESID")
+ }
+}
+
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl<Discriminant: fmt::Debug> std::error::Error for DecodeError<Discriminant> {}
+
+/// The error type when encoding of a TESID failed.
+///
+/// Encoding can fail when `encode_long` is used with a value of 2¹²⁰ or more.
+///
+#[cfg_attr(feature = "std", doc = "Since <span class='stab portability'>**crate feature `std`**</span> is enabled, this type implements [`std::error::Error`].")]
+#[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>.")]
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ValueTooLarge;
+
+impl fmt::Display for ValueTooLarge {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("cannot TESID-encode a value 2¹⁰⁰ or above")
+ }
+}
+
+#[cfg(feature = "std")]
+#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+impl std::error::Error for ValueTooLarge {}
+
+/// The TESID coder.
+#[derive(Clone)]
+pub struct TesidCoder {
+ expanded_key: [u64; 30],
+}
+
+impl Drop for TesidCoder {
+ fn drop(&mut self) {
+ // Might as well zero the memory in which lies the expanded key, as good security practice.
+ // SAFETY: the pointer is trivially valid and aligned, and not accessed concurrently.
+ unsafe { ptr::write_volatile(&mut self.expanded_key, Default::default()); }
+ atomic::compiler_fence(atomic::Ordering::SeqCst);
+ }
+}
+
+impl TesidCoder {
+ /// Initialise a TESID coder with a key in hexadecimal string representation.
+ ///
+ /// The key string must be made up of exactly 32 lowercase hexadecimal (0-9a-f) characters,
+ /// and should have been generated randomly.
+ /// Refer to external documentation for advice on key generation.
+ ///
+ /// ```
+ /// tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// ```
+ pub fn new(key: &str) -> Option<Self> {
+ // from_str_radix plus checks to remove its undesired functionality (leading +, len != 16, A-F)
+ // won’t be quite as fast as possible, but the difference should be slight enough.
+ if key.len() != 32 || key.bytes().any(|b| !matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
+ return None;
+ }
+ let key = u128::from_str_radix(key, 16).ok()?;
+ Some(TesidCoder {
+ expanded_key: fpeck::expand(key),
+ })
+ }
+
+ /// Encode an ID.
+ ///
+ /// ```rust
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// assert_eq!(&*coder.encode(0), "w2ej");
+ /// ```
+ pub fn encode(&self, id: u64) -> InlineString<20> {
+ self.encode_long(id as u128).unwrap()
+ }
+
+ /// Encode an ID, with sparsity and/or discrimination.
+ ///
+ /// This will panic if `id * sparsity + discriminant` is 2¹⁰⁰ or higher, which can only happen
+ /// if `sparsity` is 2³⁶ or higher (which would be sparsity of one in 68 billion, adding around
+ /// 7.2 characters to the TESID, extreme overkill for all reasonable scenarios—and that’s why
+ /// this method panics rather than returning a Result, because panicking just about guarantees
+ /// that you’re doing the wrong thing; yet I haven’t artificially capped `sparsity`, as you
+ /// *can* correctly use it with a higher value, so long as `id` doesn’t get too high).
+ // (Well, actually I kinda have artificially limited all three parameters to u64.)
+ ///
+ /// ```rust
+ /// // Define constants for consistent use:
+ /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
+ /// const TYPE_A: u64 = 0;
+ /// const TYPE_B: u64 = 1;
+ /// const TYPE_C: u64 = 2;
+ ///
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ ///
+ /// // id 0 with three different discriminants/tags:
+ /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_A), "w2ej");
+ /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_B), "w6um");
+ /// assert_eq!(&*coder.encode_with_tag(0, TYPE_SPARSITY, TYPE_C), "x45g");
+ ///
+ /// // id 1 with three different discriminants/tags:
+ /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_A), "dh2h");
+ /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_B), "a6xy");
+ /// assert_eq!(&*coder.encode_with_tag(1, TYPE_SPARSITY, TYPE_C), "7xgj");
+ /// ```
+ pub fn encode_with_tag(&self, id: u64, sparsity: u64, discriminant: u64) -> InlineString<20> {
+ let id = id as u128;
+ id.checked_mul(sparsity as u128)
+ .and_then(|id| id.checked_add(discriminant as u128))
+ .and_then(|id| self.encode_long(id).ok())
+ .expect("TesidCoder::encode_with_tag used badly")
+ }
+
+ /// Attempt to encode a u128 ID. This will return an error if the ID is 2¹⁰⁰ or greater.
+ ///
+ /// ```rust
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// assert_eq!(&*coder.encode_long(1<<70).unwrap(), "zk9d3setywjf7uwu");
+ /// assert_eq!(coder.encode_long(1<<100), Err(tesid::ValueTooLarge));
+ /// ```
+ pub fn encode_long(&self, id: u128) -> Result<InlineString<20>, ValueTooLarge> {
+ // Maybe in the future I’ll be able to do an inline const pattern, but for now—
+ const MIN_10: u128 = 0 ; const MAX_10: u128 = (1 << 20) - 1;
+ const MIN_15: u128 = 1 << 20; const MAX_15: u128 = (1 << 30) - 1;
+ const MIN_20: u128 = 1 << 30; const MAX_20: u128 = (1 << 40) - 1;
+ const MIN_25: u128 = 1 << 40; const MAX_25: u128 = (1 << 50) - 1;
+ const MIN_30: u128 = 1 << 50; const MAX_30: u128 = (1 << 60) - 1;
+ const MIN_35: u128 = 1 << 60; const MAX_35: u128 = (1 << 70) - 1;
+ const MIN_40: u128 = 1 << 70; const MAX_40: u128 = (1 << 80) - 1;
+ const MIN_45: u128 = 1 << 80; const MAX_45: u128 = (1 << 90) - 1;
+ const MIN_50: u128 = 1 << 90; const MAX_50: u128 = (1 << 100) - 1;
+ const MIN_TOO_LARGE: u128 = 1 << 100;
+ let (n, start) = match id {
+ MIN_10..=MAX_10 => (10, 20 - 4),
+ MIN_15..=MAX_15 => (15, 20 - 6),
+ MIN_20..=MAX_20 => (20, 20 - 8),
+ MIN_25..=MAX_25 => (25, 20 - 10),
+ MIN_30..=MAX_30 => (30, 20 - 12),
+ MIN_35..=MAX_35 => (35, 20 - 14),
+ MIN_40..=MAX_40 => (40, 20 - 16),
+ MIN_45..=MAX_45 => (45, 20 - 18),
+ MIN_50..=MAX_50 => (50, 20 - 20),
+ MIN_TOO_LARGE.. => return Err(ValueTooLarge),
+ };
+
+ // SAFETY: base32::encode guarantees its output is ASCII; and start is less than buf.len().
+ // (start lets us take the padding we want and discard the rest.)
+ Ok(InlineString {
+ buf: base32::encode(fpeck::encrypt(&self.expanded_key, n, id)),
+ start,
+ })
+ }
+
+ /// Decode a 64-bit ID.
+ ///
+ /// This is just `decode_long` followed by `u64::try_from`, but I wanted this for symmetry with
+ /// `encode`/`encode_long` which are separated because of the fallibility of the latter. By the
+ /// same token, I suppose if I just provided a u128-yielding method, then it’d be too tempting
+ /// for users to use the faulty `as u64`. Never mind that you might want an `i64` anyway (since
+ /// SQL databases don’t do unsigned integers very well) and thus will have to do the same
+ /// thing. Ah well, you can’t win them all. I got my symmetry.
+ // I thought briefly about a generic API so that you can .decode::<u128 | u64 | i64 | …>(…),
+ // but decided that would be too often too painful to use. .decode_u128(…) / .decode_u64(…) /
+ // .decode_i64(…) would probably be better if I were going that way.
+ ///
+ /// ```rust
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// assert_eq!(coder.decode("w2ej"), Ok(0));
+ /// ```
+ pub fn decode(&self, tesid: &str) -> Result<u64, DecodeError<u64>> {
+ self.decode_long(tesid).and_then(|id| id.try_into().map_err(|_| DecodeError::OutOfRange))
+ }
+
+ /// Decode a 128-bit ID.
+ ///
+ /// ```rust
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// assert_eq!(coder.decode_long("zk9d3setywjf7uwu"), Ok(1<<70));
+ /// ```
+ pub fn decode_long(&self, tesid: &str) -> Result<u128, DecodeError<u64>> {
+ let (n, minimum) = match tesid.len() {
+ 4 => (10, 0),
+ 6 => (15, 1 << 20),
+ 8 => (20, 1 << 30),
+ 10 => (25, 1 << 40),
+ 12 => (30, 1 << 50),
+ 14 => (35, 1 << 60),
+ 16 => (40, 1 << 70),
+ 18 => (45, 1 << 80),
+ 20 => (50, 1 << 90),
+ _ => return Err(DecodeError::BadLength),
+ };
+ let number = base32::decode(tesid).map_err(|()| DecodeError::BadCharacters)?;
+ let id = fpeck::decrypt(&self.expanded_key, n, number);
+ if id >= minimum {
+ Ok(id)
+ } else {
+ Err(DecodeError::OverlyLongEncoding)
+ }
+ }
+
+ /// Decode an ID that was encoded with sparsity and/or discrimination.
+ ///
+ /// See [`TesidCoder::encode_with_tag`] for a description of `sparsity` and `discriminant`.
+ ///
+ /// If you want to decode supporting any discriminant, see [`TesidCoder::split_decode`].
+ ///
+ /// ```rust
+ /// // Define constants for consistent use:
+ /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
+ /// const TYPE_A: u64 = 0;
+ /// const TYPE_B: u64 = 1;
+ /// const TYPE_C: u64 = 2;
+ ///
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ ///
+ /// // id 0 with three different discriminants/tags:
+ /// assert_eq!(coder.decode_with_tag("w2ej", TYPE_SPARSITY, TYPE_A), Ok(0));
+ /// assert_eq!(coder.decode_with_tag("w6um", TYPE_SPARSITY, TYPE_B), Ok(0));
+ /// assert_eq!(coder.decode_with_tag("x45g", TYPE_SPARSITY, TYPE_C), Ok(0));
+ ///
+ /// // id 1 with three different discriminants/tags:
+ /// assert_eq!(coder.decode_with_tag("dh2h", TYPE_SPARSITY, TYPE_A), Ok(1));
+ /// assert_eq!(coder.decode_with_tag("a6xy", TYPE_SPARSITY, TYPE_B), Ok(1));
+ /// assert_eq!(coder.decode_with_tag("7xgj", TYPE_SPARSITY, TYPE_C), Ok(1));
+ ///
+ /// // And you can’t decode an ID with the wrong discriminant (presuming correct sparsity):
+ /// assert_eq!(coder.decode_with_tag("w2ej", TYPE_SPARSITY, TYPE_C),
+ /// Err(tesid::DecodeError::WrongDiscriminant { id: 0, discriminant: 0 }));
+ /// ```
+ pub fn decode_with_tag(&self, tesid: &str, sparsity: u64, discriminant: u64)
+ -> Result<u64, DecodeError<u64>> {
+ // This deliberately DOESN’T use split_decode, which requires sparsity > discriminant,
+ // which I don’t want to be required here.
+ let id = self.decode_long(tesid)?;
+ if let Some(id) = id.checked_sub(discriminant as u128) {
+ if id % sparsity as u128 == 0 {
+ return u64::try_from(id / sparsity as u128)
+ .map_err(|_| DecodeError::OutOfRange);
+ }
+ }
+ if sparsity <= discriminant {
+ // This suggests discriminant is just being used for an offset,
+ // not as a type tag or similar. In this situation, the discriminant is irrepairable,
+ // so we just declare it bad and wash our hands of it.
+ Err(DecodeError::OutOfRange)
+ } else {
+ Err(DecodeError::WrongDiscriminant {
+ id: u64::try_from(id / sparsity as u128).map_err(|_| DecodeError::OutOfRange)?,
+ discriminant: id as u64 % sparsity,
+ })
+ }
+ }
+
+ /// Decode an ID that was encoded with certain sparsity,
+ /// separating the discriminant and returning it alongside the ID.
+ ///
+ /// This is useful if you want to accept various discriminants;
+ /// one simple use case is better error reporting:
+ /// “that’s an ID for type A, but this takes IDs for type B”.
+ ///
+ /// This allows *you* to identify the discriminant, but due to the encryption,
+ /// anyone who has only the ID cannot;
+ /// if you want users to be able to discern the discriminant,
+ /// consider adding a human-friendly prefix to the ID;
+ /// I like a single uppercase letter or a word followed by an underscore.
+ ///
+ /// This requires that the discriminant be less than the sparsity,
+ /// or incorrect values will be produced.
+ // Note the use of u64 here. This also fits into the symmetry I was talking of. If you happen
+ // to want split_decode for IDs 2⁶⁴ or higher, well, here’s the implementation, it’s simple.
+ ///
+ /// ```rust
+ /// // Define constants for consistent use:
+ /// const TYPE_SPARSITY: u64 = 256; // meaning up to 256 possible types
+ /// const TYPE_A: u64 = 0;
+ /// const TYPE_B: u64 = 1;
+ /// const TYPE_C: u64 = 2;
+ ///
+ /// let coder = tesid::TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ ///
+ /// // id 0 with three different discriminants/tags:
+ /// assert_eq!(coder.split_decode("w2ej", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_A }));
+ /// assert_eq!(coder.split_decode("w6um", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_B }));
+ /// assert_eq!(coder.split_decode("x45g", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 0, discriminant: TYPE_C }));
+ ///
+ /// // id 1 with three different discriminants/tags:
+ /// assert_eq!(coder.split_decode("dh2h", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_A }));
+ /// assert_eq!(coder.split_decode("a6xy", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_B }));
+ /// assert_eq!(coder.split_decode("7xgj", TYPE_SPARSITY),
+ /// Ok(tesid::SplitDecode { id: 1, discriminant: TYPE_C }));
+ /// ```
+ pub fn split_decode(&self, tesid: &str, sparsity: u64)
+ -> Result<SplitDecode<u64>, DecodeError<u64>> {
+ let id = self.decode_long(tesid)?;
+ Ok(SplitDecode {
+ id: u64::try_from(id / sparsity as u128)
+ .map_err(|_| DecodeError::OutOfRange)?,
+ discriminant: id as u64 % sparsity,
+ })
+ }
+}
+
+impl fmt::Debug for TesidCoder {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // Hide the expanded key.
+ f.debug_struct("TesidCoder").finish_non_exhaustive()
+ }
+}
+
+/// The output of [`TesidCoder::split_decode`], separating ID and discriminant.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SplitDecode<Discriminant> {
+ pub id: u64,
+ pub discriminant: Discriminant,
+}
+
+/// A trait to implement for type enums for their use with [`TypedTesidCoder`].
+///
+/// See [`define_type_discriminant!`] for the usual way of defining types that implement this.
+pub trait TypeDiscriminant: Sized {
+ /// The value to use for sparsity. This must exceed the highest discriminant,
+ /// or discriminants equal or greater will decode incorrectly.
+ const SPARSITY: u64;
+ fn into_discriminant(self) -> u64;
+ fn from_discriminant(discriminant: u64) -> Option<Self>;
+}
+
+// It would have been nice to impl TypeDiscriminant for u64,
+// but I would have had to shift sparsity to a runtime parameter,
+// and I don’t want to lose its place as a const on the impl.
+
+/// Define a type enum for use with `TypedTesidCoder`.
+///
+/// This is just shorthand for defining the C-style enum written,
+/// and implementing [`TypeDiscriminant`] in the obvious way on it.
+/// If it’s not to your liking, you can easily do that manually.
+///
+/// You might have expected a derive macro, `#[derive(tesid::TypeDiscriminant)]` with
+/// `#[tesid(sparsity = 256)]` or similar, but I’ve stuck with a structural macro
+/// for faster compilation and to keep zero dependencies.
+///
+/// Example:
+///
+/// ```
+/// tesid::define_type_discriminant!(sparsity=256,
+/// #[non_exhaustive]
+/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
+/// pub enum Type {
+/// A = 0,
+/// B = 1,
+/// C = 2,
+/// }
+/// );
+/// ```
+#[macro_export]
+macro_rules! define_type_discriminant {
+ (sparsity = $sparsity:expr,
+ $(#[$attr:meta])* $vis:vis enum $name:ident {
+ $($variant_name:ident = $variant_value:expr),* $(,)?
+ }) => {
+ $(#[$attr])* $vis enum $name {
+ $($variant_name = $variant_value),*
+ }
+
+ impl $crate::TypeDiscriminant for $name {
+ const SPARSITY: u64 = $sparsity;
+
+ fn into_discriminant(self) -> u64 {
+ self as u64
+ }
+
+ fn from_discriminant(discriminant: u64) -> Option<Self> {
+ match discriminant {
+ $($variant_value => Some(Self::$variant_name),)*
+ _ => None,
+ }
+ }
+ }
+ };
+}
+
+/// A TESID coder with type discrimination baked in.
+///
+/// All method examples will assume the following setup:
+///
+/// ```rust
+/// use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
+///
+/// define_type_discriminant!(sparsity=256,
+/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
+/// #[non_exhaustive]
+/// pub enum Type {
+/// A = 0,
+/// B = 1,
+/// C = 2,
+/// }
+/// );
+///
+/// let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+/// let coder = TypedTesidCoder::<Type>::new(coder);
+/// ```
+///
+/// See [`define_type_discriminant!`] for the usual way of defining a type for `D`.
+#[derive(Clone)]
+pub struct TypedTesidCoder<D: TypeDiscriminant> {
+ coder: TesidCoder,
+ marker: PhantomData<D>,
+}
+
+impl<D: TypeDiscriminant> fmt::Debug for TypedTesidCoder<D> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("TypedTesidCoder").finish_non_exhaustive()
+ }
+}
+
+impl<D: TypeDiscriminant> TypedTesidCoder<D> {
+ /// Initialise a typed TESID coder.
+ ///
+ /// This takes a [`TesidCoder`] (rather than a key) so that you can clone an existing coder,
+ /// if you don’t always use the one sparsity and type enum.
+ pub fn new(coder: TesidCoder) -> Self {
+ Self { coder, marker: PhantomData }
+ }
+
+ /// Encode an ID with a specific type.
+ ///
+ /// ```rust
+ /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
+ /// # define_type_discriminant!(sparsity=256,
+ /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
+ /// # pub enum Type { A = 0, B = 1, C = 2, });
+ /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// # let coder = TypedTesidCoder::<Type>::new(coder);
+ /// assert_eq!(&*coder.encode(Type::A, 0), "w2ej");
+ /// assert_eq!(&*coder.encode(Type::B, 0), "w6um");
+ /// assert_eq!(&*coder.encode(Type::A, 1), "dh2h");
+ /// ```
+ pub fn encode(&self, r#type: D, id: u64) -> InlineString<20> {
+ self.coder.encode_with_tag(id, D::SPARSITY, r#type.into_discriminant())
+ }
+
+ /// Decode an ID with a specific type.
+ ///
+ /// ```rust
+ /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
+ /// # define_type_discriminant!(sparsity=256,
+ /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
+ /// # pub enum Type { A = 0, B = 1, C = 2, });
+ /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// # let coder = TypedTesidCoder::<Type>::new(coder);
+ /// assert_eq!(coder.decode(Type::A, "w2ej"), Ok(0));
+ /// assert_eq!(coder.decode(Type::B, "w6um"), Ok(0));
+ /// assert_eq!(coder.decode(Type::A, "dh2h"), Ok(1));
+ /// assert_eq!(coder.decode(Type::A, "w6um"),
+ /// Err(DecodeError::WrongDiscriminant { id: 0, discriminant: Type::B }));
+ /// ```
+ pub fn decode(&self, r#type: D, tesid: &str) -> Result<u64, DecodeError<D>> {
+ self.coder.decode_with_tag(tesid, D::SPARSITY, r#type.into_discriminant())
+ .map_err(Self::convert_decode_error)
+ }
+
+ /// Decode an ID and type and return both.
+ ///
+ /// ```rust
+ /// # use tesid::{TesidCoder, TypedTesidCoder, define_type_discriminant, DecodeError, SplitDecode};
+ /// # define_type_discriminant!(sparsity=256,
+ /// # #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive]
+ /// # pub enum Type { A = 0, B = 1, C = 2, });
+ /// # let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ /// # let coder = TypedTesidCoder::<Type>::new(coder);
+ /// assert_eq!(coder.split_decode("w2ej"), Ok(SplitDecode { id: 0, discriminant: Type::A }));
+ /// assert_eq!(coder.split_decode("w6um"), Ok(SplitDecode { id: 0, discriminant: Type::B }));
+ /// assert_eq!(coder.split_decode("dh2h"), Ok(SplitDecode { id: 1, discriminant: Type::A }));
+ /// assert_eq!(coder.split_decode("6mqv"),
+ /// Err(DecodeError::MalformedDiscriminant { id: 0, discriminant: 3 }));
+ /// ```
+ pub fn split_decode(&self, tesid: &str) -> Result<SplitDecode<D>, DecodeError<D>> {
+ self.coder.split_decode(tesid, D::SPARSITY)
+ .and_then(|SplitDecode { id, discriminant }| Ok(SplitDecode {
+ id,
+ discriminant: D::from_discriminant(discriminant)
+ .ok_or(DecodeError::MalformedDiscriminant { id, discriminant })?,
+ }))
+ .map_err(Self::convert_decode_error)
+ }
+
+ /// `DecodeError<u64>` → `DecodeError<D>`, just redoing `WrongDiscriminant`.
+ fn convert_decode_error(e: DecodeError<u64>) -> DecodeError<D> {
+ match e {
+ DecodeError::BadLength => DecodeError::BadLength,
+ DecodeError::BadCharacters => DecodeError::BadCharacters,
+ DecodeError::OverlyLongEncoding => DecodeError::OverlyLongEncoding,
+ DecodeError::OutOfRange => DecodeError::OutOfRange,
+ DecodeError::WrongDiscriminant { id, discriminant } => {
+ match D::from_discriminant(discriminant) {
+ Some(discriminant) => DecodeError::WrongDiscriminant { id, discriminant },
+ None => DecodeError::MalformedDiscriminant { id, discriminant },
+ }
+ },
+ DecodeError::MalformedDiscriminant { id, discriminant } =>
+ DecodeError::MalformedDiscriminant { id, discriminant },
+ }
+ }
+}
+
+// I don’t want to use the README as the crate docs,
+// but I can still run the examples in it!
+#[cfg_attr(doctest, doc = include_str!("../README.md"))]
+fn _readme_doctests() {}
+
+#[cfg(test)]
+#[allow(unused_parens)] // The constants, for consistency
+mod tests {
+ use super::*;
+
+ // 19 known values, 12 u64 and 7 u128.
+ const FIRST_4: u64 = 0 ;
+ const LAST_4: u64 = (1 << 20) - 1;
+ const FIRST_6: u64 = (1 << 20) ;
+ const LAST_6: u64 = (1 << 30) - 1;
+ const FIRST_8: u64 = (1 << 30) ;
+ const LAST_8: u64 = (1 << 40) - 1;
+ const FIRST_10: u64 = (1 << 40) ;
+ const LAST_10: u64 = (1 << 50) - 1;
+ const FIRST_12: u64 = (1 << 50) ;
+ const LAST_12: u64 = (1 << 60) - 1;
+ const FIRST_14: u64 = (1 << 60) ;
+ const MID_14: u64 = u64::MAX ;
+ const LAST_14: u128 = (1 << 70) - 1;
+ const FIRST_16: u128 = (1 << 70) ;
+ const LAST_16: u128 = (1 << 80) - 1;
+ const FIRST_18: u128 = (1 << 80) ;
+ const LAST_18: u128 = (1 << 90) - 1;
+ const FIRST_20: u128 = (1 << 90) ;
+ const LAST_20: u128 = (1 << 100) - 1;
+
+ #[test]
+ fn tests() {
+ macro_rules! case {
+ ($coder:ident, sparsity=$sparsity:expr, discriminant=$discriminant:expr, split=$split:ident, id=$number:expr, tesid=$string:expr) => {
+ assert_eq!(&*$coder.encode_with_tag($number, $sparsity, $discriminant), $string);
+ assert_eq!($coder.decode_with_tag($string, $sparsity, $discriminant), Ok($number));
+ $split!($coder.split_decode($string, $sparsity), Ok(SplitDecode { id: $number, discriminant: $discriminant }));
+ //println!("{}", $coder.encode_with_tag($number, $sparsity, $discriminant));
+ };
+ ($coder:ident, u64 $number:expr, $string:expr) => {
+ // Test it with both the long and normal methods.
+ case!($coder, $number as u128, $string);
+ assert_eq!(&*$coder.encode($number), $string);
+ assert_eq!($coder.decode($string), Ok($number));
+ };
+ ($coder:ident, $number:expr, $string:expr) => {
+ assert_eq!(&*$coder.encode_long($number).unwrap(), $string);
+ assert_eq!($coder.decode_long($string), Ok($number));
+ //println!("{}", $coder.encode_long($number).unwrap());
+ };
+ }
+
+ let coder = TesidCoder::new("00000000000000000000000000000000").unwrap();
+ case!(coder, u64 FIRST_4 , "4kcc");
+ case!(coder, u64 LAST_4 , "3rck");
+ case!(coder, u64 FIRST_6 , "ju2sgs");
+ case!(coder, u64 LAST_6 , "zangyh");
+ case!(coder, u64 FIRST_8 , "2aux4u3h");
+ case!(coder, u64 LAST_8 , "3cd7rc4h");
+ case!(coder, u64 FIRST_10, "m8669y33k6");
+ case!(coder, u64 LAST_10, "45e9rbrvvu");
+ case!(coder, u64 FIRST_12, "t47yf553iv8t");
+ case!(coder, u64 LAST_12, "cwd8t75epzje");
+ case!(coder, u64 FIRST_14, "86hk4d8hj4yvcy");
+ case!(coder, u64 MID_14, "sirnf2k2d2m3bm");
+ case!(coder, LAST_14, "m77g4ezr3e8qay");
+ case!(coder, FIRST_16, "43xf2jj6r6qm8bw4");
+ case!(coder, LAST_16, "6h3wb7wytjr5tbrd");
+ case!(coder, FIRST_18, "4vumjq33d8iiwaharq");
+ case!(coder, LAST_18, "qd7s3csnc5yfrrud5t");
+ case!(coder, FIRST_20, "jd3vsipfn69ia72chuvx");
+ case!(coder, LAST_20, "628fg5kyid3z2vf2j4tf");
+
+ let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ case!(coder, u64 FIRST_4 , "w2ej");
+ case!(coder, u64 LAST_4 , "atcw");
+ case!(coder, u64 FIRST_6 , "8qwm6y");
+ case!(coder, u64 LAST_6 , "3eipc7");
+ case!(coder, u64 FIRST_8 , "n3md95r4");
+ case!(coder, u64 LAST_8 , "nnz4z5qb");
+ case!(coder, u64 FIRST_10, "st9fvria97");
+ case!(coder, u64 LAST_10, "qt42fug7hq");
+ case!(coder, u64 FIRST_12, "dykqxtu2ieqi");
+ case!(coder, u64 LAST_12, "h7rhnw6tfhun");
+ case!(coder, u64 FIRST_14, "xb5c8isevin9i3");
+ case!(coder, u64 MID_14, "t62mijffzuvu4e");
+ case!(coder, LAST_14, "n6n8jq6ike9dnj");
+ case!(coder, FIRST_16, "zk9d3setywjf7uwu");
+ case!(coder, LAST_16, "bqqei5vmzkqjfru3");
+ case!(coder, FIRST_18, "z83vvq5u84sit9g7pd");
+ case!(coder, LAST_18, "cpawgn8snjvverxvmp");
+ case!(coder, FIRST_20, "397btwmkh5y7sjz2xu82");
+ case!(coder, LAST_20, "ia2bvpjaiju7g5uaxn5t");
+
+ assert_eq!(coder.encode_long(1 << 100), Err(ValueTooLarge));
+ assert_eq!(coder.encode_long(u128::MAX), Err(ValueTooLarge));
+
+ // Test misencodings: 0 is w2ej, but if 0 were encrypted with n=15 instead of n=10,
+ // it’d be m2eig5—so that value isn’t allowed.
+ // Specific value found with:
+ // panic!("{}", InlineString { buf: base32::encode(fpeck::encrypt(&coder.expanded_key, 15, 0)), start: 20 - 6 });
+ assert_eq!(coder.decode("m2eig5"), Err(DecodeError::OverlyLongEncoding));
+
+ // … but slightly changed values are probably valid (since only one in 2¹⁰ is invalid).
+ assert_eq!(coder.decode("m2eig6"), Ok(473063752));
+
+ // Also a few more at the boundaries for confidence:
+ // LAST_4 (2²⁰−1) but encoded with n=15 instead of n=10
+ assert_eq!(coder.decode("vf5fem"), Err(DecodeError::OverlyLongEncoding));
+ // LAST_6 (2³⁰−1) but encoded with n=20 instead of n=15
+ assert_eq!(coder.decode("ixs6h9ma"), Err(DecodeError::OverlyLongEncoding));
+ // LAST_6 (2³⁰−1) but encoded with n=50 instead of n=10
+ assert_eq!(coder.decode("uhkprgrirp45pe54twsa"), Err(DecodeError::OverlyLongEncoding));
+
+ // Bad string lengths
+ assert_eq!(coder.decode(""), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode("2"), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode("22"), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode("222"), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode("22222"), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode_long("2222222222222222222"), Err(DecodeError::BadLength));
+ assert_eq!(coder.decode_long("222222222222222222222"), Err(DecodeError::BadLength));
+
+ // … but just so it’s clear, ones are fine, it was just the lengths that were wrong.
+ case!(coder, u64 173734, "2222");
+ case!(coder, u64 592178178, "222222");
+ case!(coder, 111515659577240532774228475483, "22222222222222222222");
+
+ // Now time for some tagging.
+ case!(coder, sparsity=1, discriminant=0, split=assert_eq, id=0, tesid="w2ej");
+ case!(coder, sparsity=1, discriminant=1, split=assert_ne, id=0, tesid="w6um");
+ case!(coder, sparsity=1, discriminant=2, split=assert_ne, id=0, tesid="x45g");
+
+ case!(coder, sparsity=100, discriminant=0, split=assert_eq, id=0, tesid="w2ej");
+ case!(coder, sparsity=100, discriminant=1, split=assert_eq, id=0, tesid="w6um");
+ case!(coder, sparsity=100, discriminant=2, split=assert_eq, id=0, tesid="x45g");
+ case!(coder, sparsity=100, discriminant=0, split=assert_eq, id=1, tesid="ypbn");
+ case!(coder, sparsity=100, discriminant=1, split=assert_eq, id=1, tesid="k9pw");
+ case!(coder, sparsity=100, discriminant=2, split=assert_eq, id=1, tesid="b7nc");
+ case!(coder, sparsity=100, discriminant=0, split=assert_eq, id=2, tesid="r9yc");
+ case!(coder, sparsity=100, discriminant=1, split=assert_eq, id=2, tesid="arf2");
+ case!(coder, sparsity=100, discriminant=2, split=assert_eq, id=2, tesid="z6wh");
+ case!(coder, 000, "w2ej");
+ case!(coder, 001, "w6um");
+ case!(coder, 002, "x45g");
+ case!(coder, 100, "ypbn");
+ case!(coder, 101, "k9pw");
+ case!(coder, 102, "b7nc");
+ case!(coder, 200, "r9yc");
+ case!(coder, 201, "arf2");
+ case!(coder, 202, "z6wh");
+ // The highest sparsity that’s always valid: 2³⁶ − 1
+ case!(coder, sparsity=(1<<36) - 1, discriminant=u64::MAX, split=assert_ne, id=u64::MAX, tesid="fjwz5jk3p4gz9aqes22e");
+ case!(coder, 1267650600228229401427983728640, "fjwz5jk3p4gz9aqes22e");
+ }
+
+ #[cfg(bench)]
+ #[bench]
+ fn bench_init(b: &mut test::Bencher) {
+ b.iter(|| {
+ TesidCoder::new(test::black_box("000102030405060708090a0b0c0d0e0f")).unwrap();
+ });
+ }
+
+ #[cfg(bench)]
+ #[bench]
+ fn bench_encode(b: &mut test::Bencher) {
+ let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ b.iter(|| {
+ // This benchmark is deliberately ordered to require a different expanded key every
+ // time, in order to mildly thwart caching and provide a *somewhat* more realistic
+ // result. When it used the same expanded key twice in a row (FIRST_4, LAST_4, FIRST_6,
+ // LAST_6, &c.) it took around 1000ns. Once I made it use a different expanded key each
+ // time, that increased to around 1770ns, which is under 100ns per TESID, hopefully not
+ // *too* unrealistic a general figure, but it is of course not completely realistic.
+ for i in test::black_box([
+ FIRST_4 as u128,
+ FIRST_6 as u128,
+ FIRST_8 as u128,
+ FIRST_10 as u128,
+ FIRST_12 as u128,
+ FIRST_14 as u128,
+ FIRST_16,
+ FIRST_18,
+ FIRST_20,
+ MID_14 as u128,
+ LAST_4 as u128,
+ LAST_6 as u128,
+ LAST_8 as u128,
+ LAST_10 as u128,
+ LAST_12 as u128,
+ LAST_14,
+ LAST_16,
+ LAST_18,
+ LAST_20,
+ ]) {
+ coder.encode_long(i).unwrap();
+ }
+ });
+ }
+
+ // Indicative figure: 82ms, meaning ~80ns per encode.
+ #[cfg(bench)]
+ #[bench]
+ fn bench_encode_all_four_character_tesids(b: &mut test::Bencher) {
+ let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ b.iter(|| for i in 0..0b100000000000000000000 {
+ test::black_box(coder.encode(test::black_box(i)));
+ })
+ }
+
+ // Indicative figure: 210ms, meaning ~200ns per encode-and-decode, suggesting decode is slower
+ // than encode!
+ #[cfg(bench)]
+ #[bench]
+ fn bench_encode_and_decode_all_four_character_tesids(b: &mut test::Bencher) {
+ let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ b.iter(|| for i in 0..0b100000000000000000000 {
+ coder.decode(&coder.encode(test::black_box(i))).unwrap();
+ })
+ }
+
+ #[cfg(feature = "std")]
+ #[test]
+ #[cfg_attr(debug_assertions, ignore)] // Takes a few seconds in debug mode (<0.4s in release).
+ // Very unscientific approach: a single run of this takes under 0.4s, which is under 400ns per
+ // iteration, which includes an encode, a decode, and a HashSet insert.
+ fn show_uniqueness_of_four_character_tesids() {
+ let coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ let mut seen = std::collections::HashSet::new();
+ for i in 0..0b100000000000000000000 {
+ let string = coder.encode(i);
+ assert_eq!(coder.decode(&string).unwrap(), i);
+ assert!(string.len() == 4);
+ assert!(seen.insert(string));
+ }
+ }
+
+ #[test]
+ fn zero_on_drop() {
+ let mut coder = TesidCoder::new("000102030405060708090a0b0c0d0e0f").unwrap();
+ unsafe {
+ ptr::drop_in_place(&mut coder);
+ }
+ assert_eq!(coder.expanded_key, [0; 30]);
+ }
+}