1use std::collections::{BTreeMap, HashSet};
2use std::fmt;
3use std::str::FromStr;
4
5use thiserror::Error;
6
7use crate::derivation::{OutputName, ParseOutputNameError};
8use crate::narinfo::Signature;
9use crate::nixhash::{self, NixHash};
10use crate::store_path::StorePath;
11
12#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
13#[cfg_attr(
14 feature = "serde",
15 derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
16)]
17pub struct DrvOutput {
18 pub drv_hash: NixHash,
19 pub output_name: OutputName,
20}
21
22impl fmt::Display for DrvOutput {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(
25 f,
26 "{}!{}",
27 self.drv_hash.as_nix_lowerhex_string_fmt(),
28 self.output_name
29 )
30 }
31}
32
33#[derive(Debug, PartialEq, Error)]
34pub enum ParseDrvOutputError {
35 #[error("invalid hash in DrvOutput")]
36 Hash(
37 #[from]
38 #[source]
39 nixhash::Error,
40 ),
41 #[error("invalid output name in DrvOutput")]
42 OutputName(
43 #[from]
44 #[source]
45 ParseOutputNameError,
46 ),
47 #[error("missing '!' in DrvOutput '{0}'")]
48 InvalidDerivationOutputId(String),
49}
50
51impl FromStr for DrvOutput {
52 type Err = ParseDrvOutputError;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 if let Some((drv_hash_s, output_name_s)) = s.split_once('!') {
56 let drv_hash = NixHash::from_str(drv_hash_s, None)?;
57 let output_name = output_name_s.parse()?;
58 Ok(DrvOutput {
59 drv_hash,
60 output_name,
61 })
62 } else {
63 Err(ParseDrvOutputError::InvalidDerivationOutputId(s.into()))
64 }
65 }
66}
67
68#[cfg_attr(
69 feature = "serde",
70 cfg_eval::cfg_eval,
71 serde_with::serde_as,
72 derive(serde::Deserialize, serde::Serialize),
73 serde(rename_all = "camelCase")
74)]
75#[derive(Debug, PartialEq, Eq, Clone)]
76pub struct Realisation {
77 pub id: DrvOutput,
78 #[cfg_attr(feature = "serde", serde_as(as = "serde_with::DisplayFromStr"))]
79 pub out_path: StorePath,
80 pub signatures: HashSet<Signature<String>>,
81 #[cfg_attr(
82 feature = "serde",
83 serde_as(as = "BTreeMap<_, serde_with::DisplayFromStr>")
84 )]
85 pub dependent_realisations: BTreeMap<DrvOutput, StorePath>,
86}
87
88pub type DrvOutputs = BTreeMap<DrvOutput, Realisation>;
89
90#[cfg(test)]
91mod unittests {
92 use rstest::rstest;
93
94 use crate::btree_map;
95 use crate::derivation::OutputName;
96 use crate::hash_set;
97 use crate::nixhash::NixHash;
98
99 use super::{DrvOutput, Realisation};
100
101 #[rstest]
102 #[case("sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1!out", DrvOutput {
103 drv_hash: NixHash::from_str("sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", None).unwrap(),
104 output_name: OutputName::default(),
105 })]
106 #[case("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394!out_put", DrvOutput {
107 drv_hash: NixHash::from_str("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394", None).unwrap(),
108 output_name: "out_put".parse().unwrap(),
109 })]
110 fn parse_drv_output(#[case] value: &str, #[case] expected: DrvOutput) {
111 let actual: DrvOutput = value.parse().unwrap();
112 assert_eq!(actual, expected);
113 }
114
115 #[rstest]
116 #[should_panic = "missing '!' in DrvOutput 'sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394'"]
117 #[case("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394")]
118 #[should_panic = "invalid hash in DrvOutput"]
119 #[case("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm39!out")]
120 #[should_panic = "invalid output name in DrvOutput"]
121 #[case("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394!out{put")]
122 fn parse_drv_output_failure(#[case] value: &str) {
123 let actual = value.parse::<DrvOutput>().unwrap_err();
124 panic!("{actual}");
125 }
126
127 #[rstest]
128 #[case(DrvOutput {
129 drv_hash: NixHash::from_str("sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", None).unwrap(),
130 output_name: OutputName::default(),
131 }, "sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1!out")]
132 #[case(DrvOutput {
133 drv_hash: NixHash::from_str("sha256:1h86vccx9vgcyrkj3zv4b7j3r8rrc0z0r4r6q3jvhf06s9hnm394", None).unwrap(),
134 output_name: OutputName::default(),
135 }, "sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1!out")]
136 #[case(DrvOutput {
137 drv_hash: NixHash::from_str("sha1:y5q4drg5558zk8aamsx6xliv3i23x644", None).unwrap(),
138 output_name: "out_put".parse().unwrap(),
139 }, "sha1:84983e441c3bd26ebaae4aa1f95129e5e54670f1!out_put")]
140 fn display_drv_output(#[case] value: DrvOutput, #[case] expected: &str) {
141 assert_eq!(value.to_string(), expected);
142 }
143
144 #[cfg(feature = "serde")]
145 #[rstest]
146 #[case(
147 "{\"dependentRealisations\":{\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a496177a9cf410ff61f20015ad!dev\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-dev\",\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a696177a9cf410ff61f20015ad!bin\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-bin\"},\"id\":\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad!out\",\"outPath\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3\",\"signatures\":[\"cache.nixos.org-1:0CpHca+06TwFp9VkMyz5OaphT3E8mnS+1SWymYlvFaghKSYPCMQ66TS1XPAr1+y9rfQZPLaHrBjjnIRktE/nAA==\"]}",
148 Realisation {
149 id: "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad!out".parse().unwrap(),
150 out_path: "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3".parse().unwrap(),
151 signatures: hash_set!["cache.nixos.org-1:0CpHca+06TwFp9VkMyz5OaphT3E8mnS+1SWymYlvFaghKSYPCMQ66TS1XPAr1+y9rfQZPLaHrBjjnIRktE/nAA=="],
152 dependent_realisations: btree_map![
153 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a496177a9cf410ff61f20015ad!dev" => "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-dev",
154 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a696177a9cf410ff61f20015ad!bin" => "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-bin",
155
156 ],
157 }
158 )]
159 fn parse_realisation(#[case] value: &str, #[case] expected: Realisation) {
160 let actual: Realisation = serde_json::from_str(value).unwrap();
161 pretty_assertions::assert_eq!(actual, expected);
162 }
163
164 #[cfg(feature = "serde")]
165 #[rstest]
166 #[case(
167 Realisation {
168 id: "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad!out".parse().unwrap(),
169 out_path: "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3".parse().unwrap(),
170 signatures: hash_set!["cache.nixos.org-1:0CpHca+06TwFp9VkMyz5OaphT3E8mnS+1SWymYlvFaghKSYPCMQ66TS1XPAr1+y9rfQZPLaHrBjjnIRktE/nAA=="],
171 dependent_realisations: btree_map![
172 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a496177a9cf410ff61f20015ad!dev" => "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-dev",
173 "sha256:ba7816bf8f01cfea414140de5dae2223b00361a696177a9cf410ff61f20015ad!bin" => "7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-bin",
174
175 ],
176 },
177 "{\"id\":\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad!out\",\"outPath\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3\",\"signatures\":[\"cache.nixos.org-1:0CpHca+06TwFp9VkMyz5OaphT3E8mnS+1SWymYlvFaghKSYPCMQ66TS1XPAr1+y9rfQZPLaHrBjjnIRktE/nAA==\"],\"dependentRealisations\":{\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a496177a9cf410ff61f20015ad!dev\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-dev\",\"sha256:ba7816bf8f01cfea414140de5dae2223b00361a696177a9cf410ff61f20015ad!bin\":\"7h7qgvs4kgzsn8a6rb273saxyqh4jxlz-konsole-18.12.3-bin\"}}",
178 )]
179 fn write_realisation(#[case] value: Realisation, #[case] expected: &str) {
180 let actual = serde_json::to_string(&value).unwrap();
181 pretty_assertions::assert_eq!(actual, expected);
182 }
183}