1use crate::nixbase32;
2use data_encoding::DecodeError;
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5use std::{
6 fmt,
7 str::{self, FromStr},
8};
9use thiserror;
10
11mod utils;
12
13pub use utils::*;
14
15pub const DIGEST_SIZE: usize = 20;
16pub const ENCODED_DIGEST_SIZE: usize = nixbase32::encode_len(DIGEST_SIZE);
17
18pub const STORE_DIR: &str = "/nix/store";
21pub const STORE_DIR_WITH_SLASH: &str = "/nix/store/";
22
23#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
25pub enum ValidateNameError {
26 #[error("Invalid length")]
27 InvalidLength,
28 #[error("Invalid name")]
29 InvalidName,
30}
31
32impl From<ValidateNameError> for Error {
33 fn from(value: ValidateNameError) -> Self {
34 match value {
35 ValidateNameError::InvalidLength => Error::InvalidLength,
36 ValidateNameError::InvalidName => Error::InvalidName,
37 }
38 }
39}
40
41#[derive(Debug, PartialEq, Eq, thiserror::Error)]
43pub enum Error {
44 #[error("Dash is missing between hash and name")]
45 MissingDash,
46 #[error("Hash encoding is invalid: {0}")]
47 InvalidHashEncoding(#[from] DecodeError),
48 #[error("Invalid length")]
49 InvalidLength,
50 #[error("Invalid name")]
51 InvalidName,
52 #[error("Tried to parse an absolute path which was missing the store dir prefix.")]
53 MissingStoreDir,
54}
55
56#[derive(Clone, Debug)]
68pub struct StorePath<S> {
69 digest: [u8; DIGEST_SIZE],
70 name: S,
71}
72
73impl<S> PartialEq for StorePath<S>
74where
75 S: AsRef<str>,
76{
77 fn eq(&self, other: &Self) -> bool {
78 self.digest() == other.digest() && self.name().as_ref() == other.name().as_ref()
79 }
80}
81
82impl<S> Eq for StorePath<S> where S: AsRef<str> {}
83
84impl<S> std::hash::Hash for StorePath<S>
85where
86 S: AsRef<str>,
87{
88 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
89 state.write(&self.digest);
90 state.write(self.name.as_ref().as_bytes());
91 }
92}
93
94#[cfg(feature = "hashbrown")]
97impl hashbrown::Equivalent<StorePath<String>> for StorePathRef<'_> {
98 fn equivalent(&self, key: &StorePath<String>) -> bool {
99 self.digest == key.digest && self.name == key.name
100 }
101}
102
103pub type StorePathRef<'a> = StorePath<&'a str>;
106
107impl<S> StorePath<S>
108where
109 S: AsRef<str>,
110{
111 pub fn digest(&self) -> &[u8; DIGEST_SIZE] {
112 &self.digest
113 }
114
115 pub fn name(&self) -> &S {
116 &self.name
117 }
118
119 pub fn as_ref(&self) -> StorePathRef<'_> {
120 StorePathRef {
121 digest: self.digest,
122 name: self.name.as_ref(),
123 }
124 }
125
126 pub fn to_owned(&self) -> StorePath<String> {
127 StorePath {
128 digest: self.digest,
129 name: self.name.as_ref().to_string(),
130 }
131 }
132
133 pub fn from_bytes<'a>(s: &'a [u8]) -> Result<Self, Error>
136 where
137 S: From<&'a str>,
138 {
139 if s.len() < ENCODED_DIGEST_SIZE + 2 {
145 Err(Error::InvalidLength)?
146 }
147
148 let digest = nixbase32::decode_fixed(&s[..ENCODED_DIGEST_SIZE])?;
149
150 if s[ENCODED_DIGEST_SIZE] != b'-' {
151 return Err(Error::MissingDash);
152 }
153
154 Ok(StorePath {
155 digest,
156 name: validate_name(&s[ENCODED_DIGEST_SIZE + 1..])?.into(),
157 })
158 }
159
160 pub fn from_name_and_digest<'a>(name: &'a str, digest: &[u8]) -> Result<Self, Error>
163 where
164 S: From<&'a str>,
165 {
166 let digest_fixed = digest.try_into().map_err(|_| Error::InvalidLength)?;
167 Self::from_name_and_digest_fixed(name, digest_fixed)
168 }
169
170 pub fn from_name_and_digest_fixed<'a>(
173 name: &'a str,
174 digest: [u8; DIGEST_SIZE],
175 ) -> Result<Self, Error>
176 where
177 S: From<&'a str>,
178 {
179 Ok(Self {
180 name: validate_name(name)?.into(),
181 digest,
182 })
183 }
184
185 pub fn from_absolute_path<'a>(s: &'a [u8]) -> Result<Self, Error>
189 where
190 S: From<&'a str>,
191 {
192 match s.strip_prefix(STORE_DIR_WITH_SLASH.as_bytes()) {
193 Some(s_stripped) => Self::from_bytes(s_stripped),
194 None => Err(Error::MissingStoreDir),
195 }
196 }
197
198 pub fn from_absolute_path_full<'p: 'sp, 'sp, P>(
201 path: &'p P,
202 ) -> Result<(Self, &'p std::path::Path), Error>
203 where
204 S: From<&'sp str>,
205 P: AsRef<std::path::Path> + 'p + ?Sized,
206 {
207 let p = path
209 .as_ref()
210 .strip_prefix(STORE_DIR_WITH_SLASH)
211 .map_err(|_| Error::MissingStoreDir)?;
212
213 let mut components = p.components();
214
215 use bstr::ByteSlice;
216 let first_component =
217 <[u8]>::from_os_str(components.next().ok_or(Error::InvalidLength)?.as_os_str())
218 .ok_or(Error::InvalidName)?;
219
220 if first_component.len() < 34 {
222 return Err(Error::InvalidLength);
223 }
224
225 let store_path = StorePath::from_bytes(first_component)?;
226
227 Ok((store_path, components.as_path()))
228 }
229
230 pub fn to_absolute_path(&self) -> String {
232 format!("{}", self.as_absolute_path_fmt())
233 }
234
235 pub fn as_absolute_path_fmt(&self) -> impl std::fmt::Display + '_ {
237 struct WithAbsolutePath<'a>(StorePathRef<'a>);
238
239 impl std::fmt::Display for WithAbsolutePath<'_> {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 write!(f, "{STORE_DIR_WITH_SLASH}{}", self.0)
242 }
243 }
244 WithAbsolutePath(self.as_ref())
245 }
246}
247
248impl<S> PartialOrd for StorePath<S>
249where
250 S: AsRef<str>,
251{
252 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
253 Some(self.cmp(other))
254 }
255}
256
257impl<S> Ord for StorePath<S>
260where
261 S: AsRef<str>,
262{
263 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
264 self.digest.iter().rev().cmp(other.digest.iter().rev())
265 }
266}
267
268impl FromStr for StorePath<String> {
269 type Err = Error;
270
271 fn from_str(s: &str) -> Result<Self, Self::Err> {
274 StorePath::<String>::from_bytes(s.as_bytes())
275 }
276}
277
278#[cfg(feature = "serde")]
279impl<'a, 'de: 'a, S> Deserialize<'de> for StorePath<S>
280where
281 S: AsRef<str> + From<&'a str>,
282{
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: serde::Deserializer<'de>,
286 {
287 let string: &'de str = Deserialize::deserialize(deserializer)?;
288 let stripped: Option<&str> = string.strip_prefix(STORE_DIR_WITH_SLASH);
289 let stripped: &str = stripped.ok_or_else(|| {
290 serde::de::Error::invalid_value(
291 serde::de::Unexpected::Str(string),
292 &"store path prefix",
293 )
294 })?;
295 StorePath::from_bytes(stripped.as_bytes()).map_err(|_| {
296 serde::de::Error::invalid_value(serde::de::Unexpected::Str(string), &"StorePath")
297 })
298 }
299}
300
301#[cfg(feature = "serde")]
302impl<S> Serialize for StorePath<S>
303where
304 S: AsRef<str>,
305{
306 fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error>
307 where
308 SR: serde::Serializer,
309 {
310 let string: String = self.to_absolute_path();
311 string.serialize(serializer)
312 }
313}
314
315static NAME_CHARS: [bool; 256] = {
317 let mut tbl = [false; 256];
318 let mut c = 0;
319
320 loop {
321 tbl[c as usize] = matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'-' | b'_' | b'?' | b'=' | b'.');
322
323 if c == u8::MAX {
324 break;
325 }
326
327 c += 1;
328 }
329
330 tbl
331};
332
333pub fn validate_name(s: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str, ValidateNameError> {
336 let s = s.as_ref();
337
338 if s.is_empty() || s.len() > 211 {
340 return Err(ValidateNameError::InvalidLength);
341 }
342
343 let mut valid = true;
344 for &c in s {
345 valid = valid && NAME_CHARS[c as usize];
346 }
347
348 if !valid {
349 for &c in s.iter() {
350 if !NAME_CHARS[c as usize] {
351 return Err(ValidateNameError::InvalidName);
352 }
353 }
354
355 unreachable!();
356 }
357
358 Ok(unsafe { str::from_utf8_unchecked(s) })
360}
361
362pub fn validate_name_as_os_str(
365 s: &(impl AsRef<std::ffi::OsStr> + ?Sized),
366) -> Result<&str, ValidateNameError> {
367 let s = s.as_ref().to_str().ok_or(ValidateNameError::InvalidName)?;
368
369 validate_name(s)
370}
371
372impl<S> fmt::Display for StorePath<S>
373where
374 S: AsRef<str>,
375{
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 write!(
381 f,
382 "{}-{}",
383 nixbase32::encode(&self.digest),
384 self.name.as_ref()
385 )
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::Error;
392
393 use crate::store_path::{DIGEST_SIZE, StorePath, StorePathRef};
394 use hex_literal::hex;
395 use pretty_assertions::assert_eq;
396 use rstest::rstest;
397 #[cfg(feature = "serde")]
398 use serde::Deserialize;
399
400 #[cfg(feature = "serde")]
403 #[derive(Deserialize)]
404 struct Container<'a> {
405 #[serde(borrow)]
406 store_path: StorePathRef<'a>,
407 }
408
409 #[test]
410 fn happy_path() {
411 let example_nix_path_str =
412 "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
413 let nixpath = StorePathRef::from_bytes(example_nix_path_str.as_bytes())
414 .expect("Error parsing example string");
415
416 let expected_digest: [u8; DIGEST_SIZE] = hex!("8a12321522fd91efbd60ebb2481af88580f61600");
417
418 assert_eq!("net-tools-1.60_p20170221182432", *nixpath.name());
419 assert_eq!(nixpath.digest, expected_digest);
420
421 assert_eq!(example_nix_path_str, nixpath.to_string())
422 }
423
424 #[test]
425 fn store_path_ordering() {
426 let store_paths = [
427 "/nix/store/0lk5dgi01r933abzfj9c9wlndg82yd3g-psutil-5.9.6.tar.gz.drv",
428 "/nix/store/1xj43bva89f9qmwm37zl7r3d7m67i9ck-shorttoc-1.3-tex.drv",
429 "/nix/store/2gb633czchi20jq1kqv70rx2yvvgins8-lifted-base-0.2.3.12.tar.gz.drv",
430 "/nix/store/2vksym3r3zqhp15q3fpvw2mnvffv11b9-docbook-xml-4.5.zip.drv",
431 "/nix/store/5q918awszjcz5720xvpc2czbg1sdqsf0-rust_renaming-0.1.0-lib",
432 "/nix/store/7jw30i342sr2p1fmz5xcfnch65h4zbd9-dbus-1.14.10.tar.xz.drv",
433 "/nix/store/96yqwqhnp3qya4rf4n0rcl0lwvrylp6k-eap8021x-222.40.1.tar.gz.drv",
434 "/nix/store/9gjqg36a1v0axyprbya1hkaylmnffixg-virtualenv-20.24.5.tar.gz.drv",
435 "/nix/store/a4i5mci2g9ada6ff7ks38g11dg6iqyb8-perl-5.32.1.drv",
436 "/nix/store/a5g76ljava4h5pxlggz3aqdhs3a4fk6p-ToolchainInfo.plist.drv",
437 "/nix/store/db46l7d6nswgz4ffp1mmd56vjf9g51v6-version.plist.drv",
438 "/nix/store/g6f7w20sd7vwy0rc1r4bfsw4ciclrm4q-crates-io-num_cpus-1.12.0.drv",
439 "/nix/store/iw82n1wwssb8g6772yddn8c3vafgv9np-bootstrap-stage1-sysctl-stdenv-darwin.drv",
440 "/nix/store/lp78d1y5wxpcn32d5c4r7xgbjwiw0cgf-logo.svg.drv",
441 "/nix/store/mf00ank13scv1f9l1zypqdpaawjhfr3s-python3.11-psutil-5.9.6.drv",
442 "/nix/store/mpfml61ra7pz90124jx9r3av0kvkz2w1-perl5.36.0-Encode-Locale-1.05",
443 "/nix/store/qhsvwx4h87skk7c4mx0xljgiy3z93i23-source.drv",
444 "/nix/store/riv7d73adim8hq7i04pr8kd0jnj93nav-fdk-aac-2.0.2.tar.gz.drv",
445 "/nix/store/s64b9031wga7vmpvgk16xwxjr0z9ln65-human-signals-5.0.0.tgz-extracted",
446 "/nix/store/w6svg3m2xdh6dhx0gl1nwa48g57d3hxh-thiserror-1.0.49",
447 ];
448
449 for w in store_paths.windows(2) {
450 if w.len() < 2 {
451 continue;
452 }
453
454 let pa = StorePathRef::from_absolute_path(w[0].as_bytes()).expect("parseable");
455 let pb = StorePathRef::from_absolute_path(w[1].as_bytes()).expect("parseable");
456
457 assert!(pa < pb, "{pa} not less than {pb}");
458 }
459 }
460
461 #[test]
470 fn starts_with_dot() {
471 StorePathRef::from_bytes(b"fli4bwscgna7lpm7v5xgnjxrxh0yc7ra-.gitignore")
472 .expect("must succeed");
473 }
474
475 #[test]
476 fn empty_name() {
477 StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-").expect_err("must fail");
478 }
479
480 #[test]
481 fn excessive_length() {
482 StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
483 .expect_err("must fail");
484 }
485
486 #[test]
487 fn invalid_hash_length() {
488 StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432")
489 .expect_err("must fail");
490 }
491
492 #[test]
493 fn invalid_encoding_hash() {
494 StorePathRef::from_bytes(
495 b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432",
496 )
497 .expect_err("must fail");
498 }
499
500 #[test]
501 fn more_than_just_the_bare_nix_store_path() {
502 StorePathRef::from_bytes(
503 b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432/bin/arp",
504 )
505 .expect_err("must fail");
506 }
507
508 #[test]
509 fn no_dash_between_hash_and_name() {
510 StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44lanet-tools-1.60_p20170221182432")
511 .expect_err("must fail");
512 }
513
514 #[test]
515 fn absolute_path() {
516 let example_nix_path_str =
517 "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
518 let nixpath_expected =
519 StorePathRef::from_bytes(example_nix_path_str.as_bytes()).expect("must parse");
520
521 let nixpath_actual = StorePathRef::from_absolute_path(
522 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432".as_bytes(),
523 )
524 .expect("must parse");
525
526 assert_eq!(nixpath_expected, nixpath_actual);
527
528 assert_eq!(
529 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
530 nixpath_actual.to_absolute_path(),
531 );
532 }
533
534 #[test]
535 fn absolute_path_missing_prefix() {
536 assert_eq!(
537 Error::MissingStoreDir,
538 StorePathRef::from_absolute_path(b"foobar-123").expect_err("must fail")
539 );
540 }
541
542 #[cfg(feature = "serde")]
543 #[test]
544 fn serialize_ref() {
545 let nixpath_actual = StorePathRef::from_bytes(
546 b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
547 )
548 .expect("can parse");
549
550 let serialized = serde_json::to_string(&nixpath_actual).expect("can serialize");
551
552 assert_eq!(
553 "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"",
554 &serialized
555 );
556 }
557
558 #[cfg(feature = "serde")]
559 #[test]
560 fn serialize_owned() {
561 let nixpath_actual = StorePathRef::from_bytes(
562 b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
563 )
564 .expect("can parse");
565
566 let serialized = serde_json::to_string(&nixpath_actual).expect("can serialize");
567
568 assert_eq!(
569 "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"",
570 &serialized
571 );
572 }
573
574 #[cfg(feature = "serde")]
575 #[test]
576 fn deserialize_ref() {
577 let store_path_str_json =
578 "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"";
579
580 let store_path: StorePathRef<'_> =
581 serde_json::from_str(store_path_str_json).expect("valid json");
582
583 assert_eq!(
584 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
585 store_path.to_absolute_path()
586 );
587 }
588
589 #[cfg(feature = "serde")]
590 #[test]
591 fn deserialize_ref_container() {
592 let str_json = "{\"store_path\":\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"}";
593
594 let container: Container<'_> = serde_json::from_str(str_json).expect("must deserialize");
595
596 assert_eq!(
597 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
598 container.store_path.to_absolute_path()
599 );
600 }
601
602 #[cfg(feature = "serde")]
603 #[test]
604 fn deserialize_owned() {
605 let store_path_str_json =
606 "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\"";
607
608 let store_path: StorePath<String> =
609 serde_json::from_str(store_path_str_json).expect("valid json");
610
611 assert_eq!(
612 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
613 store_path.to_absolute_path()
614 );
615 }
616
617 #[rstest]
618 #[case::without_prefix(
619 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
620 StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), "")]
621 #[case::without_prefix_but_trailing_slash(
622 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/",
623 StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), "")]
624 #[case::with_prefix(
625 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp",
626 StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), "bin/arp")]
627 #[case::with_prefix_and_trailing_slash(
628 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp/",
629 StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), "bin/arp/")]
630 fn from_absolute_path_full(
631 #[case] s: &str,
632 #[case] exp_store_path: StorePath<&str>,
633 #[case] exp_rest_str: &str,
634 ) {
635 let (actual_store_path, actual_rest) =
636 StorePath::from_absolute_path_full(s).expect("must succeed");
637
638 assert_eq!(exp_store_path, actual_store_path);
639 assert_eq!(exp_rest_str, actual_rest);
640 }
641
642 #[test]
643 fn from_absolute_path_errors() {
644 assert_eq!(
645 Error::InvalidLength,
646 StorePathRef::from_absolute_path_full("/nix/store/").expect_err("must fail")
647 );
648 assert_eq!(
649 Error::InvalidLength,
650 StorePathRef::from_absolute_path_full("/nix/store/foo").expect_err("must fail")
651 );
652 assert_eq!(
653 Error::MissingStoreDir,
654 StorePathRef::from_absolute_path_full(
655 "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
656 )
657 .expect_err("must fail")
658 );
659 }
660}