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    #[cfg(feature = "serde")]
123    #[test]
124    fn test_serde_roundtrip() {
125        let id = UploadId::try_from("wcfytxww4opin4jmjjes4ccfd".to_string())
126            .expect("Failed to decode UploadId");
127        let serialized = serde_json::to_string(&id).expect("Failed to serialize UploadId");
128        let deserialized: UploadId =
129            serde_json::from_str(&serialized).expect("Failed to deserialize UploadId");
130        assert_eq!(id, deserialized);
131    }
132
133    #[test]
134    fn test_too_short_error() {
135        let invalid_id = String::from("aa");
136        let result = UploadId::try_from(invalid_id);
137        assert!(result.is_err());
138        let err = result.expect_err("Should fail due to invalid check-bit");
139        assert_eq!(err.to_string(), "ID length too short, minimum 3 characters");
140    }
141
142    #[test]
143    fn test_weird_unicode() {
144        let invalid_id = String::from("🦀🦀🦀");
145        let result = UploadId::try_from(invalid_id);
146        assert!(result.is_err());
147        let err = result.expect_err("Should fail due to invalid characters");
148        assert_eq!(err.to_string(), "Invalid character in ID");
149    }
150
151    #[test]
152    fn test_invalid_chars() {
153        let invalid_id = String::from("¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿gg");
154        let result = UploadId::try_from(invalid_id);
155        assert!(result.is_err());
156        let err = result.expect_err("Should fail due to invalid characters");
157        assert_eq!(err.to_string(), "Invalid character in ID");
158    }
159}