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'-' if i == 0 || i == len - 1 => {
230 return Err(IotaNamesError::HyphensAsFirstOrLastLabelChar);
231 }
232 _ => return Err(IotaNamesError::InvalidLabelChar),
233 };
234 }
235
236 Ok(label)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn parent_extraction() {
245 let name = Domain::from_str("leaf.node.test.iota")
246 .unwrap()
247 .parent()
248 .unwrap();
249
250 assert_eq!(name.to_string(), "node.test.iota");
251
252 let name = name.parent().unwrap();
253
254 assert_eq!(name.to_string(), "test.iota");
255
256 assert!(name.parent().is_none());
257 }
258
259 #[test]
260 fn name_service_outputs() {
261 assert_eq!("@test".parse::<Domain>().unwrap().to_string(), "test.iota");
262 assert_eq!(
263 "test.iota".parse::<Domain>().unwrap().to_string(),
264 "test.iota"
265 );
266 assert_eq!(
267 "test@sld".parse::<Domain>().unwrap().to_string(),
268 "test.sld.iota"
269 );
270 assert_eq!(
271 "test.test@example".parse::<Domain>().unwrap().to_string(),
272 "test.test.example.iota"
273 );
274 assert_eq!(
275 "iota@iota".parse::<Domain>().unwrap().to_string(),
276 "iota.iota.iota"
277 );
278 assert_eq!("@iota".parse::<Domain>().unwrap().to_string(), "iota.iota");
279 assert_eq!(
280 "test.test.iota".parse::<Domain>().unwrap().to_string(),
281 "test.test.iota"
282 );
283 assert_eq!(
284 "test.test.test.iota".parse::<Domain>().unwrap().to_string(),
285 "test.test.test.iota"
286 );
287 }
288
289 #[test]
290 fn invalid_inputs() {
291 assert!(".".parse::<Domain>().is_err());
292 assert!("@".parse::<Domain>().is_err());
293 assert!("@inner.iota".parse::<Domain>().is_err());
294 assert!("test@".parse::<Domain>().is_err());
295 assert!("iota".parse::<Domain>().is_err());
296 assert!("test.test@example.iota".parse::<Domain>().is_err());
297 assert!("test@test@example".parse::<Domain>().is_err());
298 assert!("test.atoi".parse::<Domain>().is_err());
299 }
300
301 #[test]
302 fn outputs() {
303 let mut domain = "test.iota".parse::<Domain>().unwrap();
304 assert!(domain.format(DomainFormat::Dot) == "test.iota");
305 assert!(domain.format(DomainFormat::At) == "@test");
306
307 domain = "test.test.iota".parse::<Domain>().unwrap();
308 assert!(domain.format(DomainFormat::Dot) == "test.test.iota");
309 assert!(domain.format(DomainFormat::At) == "test@test");
310
311 domain = "test.test.test.iota".parse::<Domain>().unwrap();
312 assert!(domain.format(DomainFormat::Dot) == "test.test.test.iota");
313 assert!(domain.format(DomainFormat::At) == "test.test@test");
314
315 domain = "test.test.test.test.iota".parse::<Domain>().unwrap();
316 assert!(domain.format(DomainFormat::Dot) == "test.test.test.test.iota");
317 assert!(domain.format(DomainFormat::At) == "test.test.test@test");
318 }
319}