harsh/
harsh.rs

1use std::{error, fmt, result, str};
2
3use crate::{builder::HarshBuilder, shuffle};
4
5type Result<T, E = Error> = result::Result<T, E>;
6
7#[derive(Clone, Debug)]
8pub enum Error {
9    Hex,
10    Decode(DecodeError),
11}
12
13#[derive(Clone, Debug)]
14pub enum DecodeError {
15    Value,
16    Hash,
17}
18
19impl fmt::Display for DecodeError {
20    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21        match self {
22            DecodeError::Value => f.write_str("Found bad value"),
23            DecodeError::Hash => f.write_str("Malformed hashid"),
24        }
25    }
26}
27
28impl error::Error for DecodeError {}
29
30impl fmt::Display for Error {
31    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32        match self {
33            Error::Hex => f.write_str("Failed to decode hex value"),
34            Error::Decode(e) => match e {
35                DecodeError::Value => f.write_str("Found bad value"),
36                DecodeError::Hash => f.write_str("Malformed hashid"),
37            },
38        }
39    }
40}
41
42impl error::Error for Error {
43    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
44        match self {
45            Error::Hex => None,
46            Error::Decode(ref e) => Some(e),
47        }
48    }
49}
50
51/// A hashids-compatible hasher.
52///
53/// It's probably not a great idea to use the default, because in that case
54/// your values will be entirely trivial to decode. On the other hand, this is
55/// not intended to be cryptographically-secure, so go nuts!
56#[derive(Clone, Debug)]
57pub struct Harsh {
58    alphabet: Box<[u8]>,
59    guards: Box<[u8]>,
60    hash_length: usize,
61    salt: Box<[u8]>,
62    separators: Box<[u8]>,
63}
64
65impl Harsh {
66    /// Create a default instance of Harsh.
67    pub fn new() -> Self {
68        HarshBuilder::new()
69            .build()
70            .expect("Default options should not fail")
71    }
72
73    /// Build a new instance of Harsh.
74    pub fn builder() -> HarshBuilder {
75        HarshBuilder::new()
76    }
77
78    pub(crate) fn initialize(
79        alphabet: Box<[u8]>,
80        guards: Box<[u8]>,
81        hash_length: usize,
82        salt: Box<[u8]>,
83        separators: Box<[u8]>,
84    ) -> Self {
85        Harsh {
86            alphabet,
87            guards,
88            hash_length,
89            salt,
90            separators,
91        }
92    }
93
94    /// Encodes a slice of `u64` values into a single hashid.
95    pub fn encode(&self, values: &[u64]) -> String {
96        if values.is_empty() {
97            return String::new();
98        }
99
100        let nhash = create_nhash(values);
101
102        let mut alphabet = self.alphabet.clone();
103        let mut buffer = String::new();
104
105        let idx = (nhash % alphabet.len() as u64) as usize;
106        let lottery = alphabet[idx];
107        buffer.push(lottery as char);
108
109        for (idx, &value) in values.iter().enumerate() {
110            let mut value = value;
111            let mut temp = Vec::with_capacity(self.salt.len() + alphabet.len() + 1);
112            temp.push(lottery);
113            temp.extend_from_slice(&self.salt);
114            temp.extend_from_slice(&alphabet);
115
116            let alphabet_len = alphabet.len();
117            shuffle(&mut alphabet, &temp[..alphabet_len]);
118
119            let last = hash(value, &alphabet);
120            buffer.push_str(&last);
121
122            if idx + 1 < values.len() {
123                value %= (last.bytes().next().unwrap_or(0) as usize + idx) as u64;
124                buffer
125                    .push(self.separators[(value % self.separators.len() as u64) as usize] as char);
126            }
127        }
128
129        if buffer.len() < self.hash_length {
130            let guard_index = (nhash as usize
131                + buffer.bytes().next().expect("hellfire and damnation") as usize)
132                % self.guards.len();
133            let guard = self.guards[guard_index];
134            buffer.insert(0, guard as char);
135
136            if buffer.len() < self.hash_length {
137                let guard_index = (nhash as usize
138                    + buffer.as_bytes()[2] as usize)
139                    % self.guards.len();
140                let guard = self.guards[guard_index];
141                buffer.push(guard as char);
142            }
143        }
144
145        let half_length = alphabet.len() / 2;
146        while buffer.len() < self.hash_length {
147            {
148                let alphabet_copy = alphabet.clone();
149                shuffle(&mut alphabet, &alphabet_copy);
150            }
151
152            let (left, right) = alphabet.split_at(half_length);
153            buffer = format!(
154                "{}{}{}",
155                String::from_utf8_lossy(right),
156                buffer,
157                String::from_utf8_lossy(left)
158            );
159
160            let excess = buffer.len() as i32 - self.hash_length as i32;
161            if excess > 0 {
162                let marker = excess as usize / 2;
163                buffer = buffer[marker..marker + self.hash_length].to_owned();
164            }
165        }
166
167        buffer
168    }
169
170    /// Decodes a single hashid into a slice of `u64` values.
171    pub fn decode<T: AsRef<str>>(&self, input: T) -> Result<Vec<u64>> {
172        let mut value = input.as_ref().as_bytes();
173
174        if let Some(guard_idx) = value.iter().position(|u| self.guards.contains(u)) {
175            value = &value[(guard_idx + 1)..];
176        }
177
178        if let Some(guard_idx) = value.iter().rposition(|u| self.guards.contains(u)) {
179            value = &value[..guard_idx];
180        }
181
182        if value.len() < 2 {
183            return Err(Error::Decode(DecodeError::Hash));
184        }
185
186        let mut alphabet = self.alphabet.clone();
187
188        let lottery = value[0];
189        let value = &value[1..];
190        let segments = value.split(|u| self.separators.contains(u));
191
192        let result: Option<Vec<_>> = segments
193            .into_iter()
194            .map(|segment| {
195                let mut buffer = Vec::with_capacity(self.salt.len() + alphabet.len() + 1);
196                buffer.push(lottery);
197                buffer.extend_from_slice(&self.salt);
198                buffer.extend_from_slice(&alphabet);
199
200                let alphabet_len = alphabet.len();
201                shuffle(&mut alphabet, &buffer[..alphabet_len]);
202                unhash(segment, &alphabet)
203            })
204            .collect();
205
206        match result {
207            None => Err(Error::Decode(DecodeError::Value)),
208            Some(result) => {
209                if self.encode(&result) == input.as_ref() {
210                    Ok(result)
211                } else {
212                    Err(Error::Decode(DecodeError::Hash))
213                }
214            }
215        }
216    }
217
218    /// Encodes a hex string into a hashid.
219    pub fn encode_hex(&self, hex: &str) -> Result<String> {
220        let values: Option<Vec<_>> = hex
221            .as_bytes()
222            .chunks(12)
223            .map(|chunk| {
224                str::from_utf8(chunk)
225                    .ok()
226                    .and_then(|s| u64::from_str_radix(&("1".to_owned() + s), 16).ok())
227            })
228            .collect();
229
230        match values {
231            Some(values) => Ok(self.encode(&values)),
232            None => Err(Error::Hex),
233        }
234    }
235
236    /// Decodes a hashid into a hex string.
237    pub fn decode_hex(&self, value: &str) -> Result<String> {
238        use std::fmt::Write;
239
240        let values = self.decode(value)?;
241
242        let mut result = String::new();
243        let mut buffer = String::new();
244
245        for n in values {
246            write!(buffer, "{:x}", n).unwrap();
247            result.push_str(&buffer[1..]);
248            buffer.clear();
249        }
250
251        Ok(result)
252    }
253}
254
255impl Default for Harsh {
256    fn default() -> Self {
257        Harsh::new()
258    }
259}
260
261#[inline]
262fn create_nhash(values: &[u64]) -> u64 {
263    values
264        .iter()
265        .enumerate()
266        .fold(0, |a, (idx, value)| a + (value % (idx + 100) as u64))
267}
268
269fn hash(mut value: u64, alphabet: &[u8]) -> String {
270    let length = alphabet.len() as u64;
271    let mut hash = Vec::new();
272
273    loop {
274        hash.push(alphabet[(value % length) as usize]);
275        value /= length;
276
277        if value == 0 {
278            hash.reverse();
279            return String::from_utf8(hash).expect("omg fml");
280        }
281    }
282}
283
284fn unhash(input: &[u8], alphabet: &[u8]) -> Option<u64> {
285    input.iter().enumerate().fold(Some(0), |a, (idx, &value)| {
286        let pos = alphabet.iter().position(|&item| item == value)? as u64;
287        let b = (alphabet.len() as u64).checked_pow((input.len() - idx - 1) as u32)?;
288        let c = pos.checked_mul(b)?;
289        a.map(|a| a + c)
290    })
291}
292
293#[cfg(test)]
294mod tests {
295    use super::{Harsh, HarshBuilder};
296
297    #[test]
298    fn harsh_default_does_not_panic() {
299        Harsh::default();
300    }
301
302    #[test]
303    fn can_encode() {
304        let harsh = HarshBuilder::new()
305            .salt("this is my salt")
306            .build()
307            .expect("failed to initialize harsh");
308
309        assert_eq!(
310            "4o6Z7KqxE",
311            harsh.encode(&[1226198605112]),
312            "error encoding [1226198605112]"
313        );
314        assert_eq!("laHquq", harsh.encode(&[1, 2, 3]));
315    }
316
317    #[test]
318    fn can_encode_with_guards() {
319        let harsh = HarshBuilder::new()
320            .salt("this is my salt")
321            .length(8)
322            .build()
323            .expect("failed to initialize harsh");
324
325        assert_eq!("GlaHquq0", harsh.encode(&[1, 2, 3]));
326    }
327
328    #[test]
329    fn can_encode_with_padding() {
330        let harsh = HarshBuilder::new()
331            .salt("this is my salt")
332            .length(12)
333            .build()
334            .expect("failed to initialize harsh");
335
336        assert_eq!("9LGlaHquq06D", harsh.encode(&[1, 2, 3]));
337    }
338
339    #[test]
340    fn can_decode() {
341        let harsh = HarshBuilder::new()
342            .salt("this is my salt")
343            .build()
344            .expect("failed to initialize harsh");
345
346        assert_eq!(
347            &[1226198605112],
348            &harsh.decode("4o6Z7KqxE").expect("failed to decode")[..],
349            "error decoding \"4o6Z7KqxE\""
350        );
351        assert_eq!(
352            &[1u64, 2, 3],
353            &harsh.decode("laHquq").expect("failed to decode")[..]
354        );
355    }
356
357    #[test]
358    fn can_decode_with_guards() {
359        let harsh = HarshBuilder::new()
360            .salt("this is my salt")
361            .length(8)
362            .build()
363            .expect("failed to initialize harsh");
364
365        assert_eq!(
366            &[1u64, 2, 3],
367            &harsh.decode("GlaHquq0").expect("failed to decode")[..]
368        );
369    }
370
371    #[test]
372    fn can_decode_with_padding() {
373        let harsh = HarshBuilder::new()
374            .salt("this is my salt")
375            .length(12)
376            .build()
377            .expect("failed to initialize harsh");
378
379        assert_eq!(
380            &[1u64, 2, 3],
381            &harsh.decode("9LGlaHquq06D").expect("failed to decode")[..]
382        );
383    }
384
385    #[test]
386    fn can_encode_hex() {
387        let harsh = HarshBuilder::new()
388            .salt("this is my salt")
389            .build()
390            .expect("failed to initialize harsh");
391
392        assert_eq!(
393            "lzY",
394            &harsh.encode_hex("FA").expect("Failed to encode"),
395            "error encoding `FA`"
396        );
397        assert_eq!(
398            "MemE",
399            &harsh.encode_hex("26dd").expect("Failed to encode"),
400            "error encoding `26dd`"
401        );
402        assert_eq!(
403            "eBMrb",
404            &harsh.encode_hex("FF1A").expect("Failed to encode"),
405            "error encoding `FF1A`"
406        );
407        assert_eq!(
408            "D9NPE",
409            &harsh.encode_hex("12abC").expect("Failed to encode"),
410            "error encoding `12abC`"
411        );
412        assert_eq!(
413            "9OyNW",
414            &harsh.encode_hex("185b0").expect("Failed to encode"),
415            "error encoding `185b0`"
416        );
417        assert_eq!(
418            "MRWNE",
419            &harsh.encode_hex("17b8d").expect("Failed to encode"),
420            "error encoding `17b8d`"
421        );
422        assert_eq!(
423            "4o6Z7KqxE",
424            &harsh.encode_hex("1d7f21dd38").expect("Failed to encode"),
425            "error encoding `1d7f21dd38`"
426        );
427        assert_eq!(
428            "ooweQVNB",
429            &harsh.encode_hex("20015111d").expect("Failed to encode"),
430            "error encoding `20015111d`"
431        );
432        assert_eq!(
433            "kRNrpKlJ",
434            &harsh.encode_hex("deadbeef").expect("Failed to encode"),
435            "error encoding `deadbeef`"
436        );
437
438        let harsh = HarshBuilder::new().build().unwrap();
439        assert_eq!(
440            "y42LW46J9luq3Xq9XMly",
441            &harsh
442                .encode_hex("507f1f77bcf86cd799439011",)
443                .expect("failed to encode",),
444            "error encoding `507f1f77bcf86cd799439011`"
445        );
446    }
447
448    #[test]
449    fn can_encode_hex_with_guards() {
450        let harsh = HarshBuilder::new()
451            .salt("this is my salt")
452            .length(10)
453            .build()
454            .expect("failed to initialize harsh");
455
456        assert_eq!(
457            "GkRNrpKlJd",
458            &harsh.encode_hex("deadbeef").expect("Failed to encode"),
459        );
460    }
461
462    #[test]
463    fn can_encode_hex_with_padding() {
464        let harsh = HarshBuilder::new()
465            .salt("this is my salt")
466            .length(12)
467            .build()
468            .expect("failed to initialize harsh");
469
470        assert_eq!(
471            "RGkRNrpKlJde",
472            &harsh.encode_hex("deadbeef").expect("Failed to encode"),
473        );
474    }
475
476    #[test]
477    fn can_decode_hex() {
478        let harsh = HarshBuilder::new()
479            .salt("this is my salt")
480            .build()
481            .expect("failed to initialize harsh");
482
483        assert_eq!(
484            "fa",
485            harsh.decode_hex("lzY").expect("failed to decode"),
486            "error decoding `FA`"
487        );
488        assert_eq!(
489            "26dd",
490            harsh.decode_hex("MemE").expect("failed to decode"),
491            "error decoding `26dd`"
492        );
493        assert_eq!(
494            "ff1a",
495            harsh.decode_hex("eBMrb").expect("failed to decode"),
496            "error decoding `FF1A`"
497        );
498        assert_eq!(
499            "12abc",
500            harsh.decode_hex("D9NPE").expect("failed to decode"),
501            "error decoding `12abC`"
502        );
503        assert_eq!(
504            "185b0",
505            harsh.decode_hex("9OyNW").expect("failed to decode"),
506            "error decoding `185b0`"
507        );
508        assert_eq!(
509            "17b8d",
510            harsh.decode_hex("MRWNE").expect("failed to decode"),
511            "error decoding `17b8d`"
512        );
513        assert_eq!(
514            "1d7f21dd38",
515            harsh.decode_hex("4o6Z7KqxE").expect("failed to decode"),
516            "error decoding `1d7f21dd38`"
517        );
518        assert_eq!(
519            "20015111d",
520            harsh.decode_hex("ooweQVNB").expect("failed to decode"),
521            "error decoding `20015111d`"
522        );
523        assert_eq!(
524            "deadbeef",
525            harsh.decode_hex("kRNrpKlJ").expect("failed to decode"),
526            "error decoding `deadbeef`"
527        );
528
529        let harsh = HarshBuilder::new().build().unwrap();
530        assert_eq!(
531            "507f1f77bcf86cd799439011",
532            harsh
533                .decode_hex("y42LW46J9luq3Xq9XMly",)
534                .expect("failed to decode",),
535            "error decoding `y42LW46J9luq3Xq9XMly`"
536        );
537    }
538
539    #[test]
540    fn can_decode_hex_with_guards() {
541        let harsh = HarshBuilder::new()
542            .salt("this is my salt")
543            .length(10)
544            .build()
545            .expect("failed to initialize harsh");
546
547        assert_eq!(
548            "deadbeef",
549            harsh.decode_hex("GkRNrpKlJd").expect("failed to decode"),
550            "failed to decode GkRNrpKlJd"
551        );
552    }
553
554    #[test]
555    fn can_decode_hex_with_padding() {
556        let harsh = HarshBuilder::new()
557            .salt("this is my salt")
558            .length(12)
559            .build()
560            .expect("failed to initialize harsh");
561
562        assert_eq!(
563            "deadbeef",
564            harsh.decode_hex("RGkRNrpKlJde").expect("failed to decode"),
565            "failed to decode RGkRNrpKlJde"
566        );
567    }
568
569    #[test]
570    fn can_encode_with_custom_alphabet() {
571        let harsh = HarshBuilder::new()
572            .alphabet("abcdefghijklmnopqrstuvwxyz")
573            .build()
574            .expect("failed to initialize harsh");
575
576        assert_eq!(
577            "mdfphx",
578            harsh.encode(&[1, 2, 3]),
579            "failed to encode [1, 2, 3]"
580        );
581    }
582
583    #[test]
584    #[should_panic]
585    fn can_decode_with_invalid_alphabet() {
586        let harsh = Harsh::default();
587        harsh.decode("this$ain't|a\number").unwrap();
588    }
589
590    #[test]
591    fn can_decode_with_custom_alphabet() {
592        let harsh = HarshBuilder::new()
593            .alphabet("abcdefghijklmnopqrstuvwxyz")
594            .build()
595            .expect("failed to initialize harsh");
596
597        assert_eq!(
598            &[1, 2, 3],
599            &harsh.decode("mdfphx").expect("failed to decode")[..],
600            "failed to decode mdfphx"
601        );
602    }
603
604    #[test]
605    fn create_nhash() {
606        let values = &[1, 2, 3];
607        let nhash = super::create_nhash(values);
608        assert_eq!(6, nhash);
609    }
610
611    #[test]
612    fn hash() {
613        let result = super::hash(22, b"abcdefghijklmnopqrstuvwxyz");
614        assert_eq!("w", result);
615    }
616
617    #[test]
618    fn shuffle() {
619        let salt = b"1234";
620        let mut values = "asdfzxcvqwer".bytes().collect::<Vec<_>>();
621        super::shuffle(&mut values, salt);
622
623        assert_eq!("vdwqfrzcsxae", String::from_utf8_lossy(&values));
624    }
625
626    #[test]
627    fn guard_characters_should_be_added_to_left_first() {
628        let harsh = HarshBuilder::new().length(3).build().unwrap();
629        let hashed_value = harsh.encode(&[1]);
630
631        assert_eq!(&hashed_value, "ejR");
632        assert_eq!(vec![1], harsh.decode("ejR").unwrap());
633    }
634
635    #[test]
636    #[should_panic]
637    fn appended_garbage_data_invalidates_hashid() {
638        let harsh = HarshBuilder::new().length(4).build().unwrap();
639        let id = harsh.encode(&[1, 2]) + "12";
640        harsh.decode(id).unwrap();
641    }
642}