1use anyhow::{bail, Result};
5use regex::Regex;
6use std::path::Path;
7
8pub fn is_non_ascii_name(name: &str) -> bool {
10 name.chars().any(|ch| ch > '\x7f')
11}
12
13pub fn is_keyword(name: &str) -> bool {
15 [
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
26pub 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
35pub fn is_conflicting_suffix(name: &str) -> bool {
37 ["alloc", "proc_macro", "proc-macro"].contains(&name)
38}
39
40pub fn is_conflicting_artifact_name(name: &str) -> bool {
43 ["deps", "examples", "build", "incremental"].contains(&name)
44}
45
46pub 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 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
83pub 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
93pub fn is_glob_pattern<T: AsRef<str>>(name: T) -> bool {
95 name.as_ref().contains(&['*', '?', '[', ']'][..])
96}
97
98pub 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 assert_valid("mock_project_name");
170 assert_valid("mock_project_name123");
171 assert_valid("mock_project_name-123-_");
172
173 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}