1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use std::collections::HashMap;

use failure::Error;

use iota_constants;

use crate::Result;

lazy_static! {
    static ref CHAR_TO_ASCII_MAP: HashMap<char, usize> = {
        let mut res: HashMap<char, usize> = HashMap::new();
        let mut ascii = 32;
        for c in " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".chars() {
            res.insert(c, ascii);
            ascii += 1;
        }
        res
    };
    static ref ASCII_TO_CHAR_MAP: HashMap<usize, char> = {
        let mut res: HashMap<usize, char> = HashMap::new();
        for (key, val) in CHAR_TO_ASCII_MAP.iter() {
            res.insert(*val, *key);
        }
        res
    };
}

#[derive(Debug, Fail)]
enum TryteConverterError {
    #[fail(display = "String [{}] is not valid ascii", string)]
    StringNotAscii { string: String },
    #[fail(display = "String [{}] is not valid trytes", string)]
    StringNotTrytes { string: String },
}

/// Converts a UTF-8 string containing ascii into a tryte-encoded string
pub fn to_trytes(input: &str) -> Result<String> {
    let mut trytes = String::new();
    let mut tmp_ascii = Vec::new();
    for c in input.chars() {
        if let Some(ascii) = CHAR_TO_ASCII_MAP.get(&c) {
            tmp_ascii.push(ascii);
        } else {
            return Err(Error::from(TryteConverterError::StringNotAscii {
                string: input.to_string(),
            }));
        }
    }
    for byte in tmp_ascii {
        let mut ascii = *byte;
        if ascii > 255 {
            ascii = 32;
        }
        let first = ascii % 27;
        let second = (ascii - first) / 27;
        trytes.push(iota_constants::TRYTE_ALPHABET[first]);
        trytes.push(iota_constants::TRYTE_ALPHABET[second]);
    }
    Ok(trytes)
}

/// Converts a tryte-encoded string into a UTF-8 string containing ascii characters
pub fn to_string(mut input_trytes: &str) -> Result<String> {
    if input_trytes.len() % 2 != 0 {
        input_trytes = &input_trytes[..input_trytes.len() - 1];
    }
    let mut tmp = String::new();
    let chars: Vec<char> = input_trytes.chars().collect();
    for letters in chars.chunks(2) {
        let first = match iota_constants::TRYTE_ALPHABET
            .iter()
            .position(|&x| x == letters[0])
        {
            Some(x) => x,
            None => {
                return Err(Error::from(TryteConverterError::StringNotTrytes {
                    string: input_trytes.to_string(),
                }))
            }
        };
        let second = match iota_constants::TRYTE_ALPHABET
            .iter()
            .position(|&x| x == letters[1])
        {
            Some(x) => x,
            None => {
                return Err(Error::from(TryteConverterError::StringNotTrytes {
                    string: input_trytes.to_string(),
                }))
            }
        };
        let decimal = first + second * 27;
        if let Some(t) = ASCII_TO_CHAR_MAP.get(&decimal) {
            tmp.push(*t);
        }
    }
    Ok(tmp)
}

#[cfg(test)]
mod tests {
    use rand::distributions::Alphanumeric;
    use rand::{self, Rng};

    use super::*;

    #[test]
    fn should_convert_string_to_trytes() {
        assert_eq!(to_trytes("Z").unwrap(), "IC");
        assert_eq!(to_trytes("JOTA JOTA").unwrap(), "TBYBCCKBEATBYBCCKB");
        assert_eq!(to_trytes(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~").unwrap(), "EAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZA9BABBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZB9CACBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZC9DADBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRD");
    }

    #[test]
    fn should_convert_trytes_to_string() {
        assert_eq!(to_string("IC").unwrap(), "Z");
        assert_eq!(to_string("TBYBCCKBEATBYBCCKB").unwrap(), "JOTA JOTA");
        assert_eq!(to_string("TBYBCCKBEATBYBCCKB9").unwrap(), "JOTA JOTA");
        assert_eq!(to_string("EAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZA9BABBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZB9CACBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZC9DADBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRD").unwrap(), " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~");
    }

    #[test]
    fn should_convert_back_and_forth() {
        let s: String = rand::thread_rng()
            .sample_iter(&Alphanumeric)
            .take(1000)
            .collect();
        let trytes = to_trytes(&s).unwrap();
        let back = to_string(&trytes).unwrap();
        assert_eq!(s, back);
    }
}