Skip to main content

nix_compat/store_path/
borrowed.rs

1use smol_str::SmolStr;
2use std::fmt;
3
4use crate::{
5    nixbase32,
6    store_path::{
7        DIGEST_SIZE, ENCODED_DIGEST_SIZE, ParseStorePathError, STORE_DIR_WITH_SLASH, StorePath,
8        validate_name,
9    },
10};
11
12/// Like [StorePath], but with a `&str` for the name.
13#[derive(Clone, Debug, Eq, PartialEq, Hash)]
14pub struct StorePathRef<'a> {
15    digest: [u8; DIGEST_SIZE],
16    name: &'a str,
17}
18
19impl<'a> StorePathRef<'a> {
20    pub fn digest(&self) -> &[u8; DIGEST_SIZE] {
21        &self.digest
22    }
23
24    pub fn name(&self) -> &'a str {
25        self.name
26    }
27
28    pub fn to_owned(&self) -> StorePath {
29        StorePath {
30            digest: self.digest,
31            name: SmolStr::new(self.name),
32        }
33    }
34
35    /// Construct by passing the `$digest-$name` string that comes after
36    /// [STORE_DIR_WITH_SLASH].
37    pub fn from_bytes(s: &'a [u8]) -> Result<Self, ParseStorePathError> {
38        // the whole string needs to be at least:
39        //
40        // - 32 characters (encoded hash)
41        // - 1 dash
42        // - 1 character for the name
43        if s.len() < ENCODED_DIGEST_SIZE + 2 {
44            Err(ParseStorePathError::Length)?
45        }
46
47        let digest = nixbase32::decode_fixed(&s[..ENCODED_DIGEST_SIZE])?;
48
49        if s[ENCODED_DIGEST_SIZE] != b'-' {
50            return Err(ParseStorePathError::MissingDash);
51        }
52
53        Ok(Self {
54            digest,
55            name: validate_name(&s[ENCODED_DIGEST_SIZE + 1..])?,
56        })
57    }
58
59    /// Construct from a name and digest.
60    /// The name is validated, and the digest checked for size.
61    pub fn from_name_and_digest(name: &'a str, digest: &[u8]) -> Result<Self, ParseStorePathError> {
62        let digest_fixed = digest.try_into().map_err(|_| ParseStorePathError::Length)?;
63        Self::from_name_and_digest_fixed(name, digest_fixed)
64    }
65
66    /// Construct from a name and digest of correct length.
67    /// The name is validated.
68    pub fn from_name_and_digest_fixed(
69        name: &'a str,
70        digest: [u8; DIGEST_SIZE],
71    ) -> Result<Self, ParseStorePathError> {
72        Ok(Self {
73            name: validate_name(name)?,
74            digest,
75        })
76    }
77
78    /// Construct from an absolute store path string.
79    /// This is equivalent to calling [StorePathRef::from_bytes], but stripping
80    /// the [STORE_DIR_WITH_SLASH] prefix before.
81    pub fn from_absolute_path(s: &'a [u8]) -> Result<Self, ParseStorePathError> {
82        match s.strip_prefix(STORE_DIR_WITH_SLASH.as_bytes()) {
83            Some(s_stripped) => Self::from_bytes(s_stripped),
84            None => Err(ParseStorePathError::MissingStoreDir),
85        }
86    }
87
88    /// Decompose a string into a [StorePathRef] and a [std::path::Path]
89    /// containing the rest of the path, or an error.
90    pub fn from_absolute_path_full<'p, P>(
91        path: &'p P,
92    ) -> Result<(Self, &'p std::path::Path), ParseStorePathError>
93    where
94        P: AsRef<std::path::Path> + 'p + ?Sized,
95        'p: 'a,
96    {
97        // strip [STORE_DIR_WITH_SLASH] from path
98        let p = path
99            .as_ref()
100            .strip_prefix(STORE_DIR_WITH_SLASH)
101            .map_err(|_| ParseStorePathError::MissingStoreDir)?;
102
103        let mut components = p.components();
104
105        use bstr::ByteSlice;
106        let first_component = <[u8]>::from_os_str(
107            components
108                .next()
109                .ok_or(ParseStorePathError::Length)?
110                .as_os_str(),
111        )
112        .ok_or(ParseStorePathError::Name)?;
113
114        // The first component must be parse-able as a [StorePath].
115        if first_component.len() < 34 {
116            return Err(ParseStorePathError::Length);
117        }
118
119        let store_path = Self::from_bytes(first_component)?;
120
121        Ok((store_path, components.as_path()))
122    }
123
124    /// Returns as an absolute store path (prefixed with [STORE_DIR_WITH_SLASH]).
125    pub fn to_absolute_path(&self) -> String {
126        self.as_absolute_path_fmt().to_string()
127    }
128
129    /// Returns a formatter writing an absolute store path (prefixed with [STORE_DIR_WITH_SLASH]).
130    pub fn as_absolute_path_fmt(&'a self) -> impl std::fmt::Display + 'a {
131        struct WithAbsolutePath<'a>(&'a StorePathRef<'a>);
132
133        impl std::fmt::Display for WithAbsolutePath<'_> {
134            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135                write!(f, "{STORE_DIR_WITH_SLASH}{}", self.0)
136            }
137        }
138        WithAbsolutePath(self)
139    }
140}
141
142impl fmt::Display for StorePathRef<'_> {
143    /// The string representation of a store path starts with a digest (20
144    /// bytes), [crate::nixbase32]-encoded, followed by a `-`,
145    /// and ends with the name.
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}-{}", nixbase32::encode(&self.digest), self.name)
148    }
149}
150
151impl PartialOrd for StorePathRef<'_> {
152    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
153        Some(self.cmp(other))
154    }
155}
156
157impl<'a> From<StorePathRef<'a>> for StorePath {
158    fn from(value: StorePathRef<'a>) -> Self {
159        Self {
160            digest: value.digest,
161            name: SmolStr::new(value.name),
162        }
163    }
164}
165
166impl StorePath {
167    pub fn as_ref(&self) -> StorePathRef<'_> {
168        StorePathRef {
169            digest: self.digest,
170            name: &self.name,
171        }
172    }
173}
174
175/// `StorePath`s are sorted by their reverse digest to match the sorting order
176/// of the nixbase32-encoded string.
177impl Ord for StorePathRef<'_> {
178    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
179        self.digest.iter().rev().cmp(other.digest.iter().rev())
180    }
181}
182
183// Ensures it's possible to get() from a HashMap using StorePathRef<'_>,
184// even if it itself uses StorePath<String>
185#[cfg(feature = "hashbrown")]
186impl hashbrown::Equivalent<StorePath> for StorePathRef<'_> {
187    fn equivalent(&self, key: &StorePath) -> bool {
188        self.digest() == key.digest() && self.name() == key.name()
189    }
190}
191
192#[cfg(feature = "serde")]
193impl<'a, 'de: 'a> serde::Deserialize<'de> for StorePathRef<'a> {
194    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
195    where
196        D: serde::Deserializer<'de>,
197    {
198        let string: &'de str = serde::Deserialize::deserialize(deserializer)?;
199        let stripped: Option<&str> = string.strip_prefix(STORE_DIR_WITH_SLASH);
200        let stripped: &str = stripped.ok_or_else(|| {
201            serde::de::Error::invalid_value(
202                serde::de::Unexpected::Str(string),
203                &"store path prefix",
204            )
205        })?;
206        StorePathRef::from_bytes(stripped.as_bytes()).map_err(|_| {
207            serde::de::Error::invalid_value(serde::de::Unexpected::Str(string), &"StorePath")
208        })
209    }
210}
211
212#[cfg(feature = "serde")]
213impl serde::Serialize for StorePathRef<'_> {
214    fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error>
215    where
216        SR: serde::Serializer,
217    {
218        let string: String = self.to_absolute_path();
219        string.serialize(serializer)
220    }
221}