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