1use bitflags::bitflags;
21use data_encoding::HEXLOWER;
22use std::{
23 fmt::{self, Display},
24 mem,
25};
26
27use crate::{
28 nixbase32,
29 nixhash::CAHash,
30 store_path::{ParseStorePathError, StorePathRef},
31};
32
33mod fingerprint;
34mod signature;
35mod signing_keys;
36mod verifying_keys;
37
38pub use fingerprint::fingerprint;
39pub use signature::{Error as SignatureError, Signature, SignatureRef};
40pub use signing_keys::parse_keypair;
41pub use signing_keys::{Error as SigningKeyError, SigningKey};
42pub use verifying_keys::{Error as VerifyingKeyError, VerifyingKey};
43
44#[derive(Debug)]
45pub struct NarInfo<'a> {
46 pub flags: Flags,
47 pub store_path: StorePathRef<'a>,
50 pub nar_hash: [u8; 32],
52 pub nar_size: u64,
54 pub references: Vec<StorePathRef<'a>>,
56 pub signatures: Vec<SignatureRef<'a>>,
59 pub ca: Option<CAHash>,
61 pub system: Option<&'a str>,
64 pub deriver: Option<StorePathRef<'a>>,
66 pub url: &'a str,
69 pub compression: Option<&'a str>,
76 pub file_hash: Option<[u8; 32]>,
78 pub file_size: Option<u64>,
80}
81
82bitflags! {
83 #[derive(Debug, Copy, Clone)]
84 pub struct Flags: u8 {
85 const UNKNOWN_FIELD = 1 << 0;
87 const COMPRESSION_DEFAULT = 1 << 1;
88 const REFERENCES_OUT_OF_ORDER = 1 << 2;
90 const NAR_HASH_HEX = 1 << 3;
91
92 const EXPLICIT_UNKNOWN_DERIVER = 1 << 4;
97
98 const REFERENCES_MISSING = 1 << 5;
100
101 const FILE_HASH_HEX = 1 << 6;
103 }
104}
105
106const TAG_STOREPATH: &str = "StorePath";
107const TAG_URL: &str = "URL";
108const TAG_COMPRESSION: &str = "Compression";
109const TAG_FILEHASH: &str = "FileHash";
110const TAG_FILESIZE: &str = "FileSize";
111const TAG_NARHASH: &str = "NarHash";
112const TAG_NARSIZE: &str = "NarSize";
113const TAG_REFERENCES: &str = "References";
114const TAG_SYSTEM: &str = "System";
115const TAG_DERIVER: &str = "Deriver";
116const TAG_SIG: &str = "Sig";
117const TAG_CA: &str = "CA";
118
119impl<'a> NarInfo<'a> {
120 pub fn parse(input: &'a str) -> Result<Self, Error> {
121 let mut flags = Flags::empty();
122 let mut store_path = None;
123 let mut url = None;
124 let mut compression = None;
125 let mut file_hash = None;
126 let mut file_size = None;
127 let mut nar_hash = None;
128 let mut nar_size = None;
129 let mut references = None;
130 let mut system = None;
131 let mut deriver = None;
132 let mut signatures = vec![];
133 let mut ca = None;
134
135 for line in input.lines() {
136 let (tag, val) = line
137 .split_once(':')
138 .ok_or_else(|| Error::InvalidLine(line.to_string()))?;
139
140 let val = val
141 .strip_prefix(' ')
142 .ok_or_else(|| Error::InvalidLine(line.to_string()))?;
143
144 match tag {
145 TAG_STOREPATH => {
146 let val = val
147 .strip_prefix("/nix/store/")
148 .ok_or(Error::InvalidStorePath(
149 ParseStorePathError::MissingStoreDir,
150 ))?;
151 let val = StorePathRef::from_bytes(val.as_bytes())
152 .map_err(Error::InvalidStorePath)?;
153
154 if store_path.replace(val).is_some() {
155 return Err(Error::DuplicateField(TAG_STOREPATH));
156 }
157 }
158 TAG_URL => {
159 if val.is_empty() {
160 return Err(Error::EmptyField(TAG_URL));
161 }
162
163 if url.replace(val).is_some() {
164 return Err(Error::DuplicateField(TAG_URL));
165 }
166 }
167 TAG_COMPRESSION => {
168 if val.is_empty() {
169 return Err(Error::EmptyField(TAG_COMPRESSION));
170 }
171
172 if compression.replace(val).is_some() {
173 return Err(Error::DuplicateField(TAG_COMPRESSION));
174 }
175 }
176 TAG_FILEHASH => {
177 let val = val
178 .strip_prefix("sha256:")
179 .ok_or(Error::MissingPrefixForHash(TAG_FILEHASH))?;
180
181 let val = if val.len() != HEXLOWER.encode_len(32) {
182 nixbase32::decode_fixed::<32>(val)
183 } else {
184 flags |= Flags::FILE_HASH_HEX;
185
186 let val = val.as_bytes();
187 let mut buf = [0u8; 32];
188
189 HEXLOWER
190 .decode_mut(val, &mut buf)
191 .map_err(|e| e.error)
192 .map(|_| buf)
193 };
194
195 let val = val.map_err(|e| Error::UnableToDecodeHash(TAG_FILEHASH, e))?;
196
197 if file_hash.replace(val).is_some() {
198 return Err(Error::DuplicateField(TAG_FILEHASH));
199 }
200 }
201 TAG_FILESIZE => {
202 let val = val
203 .parse::<u64>()
204 .map_err(|_| Error::UnableToParseSize(TAG_FILESIZE, val.to_string()))?;
205
206 if file_size.replace(val).is_some() {
207 return Err(Error::DuplicateField(TAG_FILESIZE));
208 }
209 }
210 TAG_NARHASH => {
211 let val = val
212 .strip_prefix("sha256:")
213 .ok_or(Error::MissingPrefixForHash(TAG_NARHASH))?;
214
215 let val = if val.len() != HEXLOWER.encode_len(32) {
216 nixbase32::decode_fixed::<32>(val)
217 } else {
218 flags |= Flags::NAR_HASH_HEX;
219
220 let val = val.as_bytes();
221 let mut buf = [0u8; 32];
222
223 HEXLOWER
224 .decode_mut(val, &mut buf)
225 .map_err(|e| e.error)
226 .map(|_| buf)
227 };
228
229 let val = val.map_err(|e| Error::UnableToDecodeHash(TAG_NARHASH, e))?;
230
231 if nar_hash.replace(val).is_some() {
232 return Err(Error::DuplicateField(TAG_NARHASH));
233 }
234 }
235 TAG_NARSIZE => {
236 let val = val
237 .parse::<u64>()
238 .map_err(|_| Error::UnableToParseSize(TAG_NARSIZE, val.to_string()))?;
239
240 if nar_size.replace(val).is_some() {
241 return Err(Error::DuplicateField(TAG_NARSIZE));
242 }
243 }
244 TAG_REFERENCES => {
245 let val: Vec<StorePathRef> = if !val.is_empty() {
246 let mut prev = "";
247 val.split(' ')
248 .enumerate()
249 .map(|(i, s)| {
250 if mem::replace(&mut prev, s) >= s {
252 flags |= Flags::REFERENCES_OUT_OF_ORDER;
253 }
254
255 StorePathRef::from_bytes(s.as_bytes())
256 .map_err(|err| Error::InvalidReference(i, err))
257 })
258 .collect::<Result<_, _>>()?
259 } else {
260 vec![]
261 };
262
263 if references.replace(val).is_some() {
264 return Err(Error::DuplicateField(TAG_REFERENCES));
265 }
266 }
267 TAG_SYSTEM => {
268 if val.is_empty() {
269 return Err(Error::EmptyField(TAG_SYSTEM));
270 }
271
272 if system.replace(val).is_some() {
273 return Err(Error::DuplicateField(TAG_SYSTEM));
274 }
275 }
276 TAG_DERIVER => {
277 match val.strip_suffix(".drv") {
278 Some(val) => {
279 let val = StorePathRef::from_bytes(val.as_bytes())
280 .map_err(Error::InvalidDeriverStorePath)?;
281
282 if deriver.replace(val).is_some() {
283 return Err(Error::DuplicateField(TAG_DERIVER));
284 }
285 }
286 None => {
287 if val == "unknown-deriver" {
288 flags |= Flags::EXPLICIT_UNKNOWN_DERIVER;
289 } else {
290 return Err(Error::InvalidDeriverStorePathMissingSuffix);
291 }
292 }
293 };
294 }
295 TAG_SIG => {
296 let val = SignatureRef::parse(val)
297 .map_err(|e| Error::UnableToParseSignature(signatures.len(), e))?;
298
299 signatures.push(val);
300 }
301 TAG_CA => {
302 let val = CAHash::from_nix_hex_str(val)
303 .ok_or_else(|| Error::UnableToParseCA(val.to_string()))?;
304
305 if ca.replace(val).is_some() {
306 return Err(Error::DuplicateField(TAG_CA));
307 }
308 }
309 _ => {
310 flags |= Flags::UNKNOWN_FIELD;
311 }
312 }
313 }
314
315 Ok(NarInfo {
316 store_path: store_path.ok_or(Error::MissingField("StorePath"))?,
317 nar_hash: nar_hash.ok_or(Error::MissingField("NarHash"))?,
318 nar_size: nar_size.ok_or(Error::MissingField("NarSize"))?,
319 references: match references {
320 Some(val) => val,
321 None => {
322 flags |= Flags::REFERENCES_MISSING;
323 vec![]
324 }
325 },
326 signatures,
327 ca,
328 system,
329 deriver,
330 url: url.ok_or(Error::MissingField("URL"))?,
331 compression: match compression {
332 Some("none") => None,
333 None => {
334 flags |= Flags::COMPRESSION_DEFAULT;
335 Some("bzip2")
336 }
337 _ => compression,
338 },
339 file_hash,
340 file_size,
341 flags,
342 })
343 }
344
345 pub fn fingerprint(&self) -> String {
348 fingerprint(
349 &self.store_path,
350 &self.nar_hash,
351 self.nar_size,
352 self.references.iter(),
353 )
354 }
355
356 pub fn add_signature<S>(&mut self, signer: &'a SigningKey<S>)
360 where
361 S: ed25519::signature::Signer<ed25519::Signature>,
362 {
363 let fp = self.fingerprint();
365
366 let sig = signer.sign(fp.as_bytes());
367
368 self.signatures.push(sig);
369 }
370}
371
372impl Display for NarInfo<'_> {
373 fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
374 writeln!(w, "StorePath: /nix/store/{}", self.store_path)?;
375 writeln!(w, "URL: {}", self.url)?;
376
377 if !self.flags.contains(Flags::COMPRESSION_DEFAULT) {
378 let compression = self.compression.unwrap_or("none");
379 writeln!(w, "Compression: {compression}")?;
380 };
381
382 if let Some(file_hash) = self.file_hash {
383 if !self.flags.contains(Flags::FILE_HASH_HEX) {
384 writeln!(w, "FileHash: sha256:{}", nixbase32::encode(&file_hash))?;
385 } else {
386 writeln!(w, "FileHash: sha256:{}", HEXLOWER.encode(&file_hash))?;
387 }
388 }
389
390 if let Some(file_size) = self.file_size {
391 writeln!(w, "FileSize: {file_size}")?;
392 }
393
394 if !self.flags.contains(Flags::NAR_HASH_HEX) {
395 writeln!(w, "NarHash: sha256:{}", nixbase32::encode(&self.nar_hash))?;
396 } else {
397 writeln!(w, "NarHash: sha256:{}", HEXLOWER.encode(&self.nar_hash))?;
398 }
399 writeln!(w, "NarSize: {}", self.nar_size)?;
400
401 if !self.flags.contains(Flags::REFERENCES_MISSING) {
402 write!(w, "References:")?;
403 if self.references.is_empty() {
404 write!(w, " ")?;
405 } else {
406 for path in &self.references {
407 write!(w, " {path}")?;
408 }
409 }
410 writeln!(w)?;
411 }
412
413 if let Some(deriver) = &self.deriver {
414 writeln!(w, "Deriver: {deriver}.drv")?;
415 } else if self.flags.contains(Flags::EXPLICIT_UNKNOWN_DERIVER) {
416 writeln!(w, "Deriver: unknown-deriver")?;
417 }
418
419 if let Some(system) = self.system {
420 writeln!(w, "System: {system}")?;
421 }
422
423 for sig in &self.signatures {
424 writeln!(w, "Sig: {sig}")?;
425 }
426
427 if let Some(ca) = &self.ca {
428 writeln!(w, "CA: {}", ca)?;
429 }
430
431 Ok(())
432 }
433}
434
435#[derive(thiserror::Error, Debug)]
436pub enum Error {
437 #[error("duplicate field: {0}")]
438 DuplicateField(&'static str),
439
440 #[error("missing field: {0}")]
441 MissingField(&'static str),
442
443 #[error("invalid line: {0}")]
444 InvalidLine(String),
445
446 #[error("invalid StorePath: {0}")]
447 InvalidStorePath(ParseStorePathError),
448
449 #[error("field {0} may not be empty string")]
450 EmptyField(&'static str),
451
452 #[error("invalid {0}: {1}")]
453 UnableToParseSize(&'static str, String),
454
455 #[error("unable to parse #{0} reference: {1}")]
456 InvalidReference(usize, ParseStorePathError),
457
458 #[error("invalid Deriver store path: {0}")]
459 InvalidDeriverStorePath(ParseStorePathError),
460
461 #[error("invalid Deriver store path, must end with .drv")]
462 InvalidDeriverStorePathMissingSuffix,
463
464 #[error("missing prefix for {0}")]
465 MissingPrefixForHash(&'static str),
466
467 #[error("unable to decode {0}: {1}")]
468 UnableToDecodeHash(&'static str, data_encoding::DecodeError),
469
470 #[error("unable to parse signature #{0}: {1}")]
471 UnableToParseSignature(usize, SignatureError),
472
473 #[error("unable to parse CA field: {0}")]
474 UnableToParseCA(String),
475}
476
477#[cfg(test)]
478const DUMMY_KEYPAIR: &str = "cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JEw==";
479#[cfg(test)]
480const DUMMY_VERIFYING_KEY: &str =
481 "cache.example.com-1:yKUSiqP9yaMSduDmGtw8U9iVVd/Coyv9csB1rjHtiRM=";
482
483#[cfg(test)]
484mod test {
485 use hex_literal::hex;
486 use pretty_assertions::assert_eq;
487 use std::sync::LazyLock;
488 use std::{io, str};
489
490 use crate::{
491 nixhash::{CAHash, NixHash},
492 store_path::StorePathRef,
493 };
494
495 use super::{Flags, NarInfo};
496
497 static CASES: LazyLock<&'static [&'static str]> = LazyLock::new(|| {
498 let data = zstd::decode_all(io::Cursor::new(include_bytes!(
499 "../../testdata/narinfo.zst"
500 )))
501 .unwrap();
502 let data = str::from_utf8(Vec::leak(data)).unwrap();
503 Vec::leak(
504 data.split_inclusive("\n\n")
505 .map(|s| s.strip_suffix('\n').unwrap())
506 .collect::<Vec<_>>(),
507 )
508 });
509
510 #[test]
511 fn roundtrip() {
512 for &input in *CASES {
513 let parsed = NarInfo::parse(input).expect("should parse");
514 let output = format!("{parsed}");
515 assert_eq!(input, output, "should roundtrip");
516 }
517 }
518
519 #[test]
520 fn references_out_of_order() {
521 let input = r#"StorePath: /nix/store/xi429w4ddvb1r77978hm7jfb2jsn559r-gcc-3.4.6
522URL: nar/1hr09cgkyw1hcsfkv5qp5jlpmf2mqrkrqs3xj5zklq9c1h9544ff.nar.bz2
523Compression: bzip2
524FileHash: sha256:1hr09cgkyw1hcsfkv5qp5jlpmf2mqrkrqs3xj5zklq9c1h9544ff
525FileSize: 4006
526NarHash: sha256:0ik9mpqxpd9hv325hdblj2nawqj5w7951qdyy8ikxgwr6fq7m11c
527NarSize: 21264
528References: a8922c0h87iilxzzvwn2hmv8x210aqb9-glibc-2.7 7w2acjgalb0cm7b3bg8yswza4l7iil9y-binutils-2.18 mm631h09mj964hm9q04l5fd8vw12j1mm-bash-3.2-p39 nx2zs2qd6snfcpzw4a0jnh26z9m0yihz-gcc-3.4.6 xi429w4ddvb1r77978hm7jfb2jsn559r-gcc-3.4.6
529Deriver: 2dzpn70c1hawczwhg9aavqk18zp9zsva-gcc-3.4.6.drv
530Sig: cache.nixos.org-1:o1DTsjCz0PofLJ216P2RBuSulI8BAb6zHxWE4N+tzlcELk5Uk/GO2SCxWTRN5wJutLZZ+cHTMdWqOHF88KGQDg==
531"#;
532 let parsed = NarInfo::parse(input).expect("should parse");
533
534 assert!(parsed.flags.contains(Flags::REFERENCES_OUT_OF_ORDER));
535 assert_eq!(
536 vec![
537 "a8922c0h87iilxzzvwn2hmv8x210aqb9-glibc-2.7",
538 "7w2acjgalb0cm7b3bg8yswza4l7iil9y-binutils-2.18",
539 "mm631h09mj964hm9q04l5fd8vw12j1mm-bash-3.2-p39",
540 "nx2zs2qd6snfcpzw4a0jnh26z9m0yihz-gcc-3.4.6",
541 "xi429w4ddvb1r77978hm7jfb2jsn559r-gcc-3.4.6"
542 ],
543 parsed
544 .references
545 .iter()
546 .map(StorePathRef::to_string)
547 .collect::<Vec<_>>(),
548 );
549 assert_eq!(input, parsed.to_string(), "should roundtrip");
550 }
551
552 #[test]
553 fn ca_nar_hash_sha1() {
554 let input = {
556 let input = r#"StorePath: /nix/store/k20pahypzvr49fy82cw5sx72hdfg3qcr-texlive-hyphenex-37354
557URL: nar/0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi.nar.xz
558Compression: xz
559FileHash: sha256:0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi
560FileSize: 7120
561NarHash: sha256:0h1bm4sj1cnfkxgyhvgi8df1qavnnv94sd0v09wcrm971602shfg
562NarSize: 22552
563References:!!WSP!!
564Sig: cache.nixos.org-1:u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw==
565CA: fixed:r:sha1:1ak1ymbmsfx7z8kh09jzkr3a4dvkrfjw
566"#;
567 input.replace("!!WSP!!", " ")
568 };
569 let parsed = NarInfo::parse(&input).expect("should parse");
570
571 assert_eq!(
572 parsed.ca,
573 Some(CAHash::Nar(NixHash::Sha1(hex!(
574 "5cba3c77236ae4f9650270a27fbad375551fa60a"
575 ))))
576 );
577 }
578
579 #[test]
580 fn compression_default() {
581 let input = r#"StorePath: /nix/store/a1jjalr4csx9hcga7fnm122aqabrjnch-digikam-2.6.0
584URL: nar/1fzimfnvq2k8b40n4g54abmncpx2ddckh6qlb77pgq6xiysyil69.nar.bz2
585FileHash: sha256:1fzimfnvq2k8b40n4g54abmncpx2ddckh6qlb77pgq6xiysyil69
586FileSize: 43503778
587NarHash: sha256:0zpbbwipqzr5p8mlpag9wrsp5hlaxkq7gax5jj0hg3vvdziypcw5
588NarSize: 100658640
589References: 0izkyk7bq2ag9393nvnhgm87p75cq09w-liblqr-1-0.4.1 1cslpgyb7vb30inj3210jv6agqv42jxz-qca-2.0.3 1sya3bwjxkzpkmwn67gfzp4gz4g62l36-libXrandr-1.3.1 26yxdaa9z0ma5sgw02i670rsqnl57crs-glib-2.30.3 27lnjh99236kmhbpc5747599zcymfzmg-qt-4.8.2 2v6x378vcfvyxilkvihs60zha54z2x2y-qjson-0.7.1 45hgr3fbnr45n795hn2x7hsymp0h2j2m-libjpeg-8c 4kw1b212s80ap2iyibxrimcqb5imhfj7-libkexiv2-4.7.4 7dvylm5crlc0sfafcc0n46mb5ch67q0j-glibc-2.13 a05cbh1awjbl1rbyb2ynyf4k42v5a9a7-boost-1.47.0 a1jjalr4csx9hcga7fnm122aqabrjnch-digikam-2.6.0 aav5ffg8wlnilgnvdb2jnrv2aam4zmmz-perl-5.14.2 ab0m9h30nsr13w48qriv0k350kmwx567-kdelibs-4.7.4 avffkd49cqvpwdkzry8bn69dkbw4cy29-lensfun-0.2.5 cy8rl8h4yp2j3h8987vkklg328q3wmjz-gcc-4.6.3 dmmh5ihyg1r2dm4azgsfj2kprj92czlg-libSM-1.2.0 fl56j5n4shfw9c0r6vs2i4f1h9zx5kac-soprano-2.7.6 g15cmvh15ggdjcwapskngv20q4yhix40-jasper-1.900.1 i04maxd0din6v92rnqcwl9yra0kl2vk5-marble-4.7.4 kqjjb3m26rdddwwwkk8v45821aps877k-libICE-1.0.7 lxz9r135wkndvi642z4bjgmvyypsgirb-libtiff-3.9.4 m9c8i0a6cl30lcqp654dqkbag3wjmd00-libX11-1.4.1 mpnj4k2ijrgyfkh48fg96nzcmklfh5pl-coreutils-8.15 nppljblap477s0893c151lyq7r7n5v1q-zlib-1.2.7 nw9mdbyp8kyn3v4vkdzq0gsnqbc4mnx3-expat-2.0.1 p1a0dn931mzdkvj6h5yzshbmgxba5r0z-libgphoto2-2.4.11 pvjj07xa1cfkad3gwk376nzdrgknbcqm-mesa-7.11.2 pzcxag98jqccp9ycbxknyh0w95pgnsk4-lcms-1.19 qfi5pgds33kg6vlnxsmj0hyl74vcmyiz-libpng-1.5.10 scm6bj86s3qh3s3x0b9ayjp6755p4q86-mysql-5.1.54 sd23qspcyg385va0lr35xgz3hvlqphg6-libkipi-4.7.4 svmbrhc6kzfzakv20a7zrfl6kbr5mfpq-kdepimlibs-4.7.4 v7kh3h7xfwjz4hgffg3gwrfzjff9bw9d-bash-4.2-p24 vi17f22064djgpk0w248da348q8gxkww-libkdcraw-4.7.4 wkjdzmj3z4dcbsc9f833zs6krdgg2krk-phonon-4.6.0 xf3i3awqi0035ixy2qyb6hk4c92r3vrn-opencv-2.4.2 y1vr0nz8i59x59501020nh2k1dw3bhwq-libusb-0.1.12 yf3hin2hb6i08n7zrk8g3acy54rhg9bp-libXext-1.2.0
590Deriver: la77dr44phk5m5jnl4dvk01cwpykyw9s-digikam-2.6.0.drv
591System: i686-linux
592Sig: cache.nixos.org-1:92fl0i5q7EyegCj5Yf4L0bENkWuVAtgveiRcTEEUH0P6HvCE1xFcPbz/0Pf6Np+K1LPzHK+s5RHOmVoxRsvsDg==
593"#;
594 let parsed = NarInfo::parse(input).expect("should parse");
595
596 assert!(parsed.flags.contains(Flags::COMPRESSION_DEFAULT));
597 assert_eq!(parsed.compression, Some("bzip2"));
598 assert_eq!(parsed.to_string(), input, "should roundtrip");
599 }
600
601 #[test]
602 fn compression_none() {
603 let input = r#"StorePath: /nix/store/a1jjalr4csx9hcga7fnm122aqabrjnch-digikam-2.6.0
606URL: nar/1fzimfnvq2k8b40n4g54abmncpx2ddckh6qlb77pgq6xiysyil69.nar.bz2
607Compression: none
608FileHash: sha256:0zpbbwipqzr5p8mlpag9wrsp5hlaxkq7gax5jj0hg3vvdziypcw5
609FileSize: 100658640
610NarHash: sha256:0zpbbwipqzr5p8mlpag9wrsp5hlaxkq7gax5jj0hg3vvdziypcw5
611NarSize: 100658640
612References: 0izkyk7bq2ag9393nvnhgm87p75cq09w-liblqr-1-0.4.1 1cslpgyb7vb30inj3210jv6agqv42jxz-qca-2.0.3 1sya3bwjxkzpkmwn67gfzp4gz4g62l36-libXrandr-1.3.1 26yxdaa9z0ma5sgw02i670rsqnl57crs-glib-2.30.3 27lnjh99236kmhbpc5747599zcymfzmg-qt-4.8.2 2v6x378vcfvyxilkvihs60zha54z2x2y-qjson-0.7.1 45hgr3fbnr45n795hn2x7hsymp0h2j2m-libjpeg-8c 4kw1b212s80ap2iyibxrimcqb5imhfj7-libkexiv2-4.7.4 7dvylm5crlc0sfafcc0n46mb5ch67q0j-glibc-2.13 a05cbh1awjbl1rbyb2ynyf4k42v5a9a7-boost-1.47.0 a1jjalr4csx9hcga7fnm122aqabrjnch-digikam-2.6.0 aav5ffg8wlnilgnvdb2jnrv2aam4zmmz-perl-5.14.2 ab0m9h30nsr13w48qriv0k350kmwx567-kdelibs-4.7.4 avffkd49cqvpwdkzry8bn69dkbw4cy29-lensfun-0.2.5 cy8rl8h4yp2j3h8987vkklg328q3wmjz-gcc-4.6.3 dmmh5ihyg1r2dm4azgsfj2kprj92czlg-libSM-1.2.0 fl56j5n4shfw9c0r6vs2i4f1h9zx5kac-soprano-2.7.6 g15cmvh15ggdjcwapskngv20q4yhix40-jasper-1.900.1 i04maxd0din6v92rnqcwl9yra0kl2vk5-marble-4.7.4 kqjjb3m26rdddwwwkk8v45821aps877k-libICE-1.0.7 lxz9r135wkndvi642z4bjgmvyypsgirb-libtiff-3.9.4 m9c8i0a6cl30lcqp654dqkbag3wjmd00-libX11-1.4.1 mpnj4k2ijrgyfkh48fg96nzcmklfh5pl-coreutils-8.15 nppljblap477s0893c151lyq7r7n5v1q-zlib-1.2.7 nw9mdbyp8kyn3v4vkdzq0gsnqbc4mnx3-expat-2.0.1 p1a0dn931mzdkvj6h5yzshbmgxba5r0z-libgphoto2-2.4.11 pvjj07xa1cfkad3gwk376nzdrgknbcqm-mesa-7.11.2 pzcxag98jqccp9ycbxknyh0w95pgnsk4-lcms-1.19 qfi5pgds33kg6vlnxsmj0hyl74vcmyiz-libpng-1.5.10 scm6bj86s3qh3s3x0b9ayjp6755p4q86-mysql-5.1.54 sd23qspcyg385va0lr35xgz3hvlqphg6-libkipi-4.7.4 svmbrhc6kzfzakv20a7zrfl6kbr5mfpq-kdepimlibs-4.7.4 v7kh3h7xfwjz4hgffg3gwrfzjff9bw9d-bash-4.2-p24 vi17f22064djgpk0w248da348q8gxkww-libkdcraw-4.7.4 wkjdzmj3z4dcbsc9f833zs6krdgg2krk-phonon-4.6.0 xf3i3awqi0035ixy2qyb6hk4c92r3vrn-opencv-2.4.2 y1vr0nz8i59x59501020nh2k1dw3bhwq-libusb-0.1.12 yf3hin2hb6i08n7zrk8g3acy54rhg9bp-libXext-1.2.0
613Deriver: la77dr44phk5m5jnl4dvk01cwpykyw9s-digikam-2.6.0.drv
614System: i686-linux
615Sig: cache.nixos.org-1:92fl0i5q7EyegCj5Yf4L0bENkWuVAtgveiRcTEEUH0P6HvCE1xFcPbz/0Pf6Np+K1LPzHK+s5RHOmVoxRsvsDg==
616"#;
617 let parsed = NarInfo::parse(input).expect("should parse");
618
619 assert!(!parsed.flags.contains(Flags::COMPRESSION_DEFAULT));
620 assert_eq!(parsed.compression, None);
621 assert_eq!(parsed.to_string(), input);
622 }
623
624 #[test]
625 fn explicit_unknown_deriver() {
626 let input = r#"StorePath: /nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432
629URL: nar/1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar.xz
630Compression: xz
631FileHash: sha256:1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d
632FileSize: 114980
633NarHash: sha256:0lxjvvpr59c2mdram7ympy5ay741f180kv3349hvfc3f8nrmbqf6
634NarSize: 464152
635References: 7gx4kiv5m0i7d7qkixq2cwzbr10lvxwc-glibc-2.27
636Deriver: unknown-deriver
637Sig: cache.nixos.org-1:sn5s/RrqEI+YG6/PjwdbPjcAC7rcta7sJU4mFOawGvJBLsWkyLtBrT2EuFt/LJjWkTZ+ZWOI9NTtjo/woMdvAg==
638Sig: hydra.other.net-1:JXQ3Z/PXf0EZSFkFioa4FbyYpbbTbHlFBtZf4VqU0tuMTWzhMD7p9Q7acJjLn3jofOtilAAwRILKIfVuyrbjAA==
639"#;
640 let parsed = NarInfo::parse(input).expect("should parse");
641
642 assert!(parsed.flags.contains(Flags::EXPLICIT_UNKNOWN_DERIVER));
643 assert!(parsed.deriver.is_none());
644 assert_eq!(parsed.to_string(), input, "should roundtrip");
645 }
646
647 #[test]
648 fn nar_hash_hex() {
649 let input = r#"StorePath: /nix/store/0vpqfxbkx0ffrnhbws6g9qwhmliksz7f-perl-HTTP-Cookies-6.01
650URL: nar/1rv1m9inydm1r4krw8hmwg1hs86d0nxddd1pbhihx7l7fycjvfk3.nar.xz
651Compression: xz
652FileHash: sha256:1rv1m9inydm1r4krw8hmwg1hs86d0nxddd1pbhihx7l7fycjvfk3
653FileSize: 19912
654NarHash: sha256:60adfd293a4d81ad7cd7e47263cbb3fc846309ef91b154a08ba672b558f94ff3
655NarSize: 45840
656References: 0vpqfxbkx0ffrnhbws6g9qwhmliksz7f-perl-HTTP-Cookies-6.01 9vrhbib2lxd9pjlg6fnl5b82gblidrcr-perl-HTTP-Message-6.06 wy20zslqxzxxfpzzk0rajh41d7a6mlnf-perl-HTTP-Date-6.02
657Deriver: fb4ihlq3psnsjq95mvvs49rwpplpc8zj-perl-HTTP-Cookies-6.01.drv
658Sig: cache.nixos.org-1:HhaiY36Uk3XV1JGe9d9xHnzAapqJXprU1YZZzSzxE97jCuO5RR7vlG2kF7MSC5thwRyxAtdghdSz3AqFi+QSCw==
659"#;
660 let parsed = NarInfo::parse(input).expect("should parse");
661
662 assert!(parsed.flags.contains(Flags::NAR_HASH_HEX));
663 assert_eq!(
664 hex!("60adfd293a4d81ad7cd7e47263cbb3fc846309ef91b154a08ba672b558f94ff3"),
665 parsed.nar_hash,
666 );
667 assert_eq!(parsed.to_string(), input, "should roundtrip");
668 }
669
670 #[test]
671 fn references_missing() {
672 let input = r#"StorePath: /nix/store/64s9zav4fk5qiba1jq0ipvyhnn57r7dq-cfg-if-1.0.0
675URL: nar/0lxxfhy5fmfz0sbnqkqjdf7gx9gsxrfzz49n19y8sr93inawhshh.nar?hash=64s9zav4fk5qiba1jq0ipvyhnn57r7dq
676Compression: none
677FileHash: sha256:0lxxfhy5fmfz0sbnqkqjdf7gx9gsxrfzz49n19y8sr93inawhshh
678FileSize: 24944
679NarHash: sha256:0lxxfhy5fmfz0sbnqkqjdf7gx9gsxrfzz49n19y8sr93inawhshh
680NarSize: 24944
681Deriver: s409kxiz6bx2g0da01gzvlnnjpl3i4h9-cfg-if-1.0.0.drv
682Sig: cache.nixos.org-1:WDvKIdxSnQ8p2w9SD0ffdibUSNMz6QQN6jpe+A8LLNHmZFsX+m8GZF0x9DN6PWV6k+OlnBT5UVbiWQYgXIsQAQ==
683"#;
684 let parsed = NarInfo::parse(input).expect("should parse");
685
686 assert!(parsed.flags.contains(Flags::REFERENCES_MISSING));
687 assert_eq!(parsed.references, vec![]);
688 assert_eq!(parsed.to_string(), input, "should roundtrip");
689 }
690
691 #[test]
692 fn file_hash_hex() {
693 let input = {
694 let input = r#"StorePath: /nix/store/qwiryv0pb8sifik5npsgn9hdngh21r62-scalals-native-0.1.10
695URL: nar/4a599968-09b5-4641-8b5a-1de6fb5e4b34.nar.zst
696Compression: zstd
697FileHash: sha256:89adb48876927a7d0f228749f10009a37787af99477470580e8dc225466247fc
698FileSize: 2182839
699NarHash: sha256:1n5chw2qf1vjc0r1n91jpm45p8ir18v9arj7n81b6vllmc0wzsq8
700NarSize: 8415576
701References:!!WSP!!
702Deriver: 2drfc3vjhjk1zkq40m99ba8abd261xwd-scalals-native-0.1.10.drv
703Sig: cbley.cachix.org-1:xPaA+lJEgJkCOAhDOET3PdytqL8jJjBcnNdG6SRasOYd6DPNBOnrdr30vS9dtjqNSRtYeWoN5acqmC6AuPlaAQ==
704"#;
705 input.replace("!!WSP!!", " ")
706 };
707
708 let parsed = NarInfo::parse(&input).expect("should parse");
709
710 assert!(parsed.flags.contains(Flags::FILE_HASH_HEX));
711 assert_eq!(parsed.to_string(), input, "should roundtrip");
712 }
713
714 #[test]
718 fn sign() {
719 let input = {
721 let input = r#"StorePath: /nix/store/0vpqfxbkx0ffrnhbws6g9qwhmliksz7f-perl-HTTP-Cookies-6.01
722URL: nar/0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi.nar.xz
723Compression: xz
724FileHash: sha256:0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi
725FileSize: 7120
726NarHash: sha256:0h1bm4sj1cnfkxgyhvgi8df1qavnnv94sd0v09wcrm971602shfg
727NarSize: 22552
728References:!!WSP!!
729CA: fixed:r:sha1:1ak1ymbmsfx7z8kh09jzkr3a4dvkrfjw
730"#;
731 input.replace("!!WSP!!", " ")
732 };
733 let mut narinfo = NarInfo::parse(&input).expect("should parse");
734
735 let fp = narinfo.fingerprint();
736
737 let (signing_key, _verifying_key) =
739 super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed");
740
741 narinfo.add_signature(&signing_key);
743
744 let new_sig = narinfo.signatures.last().unwrap();
746 assert_eq!(signing_key.name(), *new_sig.name());
747
748 let verifying_key = super::VerifyingKey::parse(super::DUMMY_VERIFYING_KEY)
750 .expect("parsing dummy verifying key");
751
752 assert!(
753 verifying_key.verify(&fp, new_sig),
754 "expect signature to be valid"
755 );
756 }
757}