Skip to main content

nix_compat/derivation/
output_name.rs

1use std::{fmt, str::FromStr};
2
3use smol_str::SmolStr;
4
5use crate::store_path;
6
7/// A derivation output name.
8///
9/// This is a derivation output name, so the 'out' or 'bin' bit that has
10/// been verified to not contain invalid characters.
11///
12/// Output names may also not be empty or be called `drv`.
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(
15    feature = "serde",
16    derive(serde_with::DeserializeFromStr, serde_with::SerializeDisplay)
17)]
18pub struct OutputName(SmolStr);
19
20impl OutputName {
21    pub fn as_str(&self) -> &str {
22        self.0.as_str()
23    }
24
25    /// Tries to construct from a &'static str.
26    pub fn from_static(s: &'static str) -> Result<Self, ParseOutputNameError> {
27        validate(s)?;
28
29        Ok(Self(SmolStr::new_static(s)))
30    }
31
32    pub const fn out() -> Self {
33        Self(SmolStr::new_static("out"))
34    }
35}
36
37fn validate<S: AsRef<str>>(s: S) -> Result<(), ParseOutputNameError> {
38    store_path::validate_name(s.as_ref())?;
39
40    // Disallow the reserved 'drv' name, which may appear in store path names,
41    // but not in Derivations.
42    if s.as_ref() == "drv" {
43        return Err(ParseOutputNameError::ReservedNameDrv);
44    }
45
46    Ok(())
47}
48
49impl PartialEq<&str> for OutputName {
50    fn eq(&self, other: &&str) -> bool {
51        self.as_str() == *other
52    }
53}
54impl PartialEq<str> for OutputName {
55    fn eq(&self, other: &str) -> bool {
56        self.as_str() == other
57    }
58}
59impl PartialEq<OutputName> for &str {
60    fn eq(&self, other: &OutputName) -> bool {
61        *self == other.as_str()
62    }
63}
64impl PartialEq<OutputName> for str {
65    fn eq(&self, other: &OutputName) -> bool {
66        self == other.as_str()
67    }
68}
69impl PartialEq<String> for OutputName {
70    fn eq(&self, other: &String) -> bool {
71        self.as_str() == other.as_str()
72    }
73}
74
75impl PartialEq<OutputName> for String {
76    fn eq(&self, other: &OutputName) -> bool {
77        self.as_str() == other.as_str()
78    }
79}
80
81impl fmt::Display for OutputName {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(self.as_str())
84    }
85}
86
87impl AsRef<str> for OutputName {
88    fn as_ref(&self) -> &str {
89        self.as_str()
90    }
91}
92
93impl std::borrow::Borrow<str> for OutputName {
94    fn borrow(&self) -> &str {
95        self.as_str()
96    }
97}
98
99impl Default for OutputName {
100    fn default() -> Self {
101        Self::out()
102    }
103}
104
105impl FromStr for OutputName {
106    type Err = ParseOutputNameError;
107
108    fn from_str(s: &str) -> Result<Self, Self::Err> {
109        validate(s)?;
110
111        Ok(Self(SmolStr::new(s)))
112    }
113}
114
115impl TryFrom<String> for OutputName {
116    type Error = ParseOutputNameError;
117
118    fn try_from(value: String) -> Result<Self, Self::Error> {
119        validate(&value)?;
120
121        Ok(Self(SmolStr::new(value)))
122    }
123}
124
125impl From<OutputName> for String {
126    fn from(value: OutputName) -> Self {
127        value.0.into()
128    }
129}
130
131impl From<&OutputName> for String {
132    fn from(value: &OutputName) -> Self {
133        value.as_str().into()
134    }
135}
136
137#[derive(thiserror::Error, Debug, PartialEq)]
138pub enum ParseOutputNameError {
139    #[error("Invalid length")]
140    InvalidLength,
141    #[error("Invalid name")]
142    InvalidName,
143    #[error("Invalid reserved name 'drv'")]
144    ReservedNameDrv,
145}
146
147impl From<store_path::ParseStorePathNameError> for ParseOutputNameError {
148    fn from(value: store_path::ParseStorePathNameError) -> Self {
149        match value {
150            store_path::ParseStorePathNameError::Length => ParseOutputNameError::InvalidLength,
151            store_path::ParseStorePathNameError::Name => ParseOutputNameError::InvalidName,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use rstest::rstest;
159
160    use super::OutputName;
161
162    #[rstest]
163    #[should_panic(expected = "InvalidName")]
164    #[case("bin{n")]
165    #[should_panic(expected = "InvalidName")]
166    #[case("bin{n")]
167    #[should_panic(expected = "InvalidName")]
168    #[case(" bin{n")]
169    #[should_panic(expected = "InvalidName")]
170    #[case("invalid name")]
171    #[should_panic(expected = "InvalidName")]
172    #[case("invalid/name")]
173    #[should_panic(expected = "ReservedNameDrv")]
174    #[case("drv")]
175    #[should_panic(expected = "InvalidLength")]
176    #[case("")]
177    fn parse_fail(#[case] value: &str) {
178        value.parse::<OutputName>().unwrap();
179    }
180
181    #[rstest]
182    #[case("out")]
183    #[case("dev")]
184    #[case("lib")]
185    #[case("bin")]
186    #[case("debug")]
187    fn parse(#[case] value: &str) {
188        value.parse::<OutputName>().unwrap();
189    }
190
191    #[test]
192    fn size() {
193        assert_eq!(size_of::<OutputName>(), size_of::<String>());
194    }
195}