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
19pub const STORE_DIR: &str = "/nix/store";
22pub const STORE_DIR_WITH_SLASH: &str = "/nix/store/";
23
24#[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#[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
81pub 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 pub fn from_bytes<'a>(s: &'a [u8]) -> Result<Self, Error>
114 where
115 S: From<&'a str>,
116 {
117 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 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 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 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 #[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 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 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 let rest_buf = it.as_path();
198
199 Ok((store_path, rest_buf))
200 }
201
202 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
219impl<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 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
277static 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
295pub(crate) fn validate_name(s: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str, Error> {
298 let s = s.as_ref();
299
300 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 Ok(unsafe { str::from_utf8_unchecked(s) })
322}
323
324impl<S> fmt::Display for StorePath<S>
325where
326 S: AsRef<str>,
327{
328 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 #[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 #[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}