Skip to main content

iota_names/
name.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{fmt, str::FromStr};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    constants::{
10        IOTA_NAMES_MAX_LABEL_LENGTH, IOTA_NAMES_MAX_NAME_LENGTH, IOTA_NAMES_MIN_LABEL_LENGTH,
11        IOTA_NAMES_SEPARATOR_AT, IOTA_NAMES_SEPARATOR_DOT, IOTA_NAMES_TLN,
12    },
13    error::IotaNamesError,
14};
15
16#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
17pub struct Name {
18    // Labels of the name, in reverse order
19    labels: Vec<String>,
20}
21
22impl FromStr for Name {
23    type Err = IotaNamesError;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        if s.len() > IOTA_NAMES_MAX_NAME_LENGTH {
27            return Err(IotaNamesError::NameLengthExceeded(
28                s.len(),
29                IOTA_NAMES_MAX_NAME_LENGTH,
30            ));
31        }
32
33        let formatted_string = convert_from_at_format(s, &IOTA_NAMES_SEPARATOR_DOT)?;
34
35        let labels = formatted_string
36            .split(IOTA_NAMES_SEPARATOR_DOT)
37            .rev()
38            .map(validate_label)
39            .collect::<Result<Vec<_>, Self::Err>>()?;
40
41        // A valid name in our system has at least a TLN and an SLN (len == 2).
42        if labels.len() < 2 {
43            return Err(IotaNamesError::NotEnoughLabels);
44        }
45
46        if labels[0] != IOTA_NAMES_TLN {
47            return Err(IotaNamesError::InvalidTln(labels[0].to_string()));
48        }
49
50        let labels = labels.into_iter().map(ToOwned::to_owned).collect();
51
52        Ok(Name { labels })
53    }
54}
55
56impl fmt::Display for Name {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        // We use to_string() to check on-chain state and parse on-chain data
59        // so we should always default to DOT format.
60        let output = self.format(NameFormat::Dot);
61        f.write_str(&output)?;
62
63        Ok(())
64    }
65}
66
67impl Name {
68    /// Derive the parent name for a given name. Only subnames have
69    /// parents; second-level names return `None`.
70    ///
71    /// ```
72    /// # use std::str::FromStr;
73    /// # use iota_names::name::Name;
74    /// assert_eq!(
75    ///     Name::from_str("test.example.iota").unwrap().parent(),
76    ///     Some(Name::from_str("example.iota").unwrap())
77    /// );
78    /// assert_eq!(
79    ///     Name::from_str("sub.test.example.iota").unwrap().parent(),
80    ///     Some(Name::from_str("test.example.iota").unwrap())
81    /// );
82    /// assert_eq!(Name::from_str("example.iota").unwrap().parent(), None);
83    /// ```
84    pub fn parent(&self) -> Option<Self> {
85        if self.is_subname() {
86            Some(Self {
87                labels: self
88                    .labels
89                    .iter()
90                    .take(self.num_labels() - 1)
91                    .cloned()
92                    .collect(),
93            })
94        } else {
95            None
96        }
97    }
98
99    /// Returns whether this name is a second-level name (Ex. `test.iota`)
100    pub fn is_sln(&self) -> bool {
101        self.num_labels() == 2
102    }
103
104    /// Returns whether this name is a subname (Ex. `sub.test.iota`)
105    pub fn is_subname(&self) -> bool {
106        self.num_labels() >= 3
107    }
108
109    /// Returns the number of labels including TLN.
110    ///
111    /// ```
112    /// # use std::str::FromStr;
113    /// # use iota_names::name::Name;
114    /// assert_eq!(Name::from_str("test.example.iota").unwrap().num_labels(), 3)
115    /// ```
116    pub fn num_labels(&self) -> usize {
117        self.labels.len()
118    }
119
120    /// Get the label at the given index
121    pub fn label(&self, index: usize) -> Option<&String> {
122        self.labels.get(index)
123    }
124
125    /// Get all of the labels. NOTE: These are in reverse order starting with
126    /// the top-level name and proceeding to subnames.
127    pub fn labels(&self) -> &[String] {
128        &self.labels
129    }
130
131    /// Formats a name into a string based on the available output formats.
132    /// The default separator is `.`
133    pub fn format(&self, format: NameFormat) -> String {
134        let mut labels = self.labels.clone();
135        let sep = &IOTA_NAMES_SEPARATOR_DOT.to_string();
136        labels.reverse();
137
138        if format == NameFormat::Dot {
139            // DOT format, all labels joined together with dots, including the TLN.
140            labels.join(sep)
141        } else {
142            // SAFETY: This is a safe operation because we only allow a
143            // name's label vector size to be >= 2 (see `Name::from_str`)
144            let _tln = labels.pop();
145            let sln = labels.pop().unwrap();
146
147            // AT format, labels minus SLN joined together with dots, then joined to SLN
148            // with @, no TLN.
149            format!("{}{IOTA_NAMES_SEPARATOR_AT}{sln}", labels.join(sep))
150        }
151    }
152}
153
154/// Two different view options for a name.
155/// `At` -> `test@example` | `Dot` -> `test.example.iota`
156#[derive(Clone, Eq, PartialEq, Debug)]
157pub enum NameFormat {
158    At,
159    Dot,
160}
161
162/// Converts @label ending to label{separator}iota ending.
163///
164/// E.g. `@example` -> `example.iota` | `test@example` -> `test.example.iota`
165fn convert_from_at_format(s: &str, separator: &char) -> Result<String, IotaNamesError> {
166    let mut splits = s.split(IOTA_NAMES_SEPARATOR_AT);
167
168    let Some(before) = splits.next() else {
169        return Err(IotaNamesError::InvalidSeparator);
170    };
171
172    let Some(after) = splits.next() else {
173        return Ok(before.to_string());
174    };
175
176    if splits.next().is_some() || after.contains(*separator) || after.is_empty() {
177        return Err(IotaNamesError::InvalidSeparator);
178    }
179
180    let mut parts = vec![];
181
182    if !before.is_empty() {
183        parts.push(before);
184    }
185
186    parts.push(after);
187    parts.push(IOTA_NAMES_TLN);
188
189    Ok(parts.join(&separator.to_string()))
190}
191
192/// Checks the validity of a label according to these rules:
193/// - length must be in
194///   [IOTA_NAMES_MIN_LABEL_LENGTH..IOTA_NAMES_MAX_LABEL_LENGTH]
195/// - must contain only '0'..'9', 'a'..'z' and '-'
196/// - must not start or end with '-'
197pub fn validate_label(label: &str) -> Result<&str, IotaNamesError> {
198    let bytes = label.as_bytes();
199    let len = bytes.len();
200
201    if !(IOTA_NAMES_MIN_LABEL_LENGTH..=IOTA_NAMES_MAX_LABEL_LENGTH).contains(&len) {
202        return Err(IotaNamesError::InvalidLabelLength(
203            len,
204            IOTA_NAMES_MIN_LABEL_LENGTH,
205            IOTA_NAMES_MAX_LABEL_LENGTH,
206        ));
207    }
208
209    for (i, character) in bytes.iter().enumerate() {
210        match character {
211            b'a'..=b'z' | b'0'..=b'9' => continue,
212            b'-' => {
213                if i == 0 || i == len - 1 {
214                    return Err(IotaNamesError::HyphensAsFirstOrLastLabelChar);
215                }
216            }
217            _ => return Err(IotaNamesError::InvalidLabelChar((*character) as char, i)),
218        };
219    }
220
221    Ok(label)
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn parent_extraction() {
230        let name = Name::from_str("leaf.node.test.iota")
231            .unwrap()
232            .parent()
233            .unwrap();
234
235        assert_eq!(name.to_string(), "node.test.iota");
236
237        let name = name.parent().unwrap();
238
239        assert_eq!(name.to_string(), "test.iota");
240
241        assert!(name.parent().is_none());
242    }
243
244    #[test]
245    fn name_service_outputs() {
246        assert_eq!("@test".parse::<Name>().unwrap().to_string(), "test.iota");
247        assert_eq!(
248            "test.iota".parse::<Name>().unwrap().to_string(),
249            "test.iota"
250        );
251        assert_eq!(
252            "test@sln".parse::<Name>().unwrap().to_string(),
253            "test.sln.iota"
254        );
255        assert_eq!(
256            "test.test@example".parse::<Name>().unwrap().to_string(),
257            "test.test.example.iota"
258        );
259        assert_eq!(
260            "test.test-with-hyphen@example-hyphen"
261                .parse::<Name>()
262                .unwrap()
263                .to_string(),
264            "test.test-with-hyphen.example-hyphen.iota"
265        );
266        assert_eq!(
267            "iota@iota".parse::<Name>().unwrap().to_string(),
268            "iota.iota.iota"
269        );
270        assert_eq!("@iota".parse::<Name>().unwrap().to_string(), "iota.iota");
271        assert_eq!(
272            "test.test.iota".parse::<Name>().unwrap().to_string(),
273            "test.test.iota"
274        );
275        assert_eq!(
276            "test.test.test.iota".parse::<Name>().unwrap().to_string(),
277            "test.test.test.iota"
278        );
279        assert_eq!(
280            "test.test-with-hyphen.test-with-hyphen.iota"
281                .parse::<Name>()
282                .unwrap()
283                .to_string(),
284            "test.test-with-hyphen.test-with-hyphen.iota"
285        );
286    }
287
288    #[test]
289    fn invalid_inputs() {
290        assert!(".".parse::<Name>().is_err());
291        assert!("@".parse::<Name>().is_err());
292        assert!("@inner.iota".parse::<Name>().is_err());
293        assert!("test@".parse::<Name>().is_err());
294        assert!("iota".parse::<Name>().is_err());
295        assert!("test.test@example.iota".parse::<Name>().is_err());
296        assert!("test@test@example".parse::<Name>().is_err());
297        assert!("test.atoi".parse::<Name>().is_err());
298        assert!("test.test@example-".parse::<Name>().is_err());
299        assert!("test.test@-example".parse::<Name>().is_err());
300        assert!("test.test-@example".parse::<Name>().is_err());
301        assert!("test.-test@example".parse::<Name>().is_err());
302        assert!("test.test-.iota".parse::<Name>().is_err());
303        assert!("test.-test.iota".parse::<Name>().is_err());
304    }
305
306    #[test]
307    fn outputs() {
308        let mut name = "test.iota".parse::<Name>().unwrap();
309        assert!(name.format(NameFormat::Dot) == "test.iota");
310        assert!(name.format(NameFormat::At) == "@test");
311
312        name = "test.test.iota".parse::<Name>().unwrap();
313        assert!(name.format(NameFormat::Dot) == "test.test.iota");
314        assert!(name.format(NameFormat::At) == "test@test");
315
316        name = "test.test.test.iota".parse::<Name>().unwrap();
317        assert!(name.format(NameFormat::Dot) == "test.test.test.iota");
318        assert!(name.format(NameFormat::At) == "test.test@test");
319
320        name = "test.test.test.test.iota".parse::<Name>().unwrap();
321        assert!(name.format(NameFormat::Dot) == "test.test.test.test.iota");
322        assert!(name.format(NameFormat::At) == "test.test.test@test");
323    }
324}