Skip to main content

name/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Data structures and functions relevant to `fuchsia.io` name processing.
6//!
7//! These names may be used to designate the location of a node as it
8//! appears in a directory.
9//!
10//! These should be aligned with the library comments in sdk/fidl/fuchsia.io/io.fidl.
11
12use fidl_fuchsia_io as fio;
13use static_assertions::const_assert_eq;
14use std::borrow::Borrow;
15use std::fmt::Display;
16use std::ops::Deref;
17use thiserror::Error;
18use zx_status::Status;
19
20/// The type for the name of a node, i.e. a single path component, e.g. `foo`.
21///
22/// ## Invariants
23///
24/// A valid node name must meet the following criteria:
25///
26/// * It cannot be longer than [MAX_NAME_LENGTH].
27/// * It cannot be empty.
28/// * It cannot be ".." (dot-dot).
29/// * It cannot be "." (single dot).
30/// * It cannot contain "/".
31/// * It cannot contain embedded NUL.
32#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
33pub struct Name(Box<str>);
34
35/// The maximum length, in bytes, of a single filesystem component.
36pub const MAX_NAME_LENGTH: usize = fio::MAX_NAME_LENGTH as usize;
37const_assert_eq!(MAX_NAME_LENGTH as u64, fio::MAX_NAME_LENGTH);
38
39#[derive(Error, Debug, Clone, PartialEq, Eq)]
40pub enum ParseNameError {
41    #[error("name is too long")]
42    TooLong,
43
44    #[error("name cannot be empty")]
45    Empty,
46
47    #[error("name cannot be `.`")]
48    Dot,
49
50    #[error("name cannot be `..`")]
51    DotDot,
52
53    #[error("name cannot contain `/`")]
54    Slash,
55
56    #[error("name cannot contain embedded NUL")]
57    EmbeddedNul,
58}
59
60impl From<ParseNameError> for Status {
61    fn from(value: ParseNameError) -> Self {
62        match value {
63            ParseNameError::TooLong => Status::BAD_PATH,
64            _ => Status::INVALID_ARGS,
65        }
66    }
67}
68
69impl Display for Name {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.write_str(&self.0)
72    }
73}
74
75/// Validates whether a string slice is a valid node name.
76///
77/// A valid node name must meet the following criteria:
78/// * It cannot be longer than [MAX_NAME_LENGTH] (255 bytes).
79/// * It cannot be empty.
80/// * It cannot be "." (single dot) or ".." (dot-dot).
81/// * It cannot contain "/" (slash) or embedded NUL (`\0`) characters.
82pub fn validate_name(name: &str) -> Result<(), ParseNameError> {
83    let len = name.len();
84    if len > MAX_NAME_LENGTH {
85        return Err(ParseNameError::TooLong);
86    }
87    if len == 0 {
88        return Err(ParseNameError::Empty);
89    }
90    let bytes = name.as_bytes();
91    if bytes[0] == b'.' {
92        if len == 1 {
93            return Err(ParseNameError::Dot);
94        }
95        if len == 2 && bytes[1] == b'.' {
96            return Err(ParseNameError::DotDot);
97        }
98    }
99    if let Some(idx) = memchr::memchr2(b'/', 0, name.as_bytes()) {
100        if name.as_bytes()[idx] == 0 {
101            return Err(ParseNameError::EmbeddedNul);
102        } else {
103            return Err(ParseNameError::Slash);
104        }
105    }
106    Ok(())
107}
108
109impl From<Name> for String {
110    fn from(value: Name) -> Self {
111        value.0.into_string()
112    }
113}
114
115impl TryFrom<String> for Name {
116    type Error = ParseNameError;
117
118    fn try_from(value: String) -> Result<Name, ParseNameError> {
119        validate_name(&value)?;
120        Ok(Name(value.into_boxed_str()))
121    }
122}
123
124impl Deref for Name {
125    type Target = str;
126
127    fn deref(&self) -> &Self::Target {
128        &self.0
129    }
130}
131
132impl Borrow<str> for Name {
133    fn borrow(&self) -> &str {
134        &*self
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use assert_matches::assert_matches;
142
143    #[test]
144    fn test_validate_name() {
145        assert_matches!(validate_name(&"a".repeat(1000)), Err(ParseNameError::TooLong));
146        assert_matches!(
147            validate_name(
148                std::str::from_utf8(&vec![65; fio::MAX_NAME_LENGTH as usize + 1]).unwrap()
149            ),
150            Err(ParseNameError::TooLong)
151        );
152        assert_matches!(
153            validate_name(std::str::from_utf8(&vec![65; fio::MAX_NAME_LENGTH as usize]).unwrap()),
154            Ok(())
155        );
156        assert_matches!(validate_name(""), Err(ParseNameError::Empty));
157        assert_matches!(validate_name("."), Err(ParseNameError::Dot));
158        assert_matches!(validate_name(".."), Err(ParseNameError::DotDot));
159        assert_matches!(validate_name(".a"), Ok(()));
160        assert_matches!(validate_name("..a"), Ok(()));
161        assert_matches!(validate_name("a/b"), Err(ParseNameError::Slash));
162        assert_matches!(validate_name("a\0b"), Err(ParseNameError::EmbeddedNul));
163        assert_matches!(validate_name("abc"), Ok(()));
164    }
165
166    #[test]
167    fn test_try_from() {
168        assert_matches!(Name::try_from("a".repeat(1000)), Err(ParseNameError::TooLong));
169        assert_matches!(Name::try_from("abc".to_string()), Ok(Name(name)) if &*name == "abc");
170    }
171
172    #[test]
173    fn test_into() {
174        let name = Name::try_from("a".to_string()).unwrap();
175        let name: String = name.into();
176        assert_eq!(name, "a".to_string());
177    }
178
179    #[test]
180    fn test_deref() {
181        let name = Name::try_from("a".to_string()).unwrap();
182        let name: &str = &name;
183        assert_eq!(name, "a");
184    }
185}