1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::path::Path;
6
7pub mod prelude {
9 pub use crate::{
10 CommandName, CommandNameError, ExecutableName, executable_name_from_path,
11 is_valid_command_name,
12 };
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum CommandNameError {
18 Empty,
20 ContainsSeparator,
22 InvalidCharacter,
24 NonUnicode,
26}
27
28impl fmt::Display for CommandNameError {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Empty => formatter.write_str("command name cannot be empty"),
32 Self::ContainsSeparator => {
33 formatter.write_str("command name cannot contain path separators")
34 },
35 Self::InvalidCharacter => {
36 formatter.write_str("command name cannot contain control characters")
37 },
38 Self::NonUnicode => formatter.write_str("executable name is not valid Unicode"),
39 }
40 }
41}
42
43impl std::error::Error for CommandNameError {}
44
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub struct CommandName {
48 name: String,
49}
50
51impl CommandName {
52 pub fn new(name: impl Into<String>) -> Result<Self, CommandNameError> {
59 let name = name.into();
60 validate_command_name(&name)?;
61 Ok(Self { name })
62 }
63
64 #[must_use]
66 pub fn as_str(&self) -> &str {
67 &self.name
68 }
69
70 #[must_use]
72 pub fn into_string(self) -> String {
73 self.name
74 }
75}
76
77impl AsRef<str> for CommandName {
78 fn as_ref(&self) -> &str {
79 self.as_str()
80 }
81}
82
83impl fmt::Display for CommandName {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(&self.name)
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Hash)]
91pub struct ExecutableName {
92 command_name: CommandName,
93}
94
95impl ExecutableName {
96 pub fn new(name: impl Into<String>) -> Result<Self, CommandNameError> {
102 Ok(Self {
103 command_name: CommandName::new(name)?,
104 })
105 }
106
107 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, CommandNameError> {
114 executable_name_from_path(path)
115 }
116
117 #[must_use]
119 pub const fn command_name(&self) -> &CommandName {
120 &self.command_name
121 }
122
123 #[must_use]
125 pub fn display_name(&self) -> &str {
126 self.command_name.as_str()
127 }
128
129 #[must_use]
131 pub fn into_command_name(self) -> CommandName {
132 self.command_name
133 }
134}
135
136impl fmt::Display for ExecutableName {
137 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138 formatter.write_str(self.display_name())
139 }
140}
141
142#[must_use]
144pub fn is_valid_command_name(name: &str) -> bool {
145 validate_command_name(name).is_ok()
146}
147
148pub fn executable_name_from_path(
155 path: impl AsRef<Path>,
156) -> Result<ExecutableName, CommandNameError> {
157 let name = path
158 .as_ref()
159 .file_name()
160 .ok_or(CommandNameError::Empty)?
161 .to_str()
162 .ok_or(CommandNameError::NonUnicode)?;
163
164 ExecutableName::new(name)
165}
166
167fn validate_command_name(name: &str) -> Result<(), CommandNameError> {
168 if name.is_empty() {
169 return Err(CommandNameError::Empty);
170 }
171
172 if name.contains('/') || name.contains('\\') {
173 return Err(CommandNameError::ContainsSeparator);
174 }
175
176 if name.chars().any(char::is_control) {
177 return Err(CommandNameError::InvalidCharacter);
178 }
179
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::{
186 CommandName, CommandNameError, ExecutableName, executable_name_from_path,
187 is_valid_command_name,
188 };
189
190 #[test]
191 fn validates_command_names() {
192 assert!(is_valid_command_name("rustuse"));
193 assert!(is_valid_command_name("rustuse.exe"));
194 assert!(!is_valid_command_name(""));
195 assert!(!is_valid_command_name("bin/rustuse"));
196 assert_eq!(
197 CommandName::new("bin/rustuse"),
198 Err(CommandNameError::ContainsSeparator)
199 );
200 }
201
202 #[test]
203 fn stores_command_and_executable_names() -> Result<(), CommandNameError> {
204 let command = CommandName::new("rustuse")?;
205 let executable = ExecutableName::new("rustuse.exe")?;
206
207 assert_eq!(command.as_str(), "rustuse");
208 assert_eq!(executable.display_name(), "rustuse.exe");
209 Ok(())
210 }
211
212 #[test]
213 fn extracts_executable_name_from_path() -> Result<(), CommandNameError> {
214 let executable = executable_name_from_path("target/debug/rustuse")?;
215
216 assert_eq!(executable.display_name(), "rustuse");
217 Ok(())
218 }
219}