Skip to main content

nix_compat/derivation/
output_name.rs

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