1use std::{fmt, str::FromStr};
5
6use iota_types::base_types::IotaAddress;
7use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 constants::{
12 IOTA_NAMES_MAX_DOMAIN_LENGTH, IOTA_NAMES_MAX_LABEL_LENGTH, IOTA_NAMES_MIN_LABEL_LENGTH,
13 IOTA_NAMES_SEPARATOR_AT, IOTA_NAMES_SEPARATOR_DOT, IOTA_NAMES_TLD,
14 },
15 error::IotaNamesError,
16};
17
18#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
19pub struct Domain {
20 labels: Vec<String>,
22}
23
24impl FromStr for Domain {
25 type Err = IotaNamesError;
26
27 fn from_str(s: &str) -> Result<Self, Self::Err> {
28 if s.len() > IOTA_NAMES_MAX_DOMAIN_LENGTH {
29 return Err(IotaNamesError::DomainLengthExceeded(
30 s.len(),
31 IOTA_NAMES_MAX_DOMAIN_LENGTH,
32 ));
33 }
34
35 let formatted_string = convert_from_at_format(s, &IOTA_NAMES_SEPARATOR_DOT)?;
36
37 let labels = formatted_string
38 .split(IOTA_NAMES_SEPARATOR_DOT)
39 .rev()
40 .map(validate_label)
41 .collect::<Result<Vec<_>, Self::Err>>()?;
42
43 if labels.len() < 2 {
45 return Err(IotaNamesError::NotEnoughLabels);
46 }
47
48 if labels[0] != IOTA_NAMES_TLD {
49 return Err(IotaNamesError::InvalidTld(labels[0].to_string()));
50 }
51
52 let labels = labels.into_iter().map(ToOwned::to_owned).collect();
53
54 Ok(Domain { labels })
55 }
56}
57
58impl fmt::Display for Domain {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 let output = self.format(DomainFormat::Dot);
63 f.write_str(&output)?;
64
65 Ok(())
66 }
67}
68
69impl Domain {
70 pub fn type_(package_address: IotaAddress) -> StructTag {
71 const IOTA_NAMES_DOMAIN_MODULE: &IdentStr = ident_str!("domain");
72 const IOTA_NAMES_DOMAIN_STRUCT: &IdentStr = ident_str!("Domain");
73
74 StructTag {
75 address: package_address.into(),
76 module: IOTA_NAMES_DOMAIN_MODULE.to_owned(),
77 name: IOTA_NAMES_DOMAIN_STRUCT.to_owned(),
78 type_params: vec![],
79 }
80 }
81
82 pub fn parent(&self) -> Option<Self> {
99 if self.is_subdomain() {
100 Some(Self {
101 labels: self
102 .labels
103 .iter()
104 .take(self.num_labels() - 1)
105 .cloned()
106 .collect(),
107 })
108 } else {
109 None
110 }
111 }
112
113 pub fn is_sld(&self) -> bool {
115 self.num_labels() == 2
116 }
117
118 pub fn is_subdomain(&self) -> bool {
120 self.num_labels() >= 3
121 }
122
123 pub fn num_labels(&self) -> usize {
134 self.labels.len()
135 }
136
137 pub fn label(&self, index: usize) -> Option<&String> {
139 self.labels.get(index)
140 }
141
142 pub fn labels(&self) -> &[String] {
145 &self.labels
146 }
147
148 pub fn format(&self, format: DomainFormat) -> String {
151 let mut labels = self.labels.clone();
152 let sep = &IOTA_NAMES_SEPARATOR_DOT.to_string();
153 labels.reverse();
154
155 if format == DomainFormat::Dot {
156 labels.join(sep)
158 } else {
159 let _tld = labels.pop();
162 let sld = labels.pop().unwrap();
163
164 format!("{}{IOTA_NAMES_SEPARATOR_AT}{sld}", labels.join(sep))
167 }
168 }
169}
170
171#[derive(Clone, Eq, PartialEq, Debug)]
174pub enum DomainFormat {
175 At,
176 Dot,
177}
178
179fn convert_from_at_format(s: &str, separator: &char) -> Result<String, IotaNamesError> {
183 let mut splits = s.split(IOTA_NAMES_SEPARATOR_AT);
184
185 let Some(before) = splits.next() else {
186 return Err(IotaNamesError::InvalidSeparator);
187 };
188
189 let Some(after) = splits.next() else {
190 return Ok(before.to_string());
191 };
192
193 if splits.next().is_some() || after.contains(*separator) || after.is_empty() {
194 return Err(IotaNamesError::InvalidSeparator);
195 }
196
197 let mut parts = vec![];
198
199 if !before.is_empty() {
200 parts.push(before);
201 }
202
203 parts.push(after);
204 parts.push(IOTA_NAMES_TLD);
205
206 Ok(parts.join(&separator.to_string()))
207}
208
209pub fn validate_label(label: &str) -> Result<&str, IotaNamesError> {
215 let bytes = label.as_bytes();
216 let len = bytes.len();
217
218 if !(IOTA_NAMES_MIN_LABEL_LENGTH..=IOTA_NAMES_MAX_LABEL_LENGTH).contains(&len) {
219 return Err(IotaNamesError::InvalidLabelLength(
220 len,
221 IOTA_NAMES_MIN_LABEL_LENGTH,
222 IOTA_NAMES_MAX_LABEL_LENGTH,
223 ));
224 }
225
226 for (i, character) in bytes.iter().enumerate() {
227 match character {
228 b'a'..=b'z' | b'0'..=b'9' => continue,
229 b'-' => {
230 if i == 0 || i == len - 1 {
231 return Err(IotaNamesError::HyphensAsFirstOrLastLabelChar);
232 }
233 }
234 _ => return Err(IotaNamesError::InvalidLabelChar((*character) as char, i)),
235 };
236 }
237
238 Ok(label)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn parent_extraction() {
247 let name = Domain::from_str("leaf.node.test.iota")
248 .unwrap()
249 .parent()
250 .unwrap();
251
252 assert_eq!(name.to_string(), "node.test.iota");
253
254 let name = name.parent().unwrap();
255
256 assert_eq!(name.to_string(), "test.iota");
257
258 assert!(name.parent().is_none());
259 }
260
261 #[test]
262 fn name_service_outputs() {
263 assert_eq!("@test".parse::<Domain>().unwrap().to_string(), "test.iota");
264 assert_eq!(
265 "test.iota".parse::<Domain>().unwrap().to_string(),
266 "test.iota"
267 );
268 assert_eq!(
269 "test@sld".parse::<Domain>().unwrap().to_string(),
270 "test.sld.iota"
271 );
272 assert_eq!(
273 "test.test@example".parse::<Domain>().unwrap().to_string(),
274 "test.test.example.iota"
275 );
276 assert_eq!(
277 "test.test-with-hyphen@example-hyphen"
278 .parse::<Domain>()
279 .unwrap()
280 .to_string(),
281 "test.test-with-hyphen.example-hyphen.iota"
282 );
283 assert_eq!(
284 "iota@iota".parse::<Domain>().unwrap().to_string(),
285 "iota.iota.iota"
286 );
287 assert_eq!("@iota".parse::<Domain>().unwrap().to_string(), "iota.iota");
288 assert_eq!(
289 "test.test.iota".parse::<Domain>().unwrap().to_string(),
290 "test.test.iota"
291 );
292 assert_eq!(
293 "test.test.test.iota".parse::<Domain>().unwrap().to_string(),
294 "test.test.test.iota"
295 );
296 assert_eq!(
297 "test.test-with-hyphen.test-with-hyphen.iota"
298 .parse::<Domain>()
299 .unwrap()
300 .to_string(),
301 "test.test-with-hyphen.test-with-hyphen.iota"
302 );
303 }
304
305 #[test]
306 fn invalid_inputs() {
307 assert!(".".parse::<Domain>().is_err());
308 assert!("@".parse::<Domain>().is_err());
309 assert!("@inner.iota".parse::<Domain>().is_err());
310 assert!("test@".parse::<Domain>().is_err());
311 assert!("iota".parse::<Domain>().is_err());
312 assert!("test.test@example.iota".parse::<Domain>().is_err());
313 assert!("test@test@example".parse::<Domain>().is_err());
314 assert!("test.atoi".parse::<Domain>().is_err());
315 assert!("test.test@example-".parse::<Domain>().is_err());
316 assert!("test.test@-example".parse::<Domain>().is_err());
317 assert!("test.test-@example".parse::<Domain>().is_err());
318 assert!("test.-test@example".parse::<Domain>().is_err());
319 assert!("test.test-.iota".parse::<Domain>().is_err());
320 assert!("test.-test.iota".parse::<Domain>().is_err());
321 }
322
323 #[test]
324 fn outputs() {
325 let mut domain = "test.iota".parse::<Domain>().unwrap();
326 assert!(domain.format(DomainFormat::Dot) == "test.iota");
327 assert!(domain.format(DomainFormat::At) == "@test");
328
329 domain = "test.test.iota".parse::<Domain>().unwrap();
330 assert!(domain.format(DomainFormat::Dot) == "test.test.iota");
331 assert!(domain.format(DomainFormat::At) == "test@test");
332
333 domain = "test.test.test.iota".parse::<Domain>().unwrap();
334 assert!(domain.format(DomainFormat::Dot) == "test.test.test.iota");
335 assert!(domain.format(DomainFormat::At) == "test.test@test");
336
337 domain = "test.test.test.test.iota".parse::<Domain>().unwrap();
338 assert!(domain.format(DomainFormat::Dot) == "test.test.test.test.iota");
339 assert!(domain.format(DomainFormat::At) == "test.test.test@test");
340 }
341}