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