Skip to main content

use_command_name/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::path::Path;
6
7/// Commonly used command name primitives.
8pub mod prelude {
9    pub use crate::{
10        CommandName, CommandNameError, ExecutableName, executable_name_from_path,
11        is_valid_command_name,
12    };
13}
14
15/// Validation errors for command and executable names.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum CommandNameError {
18    /// The name was empty.
19    Empty,
20    /// The name contained a path separator.
21    ContainsSeparator,
22    /// The name contained a control character.
23    InvalidCharacter,
24    /// A path component could not be represented as Unicode.
25    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/// A validated command name.
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub struct CommandName {
48    name: String,
49}
50
51impl CommandName {
52    /// Creates a validated command name.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`CommandNameError`] when `name` is empty, contains path separators, or contains
57    /// control characters.
58    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    /// Returns the command name.
65    #[must_use]
66    pub fn as_str(&self) -> &str {
67        &self.name
68    }
69
70    /// Returns the owned command name.
71    #[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/// A validated executable or binary name.
90#[derive(Clone, Debug, PartialEq, Eq, Hash)]
91pub struct ExecutableName {
92    command_name: CommandName,
93}
94
95impl ExecutableName {
96    /// Creates a validated executable name.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`CommandNameError`] when `name` is not a valid command name.
101    pub fn new(name: impl Into<String>) -> Result<Self, CommandNameError> {
102        Ok(Self {
103            command_name: CommandName::new(name)?,
104        })
105    }
106
107    /// Extracts a validated executable name from a path.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`CommandNameError`] when the path has no file name, the file name is not Unicode,
112    /// or the file name is not a valid command name.
113    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, CommandNameError> {
114        executable_name_from_path(path)
115    }
116
117    /// Returns the executable name as a command name.
118    #[must_use]
119    pub const fn command_name(&self) -> &CommandName {
120        &self.command_name
121    }
122
123    /// Returns the display name.
124    #[must_use]
125    pub fn display_name(&self) -> &str {
126        self.command_name.as_str()
127    }
128
129    /// Returns the owned executable name.
130    #[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/// Returns whether `name` is valid for this crate's command name primitive.
143#[must_use]
144pub fn is_valid_command_name(name: &str) -> bool {
145    validate_command_name(name).is_ok()
146}
147
148/// Extracts a validated executable name from a path.
149///
150/// # Errors
151///
152/// Returns [`CommandNameError`] when the path has no file name, the file name is not Unicode,
153/// or the file name is not a valid command name.
154pub 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}