nix_compat/store_path/
mod.rs

1use crate::nixbase32;
2use data_encoding::{BASE64, DecodeError};
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use std::{
6    fmt,
7    path::Path,
8    str::{self, FromStr},
9};
10use thiserror;
11
12mod utils;
13
14pub use utils::*;
15
16pub const DIGEST_SIZE: usize = 20;
17pub const ENCODED_DIGEST_SIZE: usize = nixbase32::encode_len(DIGEST_SIZE);
18
19// The store dir prefix, without trailing slash.
20// That's usually where the Nix store is mounted at.
21pub const STORE_DIR: &str = "/nix/store";
22pub const STORE_DIR_WITH_SLASH: &str = "/nix/store/";
23
24/// Errors that can occur when parsing a literal store path
25#[derive(Debug, PartialEq, Eq, thiserror::Error)]
26pub enum Error {
27    #[error("Dash is missing between hash and name")]
28    MissingDash,
29    #[error("Hash encoding is invalid: {0}")]
30    InvalidHashEncoding(#[from] DecodeError),
31    #[error("Invalid length")]
32    InvalidLength,
33    #[error(
34        "Invalid name: \"{}\", character at position {} is invalid",
35        std::str::from_utf8(.0).unwrap_or(&BASE64.encode(.0)),
36        .1,
37    )]
38    InvalidName(Vec<u8>, u8),
39    #[error("Tried to parse an absolute path which was missing the store dir prefix.")]
40    MissingStoreDir,
41}
42
43/// Represents a path in the Nix store (a direct child of [STORE_DIR]).
44///
45/// It consists of a digest (20 bytes), and a name, which is a string.
46/// The name may only contain ASCII alphanumeric, or one of the following
47/// characters: `-`, `_`, `.`, `+`, `?`, `=`.
48/// The name is usually used to describe the pname and version of a package.
49/// Derivation paths can also be represented as store paths, their names just
50/// end with the `.drv` prefix.
51///
52/// A [StorePath] does not encode any additional subpath "inside" the store
53/// path.
54#[derive(Clone, Debug)]
55pub struct StorePath<S> {
56    digest: [u8; DIGEST_SIZE],
57    name: S,
58}
59
60impl<S> PartialEq for StorePath<S>
61where
62    S: AsRef<str>,
63{
64    fn eq(&self, other: &Self) -> bool {
65        self.digest() == other.digest() && self.name().as_ref() == other.name().as_ref()
66    }
67}
68
69impl<S> Eq for StorePath<S> where S: AsRef<str> {}
70
71impl<S> std::hash::Hash for StorePath<S>
72where
73    S: AsRef<str>,
74{
75    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
76        state.write(&self.digest);
77        state.write(self.name.as_ref().as_bytes());
78    }
79}
80
81/// Like [StorePath], but without a heap allocation for the name.
82/// Used by [StorePath] for parsing.
83pub type StorePathRef<'a> = StorePath<&'a str>;
84
85impl<S> StorePath<S>
86where
87    S: AsRef<str>,
88{
89    pub fn digest(&self) -> &[u8; DIGEST_SIZE] {
90        &self.digest
91    }
92
93    pub fn name(&self) -> &S {
94        &self.name
95    }
96
97    pub fn as_ref(&self) -> StorePathRef<'_> {
98        StorePathRef {
99            digest: self.digest,
100            name: self.name.as_ref(),
101        }
102    }
103
104    pub fn to_owned(&self) -> StorePath<String> {
105        StorePath {
106            digest: self.digest,
107            name: self.name.as_ref().to_string(),
108        }
109    }
110
111    /// Construct a [StorePath] by passing the `$digest-$name` string
112    /// that comes after [STORE_DIR_WITH_SLASH].
113    pub fn from_bytes<'a>(s: &'a [u8]) -> Result<Self, Error>
114    where
115        S: From<&'a str>,
116    {
117        // the whole string needs to be at least:
118        //
119        // - 32 characters (encoded hash)
120        // - 1 dash
121        // - 1 character for the name
122        if s.len() < ENCODED_DIGEST_SIZE + 2 {
123            Err(Error::InvalidLength)?
124        }
125
126        let digest = nixbase32::decode_fixed(&s[..ENCODED_DIGEST_SIZE])?;
127
128        if s[ENCODED_DIGEST_SIZE] != b'-' {
129            return Err(Error::MissingDash);
130        }
131
132        Ok(StorePath {
133            digest,
134            name: validate_name(&s[ENCODED_DIGEST_SIZE + 1..])?.into(),
135        })
136    }
137
138    /// Construct a [StorePathRef] from a name and digest.
139    /// The name is validated, and the digest checked for size.
140    pub fn from_name_and_digest<'a>(name: &'a str, digest: &[u8]) -> Result<Self, Error>
141    where
142        S: From<&'a str>,
143    {
144        let digest_fixed = digest.try_into().map_err(|_| Error::InvalidLength)?;
145        Self::from_name_and_digest_fixed(name, digest_fixed)
146    }
147
148    /// Construct a [StorePathRef] from a name and digest of correct length.
149    /// The name is validated.
150    pub fn from_name_and_digest_fixed<'a>(
151        name: &'a str,
152        digest: [u8; DIGEST_SIZE],
153    ) -> Result<Self, Error>
154    where
155        S: From<&'a str>,
156    {
157        Ok(Self {
158            name: validate_name(name)?.into(),
159            digest,
160        })
161    }
162
163    /// Construct a [StorePathRef] from an absolute store path string.
164    /// This is equivalent to calling [StorePathRef::from_bytes], but stripping
165    /// the [STORE_DIR_WITH_SLASH] prefix before.
166    pub fn from_absolute_path<'a>(s: &'a [u8]) -> Result<Self, Error>
167    where
168        S: From<&'a str>,
169    {
170        match s.strip_prefix(STORE_DIR_WITH_SLASH.as_bytes()) {
171            Some(s_stripped) => Self::from_bytes(s_stripped),
172            None => Err(Error::MissingStoreDir),
173        }
174    }
175
176    /// Decompose a string into a [StorePath] and a [Path] containing the
177    /// rest of the path, or an error.
178    #[cfg(target_family = "unix")]
179    pub fn from_absolute_path_full<'a, P>(path: &'a P) -> Result<(Self, &'a Path), Error>
180    where
181        S: From<&'a str>,
182        P: AsRef<std::path::Path> + ?Sized,
183    {
184        // strip [STORE_DIR_WITH_SLASH] from s
185        let p = path
186            .as_ref()
187            .strip_prefix(STORE_DIR_WITH_SLASH)
188            .map_err(|_e| Error::MissingStoreDir)?;
189
190        let mut it = Path::new(p).components();
191
192        // The first component of the rest must be parse-able as a [StorePath]
193        let first_component = it.next().ok_or(Error::InvalidLength)?;
194        let store_path = StorePath::from_bytes(first_component.as_os_str().as_encoded_bytes())?;
195
196        // collect rest
197        let rest_buf = it.as_path();
198
199        Ok((store_path, rest_buf))
200    }
201
202    /// Returns an absolute store path string.
203    /// That is just the string representation, prefixed with the store prefix
204    /// ([STORE_DIR_WITH_SLASH]),
205    pub fn to_absolute_path(&self) -> String {
206        format!("{STORE_DIR_WITH_SLASH}{self}")
207    }
208}
209
210impl<S> PartialOrd for StorePath<S>
211where
212    S: AsRef<str>,
213{
214    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
215        Some(self.cmp(other))
216    }
217}
218
219/// `StorePath`s are sorted by their reverse digest to match the sorting order
220/// of the nixbase32-encoded string.
221impl<S> Ord for StorePath<S>
222where
223    S: AsRef<str>,
224{
225    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
226        self.digest.iter().rev().cmp(other.digest.iter().rev())
227    }
228}
229
230impl FromStr for StorePath<String> {
231    type Err = Error;
232
233    /// Construct a [StorePath] by passing the `$digest-$name` string
234    /// that comes after [STORE_DIR_WITH_SLASH].
235    fn from_str(s: &str) -> Result<Self, Self::Err> {
236        StorePath::<String>::from_bytes(s.as_bytes())
237    }
238}
239
240#[cfg(feature = "serde")]
241impl<'a, 'de: 'a, S> Deserialize<'de> for StorePath<S>
242where
243    S: AsRef<str> + From<&'a str>,
244{
245    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246    where
247        D: serde::Deserializer<'de>,
248    {
249        let string: &'de str = Deserialize::deserialize(deserializer)?;
250        let stripped: Option<&str> = string.strip_prefix(STORE_DIR_WITH_SLASH);
251        let stripped: &str = stripped.ok_or_else(|| {
252            serde::de::Error::invalid_value(
253                serde::de::Unexpected::Str(string),
254                &"store path prefix",
255            )
256        })?;
257        StorePath::from_bytes(stripped.as_bytes()).map_err(|_| {
258            serde::de::Error::invalid_value(serde::de::Unexpected::Str(string), &"StorePath")
259        })
260    }
261}
262
263#[cfg(feature = "serde")]
264impl<S> Serialize for StorePath<S>
265where
266    S: AsRef<str>,
267{
268    fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error>
269    where
270        SR: serde::Serializer,
271    {
272        let string: String = self.to_absolute_path();
273        string.serialize(serializer)
274    }
275}
276
277/// NAME_CHARS contains `true` for bytes that are valid in store path names.
278static NAME_CHARS: [bool; 256] = {
279    let mut tbl = [false; 256];
280    let mut c = 0;
281
282    loop {
283        tbl[c as usize] = matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'-' | b'_' | b'?' | b'=' | b'.');
284
285        if c == u8::MAX {
286            break;
287        }
288
289        c += 1;
290    }
291
292    tbl
293};
294
295/// Checks a given &[u8] to match the restrictions for [StorePath::name], and
296/// returns the name as string if successful.
297pub(crate) fn validate_name(s: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str, Error> {
298    let s = s.as_ref();
299
300    // Empty or excessively long names are not allowed.
301    if s.is_empty() || s.len() > 211 {
302        return Err(Error::InvalidLength);
303    }
304
305    let mut valid = true;
306    for &c in s {
307        valid = valid && NAME_CHARS[c as usize];
308    }
309
310    if !valid {
311        for (i, &c) in s.iter().enumerate() {
312            if !NAME_CHARS[c as usize] {
313                return Err(Error::InvalidName(s.to_vec(), i as u8));
314            }
315        }
316
317        unreachable!();
318    }
319
320    // SAFETY: We permit a subset of ASCII, which guarantees valid UTF-8.
321    Ok(unsafe { str::from_utf8_unchecked(s) })
322}
323
324impl<S> fmt::Display for StorePath<S>
325where
326    S: AsRef<str>,
327{
328    /// The string representation of a store path starts with a digest (20
329    /// bytes), [crate::nixbase32]-encoded, followed by a `-`,
330    /// and ends with the name.
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        write!(
333            f,
334            "{}-{}",
335            nixbase32::encode(&self.digest),
336            self.name.as_ref()
337        )
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::Error;
344    use std::cmp::Ordering;
345    use std::path::PathBuf;
346
347    use crate::store_path::{DIGEST_SIZE, StorePath, StorePathRef};
348    use hex_literal::hex;
349    use pretty_assertions::assert_eq;
350    use rstest::rstest;
351    #[cfg(feature = "serde")]
352    use serde::Deserialize;
353
354    /// An example struct, holding a StorePathRef.
355    /// Used to test deserializing StorePathRef.
356    #[cfg(feature = "serde")]
357    #[derive(Deserialize)]
358    struct Container<'a> {
359        #[serde(borrow)]
360        store_path: StorePathRef<'a>,
361    }
362
363    #[test]
364    fn happy_path() {
365        let example_nix_path_str =
366            "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
367        let nixpath = StorePathRef::from_bytes(example_nix_path_str.as_bytes())
368            .expect("Error parsing example string");
369
370        let expected_digest: [u8; DIGEST_SIZE] = hex!("8a12321522fd91efbd60ebb2481af88580f61600");
371
372        assert_eq!("net-tools-1.60_p20170221182432", *nixpath.name());
373        assert_eq!(nixpath.digest, expected_digest);
374
375        assert_eq!(example_nix_path_str, nixpath.to_string())
376    }
377
378    #[test]
379    fn store_path_ordering() {
380        let store_paths = [
381            "/nix/store/0lk5dgi01r933abzfj9c9wlndg82yd3g-psutil-5.9.6.tar.gz.drv",
382            "/nix/store/1xj43bva89f9qmwm37zl7r3d7m67i9ck-shorttoc-1.3-tex.drv",
383            "/nix/store/2gb633czchi20jq1kqv70rx2yvvgins8-lifted-base-0.2.3.12.tar.gz.drv",
384            "/nix/store/2vksym3r3zqhp15q3fpvw2mnvffv11b9-docbook-xml-4.5.zip.drv",
385            "/nix/store/5q918awszjcz5720xvpc2czbg1sdqsf0-rust_renaming-0.1.0-lib",
386            "/nix/store/7jw30i342sr2p1fmz5xcfnch65h4zbd9-dbus-1.14.10.tar.xz.drv",
387            "/nix/store/96yqwqhnp3qya4rf4n0rcl0lwvrylp6k-eap8021x-222.40.1.tar.gz.drv",
388            "/nix/store/9gjqg36a1v0axyprbya1hkaylmnffixg-virtualenv-20.24.5.tar.gz.drv",
389            "/nix/store/a4i5mci2g9ada6ff7ks38g11dg6iqyb8-perl-5.32.1.drv",
390            "/nix/store/a5g76ljava4h5pxlggz3aqdhs3a4fk6p-ToolchainInfo.plist.drv",
391            "/nix/store/db46l7d6nswgz4ffp1mmd56vjf9g51v6-version.plist.drv",
392            "/nix/store/g6f7w20sd7vwy0rc1r4bfsw4ciclrm4q-crates-io-num_cpus-1.12.0.drv",
393            "/nix/store/iw82n1wwssb8g6772yddn8c3vafgv9np-bootstrap-stage1-sysctl-stdenv-darwin.drv",
394            "/nix/store/lp78d1y5wxpcn32d5c4r7xgbjwiw0cgf-logo.svg.drv",
395            "/nix/store/mf00ank13scv1f9l1zypqdpaawjhfr3s-python3.11-psutil-5.9.6.drv",
396            "/nix/store/mpfml61ra7pz90124jx9r3av0kvkz2w1-perl5.36.0-Encode-Locale-1.05",
397            "/nix/store/qhsvwx4h87skk7c4mx0xljgiy3z93i23-source.drv",
398            "/nix/store/riv7d73adim8hq7i04pr8kd0jnj93nav-fdk-aac-2.0.2.tar.gz.drv",
399            "/nix/store/s64b9031wga7vmpvgk16xwxjr0z9ln65-human-signals-5.0.0.tgz-extracted",
400            "/nix/store/w6svg3m2xdh6dhx0gl1nwa48g57d3hxh-thiserror-1.0.49",
401        ];
402
403        for w in store_paths.windows(2) {
404            if w.len() < 2 {
405                continue;
406            }
407            let (pa, _) = StorePathRef::from_absolute_path_full(w[0]).expect("parseable");
408            let (pb, _) = StorePathRef::from_absolute_path_full(w[1]).expect("parseable");
409            assert_eq!(
410                Ordering::Less,
411                pa.cmp(&pb),
412                "{:?} not less than {:?}",
413                w[0],
414                w[1]
415            );
416        }
417    }
418
419    /// This is the store path *accepted* when `nix-store --add`'ing an
420    /// empty `.gitignore` file.
421    ///
422    /// Nix 2.4 accidentally permitted this behaviour, but the revert came
423    /// too late to beat Hyrum's law. It is now considered permissible.
424    ///
425    /// https://github.com/NixOS/nix/pull/9095 (revert)
426    /// https://github.com/NixOS/nix/pull/9867 (revert-of-revert)
427    #[test]
428    fn starts_with_dot() {
429        StorePathRef::from_bytes(b"fli4bwscgna7lpm7v5xgnjxrxh0yc7ra-.gitignore")
430            .expect("must succeed");
431    }
432
433    #[test]
434    fn empty_name() {
435        StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-").expect_err("must fail");
436    }
437
438    #[test]
439    fn excessive_length() {
440        StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
441            .expect_err("must fail");
442    }
443
444    #[test]
445    fn invalid_hash_length() {
446        StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432")
447            .expect_err("must fail");
448    }
449
450    #[test]
451    fn invalid_encoding_hash() {
452        StorePathRef::from_bytes(
453            b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432",
454        )
455        .expect_err("must fail");
456    }
457
458    #[test]
459    fn more_than_just_the_bare_nix_store_path() {
460        StorePathRef::from_bytes(
461            b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432/bin/arp",
462        )
463        .expect_err("must fail");
464    }
465
466    #[test]
467    fn no_dash_between_hash_and_name() {
468        StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44lanet-tools-1.60_p20170221182432")
469            .expect_err("must fail");
470    }
471
472    #[test]
473    fn absolute_path() {
474        let example_nix_path_str =
475            "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
476        let nixpath_expected =
477            StorePathRef::from_bytes(example_nix_path_str.as_bytes()).expect("must parse");
478
479        let nixpath_actual = StorePathRef::from_absolute_path(
480            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432".as_bytes(),
481        )
482        .expect("must parse");
483
484        assert_eq!(nixpath_expected, nixpath_actual);
485
486        assert_eq!(
487            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
488            nixpath_actual.to_absolute_path(),
489        );
490    }
491
492    #[test]
493    fn absolute_path_missing_prefix() {
494        assert_eq!(
495            Error::MissingStoreDir,
496            StorePathRef::from_absolute_path(b"foobar-123").expect_err("must fail")
497        );
498    }
499
500    #[cfg(feature = "serde")]
501    #[test]
502    fn serialize_ref() {
503        let nixpath_actual = StorePathRef::from_bytes(
504            b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
505        )
506        .expect("can parse");
507
508        let serialized = serde_json::to_string(&nixpath_actual).expect("can serialize");
509
510        assert_eq!(
511            "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"",
512            &serialized
513        );
514    }
515
516    #[cfg(feature = "serde")]
517    #[test]
518    fn serialize_owned() {
519        let nixpath_actual = StorePathRef::from_bytes(
520            b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
521        )
522        .expect("can parse");
523
524        let serialized = serde_json::to_string(&nixpath_actual).expect("can serialize");
525
526        assert_eq!(
527            "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"",
528            &serialized
529        );
530    }
531
532    #[cfg(feature = "serde")]
533    #[test]
534    fn deserialize_ref() {
535        let store_path_str_json =
536            "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"";
537
538        let store_path: StorePathRef<'_> =
539            serde_json::from_str(store_path_str_json).expect("valid json");
540
541        assert_eq!(
542            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
543            store_path.to_absolute_path()
544        );
545    }
546
547    #[cfg(feature = "serde")]
548    #[test]
549    fn deserialize_ref_container() {
550        let str_json = "{\"store_path\":\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"}";
551
552        let container: Container<'_> = serde_json::from_str(str_json).expect("must deserialize");
553
554        assert_eq!(
555            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
556            container.store_path.to_absolute_path()
557        );
558    }
559
560    #[cfg(feature = "serde")]
561    #[test]
562    fn deserialize_owned() {
563        let store_path_str_json =
564            "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"";
565
566        let store_path: StorePath<String> =
567            serde_json::from_str(store_path_str_json).expect("valid json");
568
569        assert_eq!(
570            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
571            store_path.to_absolute_path()
572        );
573    }
574
575    #[rstest]
576    #[case::without_prefix(
577        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
578        StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::new())]
579    #[case::without_prefix_but_trailing_slash(
580        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/",
581        StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::new())]
582    #[case::with_prefix(
583        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp",
584        StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::from("bin/arp"))]
585    #[case::with_prefix_and_trailing_slash(
586        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp/",
587        StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::from("bin/arp/"))]
588    fn from_absolute_path_full(
589        #[case] s: &str,
590        #[case] exp_store_path: StorePath<&str>,
591        #[case] exp_path: PathBuf,
592    ) {
593        let (actual_store_path, actual_path) =
594            StorePath::from_absolute_path_full(s).expect("must succeed");
595
596        assert_eq!(exp_store_path, actual_store_path);
597        assert_eq!(exp_path, actual_path);
598    }
599
600    #[test]
601    fn from_absolute_path_errors() {
602        assert_eq!(
603            Error::InvalidLength,
604            StorePathRef::from_absolute_path_full("/nix/store/").expect_err("must fail")
605        );
606        assert_eq!(
607            Error::InvalidLength,
608            StorePathRef::from_absolute_path_full("/nix/store/foo").expect_err("must fail")
609        );
610        assert_eq!(
611            Error::MissingStoreDir,
612            StorePathRef::from_absolute_path_full(
613                "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
614            )
615            .expect_err("must fail")
616        );
617    }
618}