1extern crate base32;
2extern crate regex;
3extern crate serde;
4extern crate toml;
5
6use regex::Regex;
7use serde::Deserialize;
8use serde::Serialize;
9use std::fmt;
10
11#[derive(Debug, PartialEq)]
12pub enum ValidationError {
13 IllegalCharacter(&'static str), TooShortLength(&'static str), TooLongLength(&'static str), Deplication(&'static str), Requires(&'static str), }
19
20type ValidationResult = Result<(), ValidationError>;
21
22impl fmt::Display for ValidationError {
23 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24 match self {
25 Self::IllegalCharacter(msg)
26 | Self::TooShortLength(msg)
27 | Self::TooLongLength(msg)
28 | Self::Deplication(msg)
29 | Self::Requires(msg) => write!(f, "{}", msg),
30 }
31 }
32}
33
34#[derive(Serialize, Deserialize, Debug, Default)]
36pub struct Config {
37 profiles: Vec<Profile>,
38}
39
40impl Config {
41 pub fn new_profile(&mut self, name: &str, secret: &str) -> ValidationResult {
42 self.push_profile(Profile::new(name, secret))
43 }
44
45 fn push_profile(&mut self, profile: Profile) -> ValidationResult {
46 match self.validate_profile(&profile) {
47 Ok(_) => {
48 self.profiles.push(profile);
50 Ok(())
51 }
52 Err(err) => Err(err),
53 }
54 }
55
56 fn validate_profile(&self, profile: &Profile) -> ValidationResult {
57 if self.find_by_name(&profile.name).is_some() {
58 return Err(ValidationError::Deplication("This name already exists."));
59 }
60
61 profile.is_vaild()
62 }
63
64 pub fn get_secret_by_name(&self, name: &str) -> Option<Vec<u8>> {
66 if let Some(profile) = self.find_by_name(name) {
67 return profile.get_secret();
68 }
69
70 None
71 }
72
73 pub fn get_profiles(&self) -> &Vec<Profile> {
75 &self.profiles
76 }
77
78 pub fn remove_profile(&mut self, name: &str) -> Result<(), String> {
80 let mut index: Option<usize> = None;
81 self.profiles.iter().enumerate().for_each(|(i, profile)| {
82 if profile.name == name {
83 index = Some(i);
84 }
85 });
86
87 match index {
88 Some(i) => {
89 self.profiles.remove(i);
90 Ok(())
91 }
92 _ => Err(format!("Can't find this profile: {}", name)),
93 }
94 }
95
96 fn find_by_name(&self, name: &str) -> Option<&Profile> {
97 self.profiles
98 .iter()
99 .find(|&profile| *profile.get_name() == *name)
100 }
101
102 pub fn serialize(&self) -> Result<String, String> {
104 match toml::to_string(&self) {
105 Ok(data) => Ok(data),
106 Err(err) => Err(err.to_string()),
107 }
108 }
109
110 pub fn deserialize(&mut self, content: &str) -> Result<(), String> {
112 match toml::from_str(content) {
113 Ok(config) => {
114 *self = config;
115 Ok(())
116 }
117 Err(err) => Err(err.to_string()),
118 }
119 }
120}
121
122#[derive(Serialize, Deserialize, Default, Debug)]
124pub struct Profile {
125 name: String,
126 secret: String,
127}
128
129impl Profile {
130 pub fn new(name: &str, secret: &str) -> Self {
131 Profile {
132 name: name.to_string(),
133 secret: secret.to_string(),
134 }
135 }
136
137 pub fn get_name(&self) -> &String {
138 &self.name
139 }
140
141 pub fn get_secret(&self) -> Option<Vec<u8>> {
143 base32::decode(base32::Alphabet::RFC4648 { padding: true }, &self.secret)
144 }
145
146 pub fn is_vaild(&self) -> ValidationResult {
149 self.is_valid_name()?;
150
151 self.is_valid_secret()?;
152
153 Ok(())
154 }
155
156 fn is_valid_name(&self) -> ValidationResult {
162 if self.name.len() < 3 {
163 return Err(ValidationError::TooShortLength(
164 "Name requires at least 3 characters.",
165 ));
166 }
167 if 20 < self.name.len() {
168 return Err(ValidationError::TooLongLength(
169 "Name requires 20 characters or less.",
170 ));
171 }
172
173 const VALID_NAME_PATTERN: &str = r"^[[[:alnum:]]_@-]+\z";
175 let re = Regex::new(VALID_NAME_PATTERN).unwrap();
176 if !re.is_match(&self.name) {
177 return Err(ValidationError::IllegalCharacter(
178 "Name can contain only alphabet, number and symbol (@-_) .",
179 ));
180 }
181
182 Ok(())
183 }
184
185 fn is_valid_secret(&self) -> ValidationResult {
190 if self.secret.is_empty() {
191 return Err(ValidationError::Requires("Secret must be present."));
192 }
193
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn serialize_profile() {
204 let profile = Profile::new("test", "secret");
205 let expected = "name = \"test\"\nsecret = \"secret\"\n";
206
207 assert_eq!(toml::to_string(&profile).unwrap(), expected);
208 }
209
210 #[test]
211 fn serialize_config() {
212 let config = Config {
213 profiles: vec![Profile::new("test", "secret")],
214 };
215 let expected = r#"[[profiles]]
216name = "test"
217secret = "secret"
218"#;
219
220 assert_eq!(config.serialize().unwrap(), expected);
221 }
222
223 #[test]
224 fn deserialize_config() {
225 let string_config = "[[profiles]]\nname = \"test\"\nsecret = \"secret\"\n ";
226 let mut config: Config = Default::default();
227
228 config.deserialize(string_config).unwrap();
229
230 assert_eq!(config.profiles.len(), 1);
231 assert_eq!(config.profiles[0].name, "test");
232 assert_eq!(config.profiles[0].secret, "secret");
233 }
234
235 #[test]
236 fn push_profile_validation_when_name_duplicates() {
237 let mut config: Config = Default::default();
238 config.new_profile("test", "a").unwrap();
239 let second_time = config.new_profile("test", "");
240
241 assert!(second_time.is_err());
242 }
243
244 #[test]
245 fn push_profile_validation_when_name_contains_multi_byte_char() {
246 let mut config: Config = Default::default();
247 let result = config.new_profile("あ", "");
248
249 assert_eq!(
250 result,
251 Err(ValidationError::IllegalCharacter(
252 "Name can contain only alphabet, number and symbol (@-_) ."
253 ))
254 );
255 }
256
257 #[test]
258 fn push_profile_validation_when_name_contains_symbols_other_than_hyphen_and_underscore_and_at_sign(
259 ) {
260 let mut config: Config = Default::default();
261 let result = config.new_profile("!# $%&", "");
262
263 assert_eq!(
264 result,
265 Err(ValidationError::IllegalCharacter(
266 "Name can contain only alphabet, number and symbol (@-_) ."
267 ))
268 );
269 }
270
271 #[test]
272 fn push_profile_validation_when_name_contains_approved_symbols() {
273 let mut config: Config = Default::default();
274 let result = config.new_profile("-_@", "secret");
275
276 assert_eq!(result, Ok(()));
277 }
278
279 #[test]
280 fn push_profile_validation_when_name_is_too_short() {
281 let mut config: Config = Default::default();
282 let result = config.new_profile("ab", "");
283
284 assert_eq!(
285 result,
286 Err(ValidationError::TooShortLength(
287 "Name requires at least 3 characters."
288 ))
289 );
290 }
291 #[test]
292 fn push_profile_validation_when_name_is_too_long() {
293 let mut config: Config = Default::default();
294 let result = config.new_profile(&"a".repeat(21), "");
295
296 assert_eq!(
297 result,
298 Err(ValidationError::TooLongLength(
299 "Name requires 20 characters or less."
300 ))
301 );
302 }
303
304 #[test]
305 fn push_profile_validation_when_secret_is_blank() {
306 let mut config: Config = Default::default();
307 let result = config.new_profile("aaa", "");
308
309 assert_eq!(
310 result,
311 Err(ValidationError::Requires("Secret must be present."))
312 );
313 }
314}