forc_util/
restricted.rs

1//! Helpers for validating and checking names like package and organization names.
2// This is based on https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/util/restricted_names.rs
3
4use anyhow::{bail, Result};
5use regex::Regex;
6use std::path::Path;
7
8/// Returns `true` if the name contains non-ASCII characters.
9pub fn is_non_ascii_name(name: &str) -> bool {
10    name.chars().any(|ch| ch > '\x7f')
11}
12
13/// Rust keywords, further bikeshedding necessary to determine a complete set of Sway keywords
14pub fn is_keyword(name: &str) -> bool {
15    // See https://doc.rust-lang.org/reference/keywords.html
16    [
17        "Self", "abstract", "as", "await", "become", "box", "break", "const", "continue", "dep",
18        "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in",
19        "let", "loop", "macro", "match", "move", "mut", "override", "priv", "pub", "ref", "return",
20        "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", "unsafe",
21        "unsized", "use", "virtual", "where", "while", "yield",
22    ]
23    .contains(&name)
24}
25
26/// These names cannot be used on Windows, even with an extension.
27pub fn is_windows_reserved(name: &str) -> bool {
28    [
29        "con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8",
30        "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
31    ]
32    .contains(&name.to_ascii_lowercase().as_str())
33}
34
35/// These names conflict with library, macro or heap allocation suffixes, or keywords.
36pub fn is_conflicting_suffix(name: &str) -> bool {
37    ["alloc", "proc_macro", "proc-macro"].contains(&name)
38}
39
40// Bikeshedding necessary to determine if relevant
41/// An artifact with this name will conflict with one of forc's build directories.
42pub fn is_conflicting_artifact_name(name: &str) -> bool {
43    ["deps", "examples", "build", "incremental"].contains(&name)
44}
45
46/// Check the package name for invalid characters.
47pub fn contains_invalid_char(name: &str, use_case: &str) -> Result<()> {
48    let mut chars = name.chars();
49    if let Some(ch) = chars.next() {
50        if ch.is_ascii_digit() {
51            // A specific error for a potentially common case.
52            bail!(
53                "the name `{name}` cannot be used as a {use_case}, \
54                the name cannot start with a digit"
55            );
56        }
57        if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {
58            bail!(
59                "invalid character `{ch}` in {use_case}: `{name}`, \
60                the first character must be a Unicode XID start character \
61                (most letters or `_`)"
62            );
63        }
64    }
65    for ch in chars {
66        if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {
67            bail!(
68                "invalid character `{ch}` in {use_case}: `{name}`, \
69                characters must be Unicode XID characters \
70                (numbers, `-`, `_`, or most letters)"
71            );
72        }
73    }
74    if name.is_empty() {
75        bail!(
76            "{use_case} cannot be left empty, \
77            please use a valid name"
78        );
79    }
80    Ok(())
81}
82
83/// Check the entire path for names reserved in Windows.
84pub fn is_windows_reserved_path(path: &Path) -> bool {
85    path.iter()
86        .filter_map(|component| component.to_str())
87        .any(|component| {
88            let stem = component.split('.').next().unwrap();
89            is_windows_reserved(stem)
90        })
91}
92
93/// Returns `true` if the name contains any glob pattern wildcards.
94pub fn is_glob_pattern<T: AsRef<str>>(name: T) -> bool {
95    name.as_ref().contains(&['*', '?', '[', ']'][..])
96}
97
98/// Check the project name format.
99pub fn is_valid_project_name_format(name: &str) -> Result<()> {
100    let re = Regex::new(r"^([a-zA-Z]([a-zA-Z0-9-_]+)|)$").unwrap();
101    if !re.is_match(name) {
102        bail!(
103            "'{name}' is not a valid name for a project. \n\
104            The name may use letters, numbers, hyphens, and underscores, and must start with a letter."
105        );
106    }
107    Ok(())
108}
109
110#[test]
111fn test_invalid_char() {
112    assert_eq!(
113        contains_invalid_char("test#proj", "package name").map_err(|e| e.to_string()),
114        std::result::Result::Err(
115            "invalid character `#` in package name: `test#proj`, \
116        characters must be Unicode XID characters \
117        (numbers, `-`, `_`, or most letters)"
118                .into()
119        )
120    );
121
122    assert_eq!(
123        contains_invalid_char("test proj", "package name").map_err(|e| e.to_string()),
124        std::result::Result::Err(
125            "invalid character ` ` in package name: `test proj`, \
126        characters must be Unicode XID characters \
127        (numbers, `-`, `_`, or most letters)"
128                .into()
129        )
130    );
131
132    assert_eq!(
133        contains_invalid_char("", "package name").map_err(|e| e.to_string()),
134        std::result::Result::Err(
135            "package name cannot be left empty, \
136        please use a valid name"
137                .into()
138        )
139    );
140
141    assert!(matches!(
142        contains_invalid_char("test_proj", "package name"),
143        std::result::Result::Ok(())
144    ));
145}
146
147#[test]
148fn test_is_valid_project_name_format() {
149    let assert_valid = |name: &str| {
150        is_valid_project_name_format(name).expect("this should pass");
151    };
152
153    let assert_invalid = |name: &str, expected_error: &str| {
154        assert_eq!(
155            is_valid_project_name_format(name).map_err(|e| e.to_string()),
156            Err(expected_error.into())
157        );
158    };
159
160    let format_error_message = |name: &str| -> String {
161        format!(
162            "'{}' is not a valid name for a project. \n\
163            The name may use letters, numbers, hyphens, and underscores, and must start with a letter.",
164            name
165        )
166    };
167
168    // Test valid project names
169    assert_valid("mock_project_name");
170    assert_valid("mock_project_name123");
171    assert_valid("mock_project_name-123-_");
172
173    // Test invalid project names
174    assert_invalid("1mock_project", &format_error_message("1mock_project"));
175    assert_invalid("mock_.project", &format_error_message("mock_.project"));
176    assert_invalid("mock_/project", &format_error_message("mock_/project"));
177}