human_friendly_ids/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(clippy::all, clippy::pedantic)]
3#![allow(clippy::uninlined_format_args)]
4
5pub mod alphabet;
6pub mod distribution;
7pub mod error;
8pub mod id;
9
10// Re-export main types for convenience
11pub use distribution::UploadIdDist;
12
13pub use crate::id::UploadId;
14
15#[allow(
16    clippy::all,
17    clippy::pedantic,
18    unused_must_use,
19    reason = "It's a test, bro."
20)]
21#[cfg(test)]
22mod tests {
23    use std::convert::TryFrom;
24
25    use rand::{Rng, distr::Distribution};
26
27    use super::*;
28    use crate::alphabet::GEN_ALPHABET;
29
30    #[test]
31    fn assert_largest_id_is_fixed() {
32        let largest = UploadId::max_length();
33        assert_eq!(largest, 838_488_366_986_797_801); // Absurdly large number, but it's fixed.
34
35        // Try and generate an id with a very large length, notably this will allocate a string
36        // of this size.
37        const TEST_SIZE: usize = 1024 * 1024; // 1mb
38
39        let mut rng = rand::rng();
40        let id = UploadIdDist::<TEST_SIZE>.sample(&mut rng);
41        assert_eq!(id.as_str().len(), TEST_SIZE);
42
43        // Decode and re-encode the id.
44        let id_str = id.to_string();
45        let id_decoded: UploadId = id_str.parse().expect("Failed to decode UploadId");
46
47        assert_eq!(id_decoded.to_string(), id_str);
48    }
49
50    #[test]
51    fn test_decode() {
52        let test_string = String::from("wcfytxww4opin4jmjjes4ccfd");
53        let decoded = UploadId::try_from(test_string).expect("Failed to decode UploadId");
54        assert_eq!(
55            decoded.as_str(),
56            "wcfytxww4opin4jmjjes4ccfd",
57            "decoded value should be equal to input string"
58        );
59    }
60
61    #[test]
62    fn fuzz_generated_ids() {
63        for _ in 0_u64..10_000_u64 {
64            let mut rng = rand::rng();
65            let id = UploadIdDist::<25>.sample(&mut rng);
66            println!("{}", id);
67            assert_eq!(id.as_str().len(), 25);
68
69            // Assert that serializing and deserializing the id doesn't change it.
70            let id_str = id.to_string();
71            let id = UploadId::try_from(id_str.clone()).expect("Failed to decode UploadId");
72            assert_eq!(id.to_string(), id_str);
73        }
74    }
75
76    #[test]
77    fn fuzz_gen_alphabet_strings() {
78        let mut rng = rand::rng();
79        for _ in 0..100_000_u64 {
80            // Generate a random string of characters from 2 to 25 characters long.
81            let string = (0..rng.random_range(2..25))
82                .map(|_| GEN_ALPHABET[rng.random_range(0..GEN_ALPHABET.len())])
83                .collect::<String>();
84
85            // Try and decode it - should not panic.
86            UploadId::try_from(string.clone());
87        }
88    }
89
90    #[test]
91    fn fuzz_random_strings() {
92        let mut rng = rand::rng();
93        for _ in 0..100_000_u64 {
94            // Generate a random string of characters from 2 to 25 characters long.
95            let string = (0..rng.random_range(2..25))
96                .map(|_| rng.random_range(0..=255) as u8 as char)
97                .collect::<String>();
98
99            // Try and decode it - should not panic.
100            UploadId::try_from(string.clone());
101        }
102    }
103
104    #[test]
105    fn test_invalid_chars_error() {
106        let id = "abc123".to_string();
107        let result = UploadId::try_from(id);
108        assert!(result.is_err());
109        let err = result.expect_err("Should fail due to invalid characters");
110        assert_eq!(err.to_string(), "Invalid check bit");
111    }
112
113    #[test]
114    fn test_invalid_check_bit_error() {
115        let invalid_id = String::from("abbsyhbbb4tyxnnmrtjx4crom");
116        let result = UploadId::try_from(invalid_id);
117        assert!(result.is_err());
118        let err = result.expect_err("Should fail due to invalid check-bit");
119        assert_eq!(err.to_string(), "Invalid check bit");
120    }
121
122    #[test]
123    fn test_too_short_error() {
124        let invalid_id = String::from("aa");
125        let result = UploadId::try_from(invalid_id);
126        assert!(result.is_err());
127        let err = result.expect_err("Should fail due to invalid check-bit");
128        assert_eq!(err.to_string(), "ID length too short, minimum 3 characters");
129    }
130
131    #[test]
132    fn test_weird_unicode() {
133        let invalid_id = String::from("🦀🦀🦀");
134        let result = UploadId::try_from(invalid_id);
135        assert!(result.is_err());
136        let err = result.expect_err("Should fail due to invalid characters");
137        assert_eq!(err.to_string(), "Invalid character in ID");
138    }
139
140    #[test]
141    fn test_invalid_chars() {
142        let invalid_id = String::from("¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿gg");
143        let result = UploadId::try_from(invalid_id);
144        assert!(result.is_err());
145        let err = result.expect_err("Should fail due to invalid characters");
146        assert_eq!(err.to_string(), "Invalid character in ID");
147    }
148}