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