Skip to main content

nix_compat/store_path/
name.rs

1use std::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
2
3use smol_str::SmolStr;
4
5/// Extension for derivation names.
6pub const DRV_EXT: &str = ".drv";
7
8/// Maximum number of bytes in a store path name.
9pub const MAX_NAME_LEN: usize = 211;
10
11/// An owned validated name of a store path.
12///
13/// The name may only contain ASCII alphanumeric, or one of the following
14/// characters: `-`, `_`, `.`, `+`, `?`, `=` and must be shorter than 211 characters long.
15#[derive(Clone, Debug, Eq, PartialEq, Hash)]
16pub struct StorePathNameBuf(SmolStr);
17
18impl StorePathNameBuf {
19    /// Maximum number of bytes in a store path name.
20    pub const fn max_len() -> usize {
21        MAX_NAME_LEN
22    }
23
24    ///Return default name of sources.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// # use nix_compat::store_path::StorePathNameBuf;
30    /// assert_eq!(StorePathNameBuf::source(), "source");
31    /// ```
32    pub fn source() -> StorePathNameBuf {
33        StorePathName::source().to_owned()
34    }
35
36    /// Checks a given `String` to match the restrictions for store path names, and
37    /// returns the name as `StorePathNameBuf` if successful.
38    pub fn from_string(name: String) -> Result<Self, ParseStorePathNameError> {
39        validate_name(name.as_bytes())?;
40        Ok(Self(SmolStr::new(name)))
41    }
42
43    /// Checks a given `SmolStr` to match the restrictions for store path names, and
44    /// returns the name as `StorePathNameBuf` if successful.
45    pub fn from_smol_str(name: SmolStr) -> Result<Self, ParseStorePathNameError> {
46        validate_name(name.as_bytes())?;
47        Ok(Self(name))
48    }
49
50    /// Checks a given `&[u8]` to match the restrictions for store path names, and
51    /// returns the name as `StorePathNameBuf` if successful.
52    pub fn from_slice(name: &[u8]) -> Result<Self, ParseStorePathNameError> {
53        let name = validate_name(name)?;
54        Ok(name.into())
55    }
56
57    /// Coerces to a [`StorePathName`] slice.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// # use nix_compat::store_path::{StorePathName, StorePathNameBuf};
63    /// let name = StorePathNameBuf::try_from("snix-build").unwrap();
64    /// assert_eq!(StorePathName::from_str("snix-build").unwrap(), name.as_name());
65    /// ```
66    #[must_use]
67    #[inline(always)]
68    pub fn as_name(&self) -> &StorePathName {
69        // SAFETY: A StorePathNameBuf always contains a valid store path name
70        unsafe { StorePathName::from_str_unchecked(&self.0) }
71    }
72}
73
74impl Default for StorePathNameBuf {
75    fn default() -> Self {
76        Self::source()
77    }
78}
79
80impl TryFrom<String> for StorePathNameBuf {
81    type Error = ParseStorePathNameError;
82
83    fn try_from(name: String) -> Result<Self, Self::Error> {
84        StorePathNameBuf::from_string(name)
85    }
86}
87
88impl TryFrom<SmolStr> for StorePathNameBuf {
89    type Error = ParseStorePathNameError;
90
91    fn try_from(name: SmolStr) -> Result<Self, Self::Error> {
92        StorePathNameBuf::from_smol_str(name)
93    }
94}
95
96impl TryFrom<&[u8]> for StorePathNameBuf {
97    type Error = ParseStorePathNameError;
98
99    fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
100        StorePathNameBuf::from_slice(name)
101    }
102}
103
104impl TryFrom<&str> for StorePathNameBuf {
105    type Error = ParseStorePathNameError;
106
107    fn try_from(name: &str) -> Result<Self, Self::Error> {
108        StorePathNameBuf::from_slice(name.as_bytes())
109    }
110}
111
112impl FromStr for StorePathNameBuf {
113    type Err = ParseStorePathNameError;
114
115    fn from_str(name: &str) -> Result<Self, Self::Err> {
116        StorePathNameBuf::from_slice(name.as_bytes())
117    }
118}
119
120impl AsRef<str> for StorePathNameBuf {
121    fn as_ref(&self) -> &str {
122        &self.0
123    }
124}
125
126impl AsRef<StorePathName> for StorePathNameBuf {
127    fn as_ref(&self) -> &StorePathName {
128        self.as_name()
129    }
130}
131
132impl Deref for StorePathNameBuf {
133    type Target = StorePathName;
134
135    fn deref(&self) -> &Self::Target {
136        self.as_name()
137    }
138}
139
140impl Borrow<StorePathName> for StorePathNameBuf {
141    fn borrow(&self) -> &StorePathName {
142        self.as_name()
143    }
144}
145
146impl<'a> From<&'a StorePathName> for StorePathNameBuf {
147    fn from(value: &'a StorePathName) -> Self {
148        value.to_owned()
149    }
150}
151
152impl PartialEq<&StorePathName> for StorePathNameBuf {
153    fn eq(&self, other: &&StorePathName) -> bool {
154        self.as_name() == *other
155    }
156}
157impl PartialEq<StorePathNameBuf> for &StorePathName {
158    fn eq(&self, other: &StorePathNameBuf) -> bool {
159        *self == other.as_name()
160    }
161}
162impl PartialEq<&str> for StorePathNameBuf {
163    fn eq(&self, other: &&str) -> bool {
164        self.as_str() == *other
165    }
166}
167impl PartialEq<str> for StorePathNameBuf {
168    fn eq(&self, other: &str) -> bool {
169        self.as_str() == other
170    }
171}
172impl PartialEq<StorePathNameBuf> for &str {
173    fn eq(&self, other: &StorePathNameBuf) -> bool {
174        *self == other.as_str()
175    }
176}
177impl PartialEq<StorePathNameBuf> for str {
178    fn eq(&self, other: &StorePathNameBuf) -> bool {
179        self == other.as_str()
180    }
181}
182impl PartialEq<String> for StorePathNameBuf {
183    fn eq(&self, other: &String) -> bool {
184        self.as_str() == other.as_str()
185    }
186}
187impl PartialEq<StorePathNameBuf> for String {
188    fn eq(&self, other: &StorePathNameBuf) -> bool {
189        self.as_str() == other.as_str()
190    }
191}
192
193impl fmt::Display for StorePathNameBuf {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        f.write_str(&self.0)
196    }
197}
198
199/// Validated name of a store path.
200///
201/// The name may only contain ASCII alphanumeric, or one of the following
202/// characters: `-`, `_`, `.`, `+`, `?`, `=` and must be shorter than 211 characters long.
203#[derive(Debug, Eq, PartialEq, Hash)]
204#[repr(transparent)]
205pub struct StorePathName(str);
206
207impl StorePathName {
208    /// Maximum number of bytes in a store path name.
209    pub const fn max_len() -> usize {
210        MAX_NAME_LEN
211    }
212
213    ///Return default name of sources.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # use nix_compat::store_path::StorePathName;
219    /// assert_eq!(StorePathName::source(), "source");
220    /// ```
221    pub const fn source() -> &'static StorePathName {
222        // SAFETY: `source` is a valid StorePathName
223        unsafe { StorePathName::from_str_unchecked("source") }
224    }
225
226    /// Converts a `str` to a `StorePathName` without checking that it is a valid name.
227    ///
228    /// # Safety
229    /// This function is unsafe because it does not check that the str passed to it is a
230    /// valid store path name. If this constraint is violated it may cause panics and memory safety
231    /// issues , as the rest of snix assumes that `StorePathName` contains a valid store path name.
232    pub const unsafe fn from_str_unchecked(name: &str) -> &StorePathName {
233        // SAFETY: `str` and `StorePathName` have the same ABI because of repr(transparent)
234        unsafe { &*(name as *const str as *const StorePathName) }
235    }
236
237    /// Checks a given `&str` to match the restrictions for `StorePathName`, and
238    /// returns the name as `&StorePathName` if successful.
239    #[expect(clippy::should_implement_trait)]
240    pub fn from_str(name: &str) -> Result<&StorePathName, ParseStorePathNameError> {
241        validate_name(name.as_bytes())
242    }
243
244    /// Checks a given `&[u8]` to match the restrictions for `StorePathName`, and
245    /// returns the name as `&StorePathName` if successful.
246    pub fn from_slice(name: &[u8]) -> Result<&StorePathName, ParseStorePathNameError> {
247        validate_name(name)
248    }
249
250    /// Checks a given `OsStr` to match the restrictions for `StorePathName`, and
251    /// returns the name as &StorePathName if successful
252    pub fn from_os_str(name: &std::ffi::OsStr) -> Result<&StorePathName, ParseStorePathNameError> {
253        validate_name(name.as_encoded_bytes())
254    }
255
256    /// Build a new `StorePathNameBuf` by adding `.drv` extension to this `StorePathName`. This
257    /// fails if this name already ends in `.drv` or the resulting name would be too long.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// # use nix_compat::store_path::StorePathName;
263    /// let n = StorePathName::from_str("snix-eval").unwrap();
264    /// assert_eq!(n.build_drv_name().unwrap(), "snix-eval.drv");
265    /// ```
266    pub fn build_drv_name(&self) -> Result<StorePathNameBuf, ParseStorePathNameError> {
267        if self.is_drv() {
268            return Err(ParseStorePathNameError::Name);
269        }
270        let s = smol_str::format_smolstr!("{self}{DRV_EXT}");
271        StorePathNameBuf::from_smol_str(s)
272    }
273
274    /// Returns `true` if this is the name of a derivation.
275    ///
276    /// Derivation names always end in `.drv` and has [`len()`] longer than 4.
277    ///
278    /// [`len()`]: Self::len
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// # use nix_compat::store_path::StorePathName;
284    /// let n = StorePathName::from_str("snix-eval").unwrap();
285    /// assert!(!n.is_drv());
286    ///
287    /// let b = n.build_drv_name().unwrap();
288    /// assert!(b.is_drv());
289    /// ```
290    #[must_use]
291    pub fn is_drv(&self) -> bool {
292        self.0.len() > DRV_EXT.len() && self.0.ends_with(DRV_EXT)
293    }
294
295    /// Remove `.drv` extension from this name or return `None` if this is not a derivation name.
296    pub fn strip_drv_ext(&self) -> Option<&StorePathName> {
297        self.strip_suffix(DRV_EXT)
298    }
299
300    /// Strip a suffix from this name or return `None` if the resulting name would have been empty.
301    pub fn strip_suffix(&self, suffix: &str) -> Option<&StorePathName> {
302        if let Some(new_name) = self.0.strip_suffix(suffix)
303            && !new_name.is_empty()
304        {
305            // SAFETY: When removing from a name it is always shorter and still contains valid characters
306            // The only exception is when all characters were stripped and that is checked above
307            unsafe { Some(StorePathName::from_str_unchecked(new_name)) }
308        } else {
309            None
310        }
311    }
312
313    /// Returns the length of this name.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// # use nix_compat::store_path::StorePathName;
319    /// assert_eq!(6, StorePathName::source().len());
320    /// ```
321    #[must_use]
322    #[inline]
323    #[expect(clippy::len_without_is_empty)]
324    pub const fn len(&self) -> usize {
325        self.0.len()
326    }
327
328    /// Converts a name to a string. To convert the string back into a name,
329    /// use the [`Self::from_str`] function.
330    ///
331    /// # Examples
332    ///
333    /// ```
334    /// # use nix_compat::store_path::StorePathName;
335    /// let s = StorePathName::source().as_str();
336    /// assert_eq!("source", s);
337    /// ```
338    #[must_use]
339    #[inline(always)]
340    pub const fn as_str(&self) -> &str {
341        &self.0
342    }
343
344    /// Converts a name to a byte slice. To convert the byte slice back into a name,
345    /// use the [`Self::from_slice`] function.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// # use nix_compat::store_path::StorePathName;
351    /// let bytes = StorePathName::source().as_bytes();
352    /// assert_eq!(b"source", bytes);
353    /// ```
354    #[must_use]
355    #[inline(always)]
356    pub const fn as_bytes(&self) -> &[u8] {
357        self.0.as_bytes()
358    }
359}
360
361impl AsRef<str> for StorePathName {
362    fn as_ref(&self) -> &str {
363        self.as_str()
364    }
365}
366
367impl AsRef<[u8]> for StorePathName {
368    fn as_ref(&self) -> &[u8] {
369        self.as_bytes()
370    }
371}
372
373impl AsRef<StorePathName> for StorePathName {
374    fn as_ref(&self) -> &StorePathName {
375        self
376    }
377}
378
379impl<'a> TryFrom<&'a [u8]> for &'a StorePathName {
380    type Error = ParseStorePathNameError;
381
382    fn try_from(name: &'a [u8]) -> Result<Self, Self::Error> {
383        StorePathName::from_slice(name)
384    }
385}
386
387impl<'a> TryFrom<&'a str> for &'a StorePathName {
388    type Error = ParseStorePathNameError;
389
390    fn try_from(name: &'a str) -> Result<Self, Self::Error> {
391        StorePathName::from_str(name)
392    }
393}
394
395impl PartialEq<&str> for StorePathName {
396    fn eq(&self, other: &&str) -> bool {
397        self.as_str() == *other
398    }
399}
400impl PartialEq<str> for StorePathName {
401    fn eq(&self, other: &str) -> bool {
402        self.as_str() == other
403    }
404}
405impl PartialEq<StorePathName> for &str {
406    fn eq(&self, other: &StorePathName) -> bool {
407        *self == other.as_str()
408    }
409}
410impl PartialEq<StorePathName> for str {
411    fn eq(&self, other: &StorePathName) -> bool {
412        self == other.as_str()
413    }
414}
415impl PartialEq<String> for StorePathName {
416    fn eq(&self, other: &String) -> bool {
417        self.as_str() == other.as_str()
418    }
419}
420impl PartialEq<StorePathName> for String {
421    fn eq(&self, other: &StorePathName) -> bool {
422        self.as_str() == other.as_str()
423    }
424}
425
426impl ToOwned for StorePathName {
427    type Owned = StorePathNameBuf;
428
429    fn to_owned(&self) -> Self::Owned {
430        StorePathNameBuf(SmolStr::new(&self.0))
431    }
432}
433
434impl fmt::Display for StorePathName {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        f.write_str(&self.0)
437    }
438}
439
440/// Error returned when creating a [`StorePathName`].
441#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
442pub enum ParseStorePathNameError {
443    /// Length of name was either too short (empty) or too long (longer than [`StorePathName::max_len()`]).
444    #[error("Invalid length")]
445    Length,
446    /// Name contained invalid characters.
447    #[error("Invalid name")]
448    Name,
449}
450
451/// NAME_CHARS contains `true` for bytes that are valid in store path names.
452static NAME_CHARS: [bool; 256] = {
453    let mut tbl = [false; 256];
454    let mut c = 0;
455
456    loop {
457        tbl[c as usize] = matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'-' | b'_' | b'?' | b'=' | b'.');
458
459        if c == u8::MAX {
460            break;
461        }
462
463        c += 1;
464    }
465
466    tbl
467};
468
469/// Checks a given `&[u8]` to match the restrictions for [StorePathName], and
470/// returns the name as `&StorePathName` if successful.
471fn validate_name(s: &[u8]) -> Result<&StorePathName, ParseStorePathNameError> {
472    // Empty or excessively long names are not allowed.
473    if s.is_empty() || s.len() > MAX_NAME_LEN {
474        return Err(ParseStorePathNameError::Length);
475    }
476
477    let mut valid = true;
478    for &c in s {
479        valid = valid && NAME_CHARS[c as usize];
480    }
481
482    if !valid {
483        for &c in s.iter() {
484            if !NAME_CHARS[c as usize] {
485                return Err(ParseStorePathNameError::Name);
486            }
487        }
488
489        unreachable!();
490    }
491
492    // SAFETY: We permit a subset of ASCII, which guarantees valid UTF-8.
493    let name = unsafe { std::str::from_utf8_unchecked(s) };
494    // SAFETY: We have justed checked that it is a name
495    let ret = unsafe { StorePathName::from_str_unchecked(name) };
496    Ok(ret)
497}