Skip to main content

nix_compat/derivation/
outputs.rs

1//! Outputs for a derivation.
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4
5use crate::{
6    derivation::{OutputHash, OutputName},
7    store_path::{self, StorePath},
8};
9
10/// Derivation outputs with filled out [`StorePath`].
11///
12/// Each output has an [`OutputName`] and a [`StorePath`] and `Outputs`
13/// provides a map of [`OutputName`] to [`StorePath`]  to provide easy
14/// access while still maintaining invariants.
15///
16/// In general there are two types of outputs: fixed or input addressed.
17/// And while the fixed output also has a name and a [`StorePath`] it
18/// addtionally also has a [`OutputHash`].
19///
20/// # Invariants
21/// - Only a single FOD output is allowed and it must be named `out`
22/// - Duplicately named outputs is an error
23/// - Outputs can not be empty
24///
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Outputs(OutputsInner);
27
28impl Outputs {
29    /// Create `Outputs` with a fixed output with provided `output_hash` and `store_path`.
30    ///
31    /// This single fixed output is always named `out`.
32    pub fn fixed_output(output_hash: OutputHash, store_path: StorePath) -> Self {
33        Outputs(OutputsInner::Fixed {
34            output_hash,
35            store_path,
36        })
37    }
38
39    /// Try to create input addressed `Outputs` from the provided iterator.
40    ///
41    /// This will return a [`OutputsError`] if the [`OutputName`], [`StorePath`] pairs
42    /// in the iterator don't follow the [invariants].
43    ///
44    /// [invariants]: #invariants
45    pub fn input_addressed_from_iter<I>(it: I) -> Result<Self, OutputsError>
46    where
47        I: IntoIterator<Item = (OutputName, StorePath)>,
48    {
49        Self::try_from_iter(
50            it.into_iter()
51                .map(|(output_name, store_path)| (output_name, store_path, None)),
52        )
53    }
54
55    /// Try to make `Outputs` from the provided iterator.
56    ///
57    /// This will return a [`OutputsError`] if the [`OutputName`], [`StorePath`], [`OutputHash`] tuple
58    /// in the iterator don't follow the [invariants].
59    ///
60    /// [invariants]: #invariants
61    pub fn try_from_iter<I>(it: I) -> Result<Self, OutputsError>
62    where
63        I: IntoIterator<Item = (OutputName, StorePath, Option<OutputHash>)>,
64    {
65        let mut builder = UnverifiedOutputsBuilder::new();
66        for (output_name, store_path, output_hash) in it {
67            builder.try_insert(output_name, store_path, output_hash)?;
68        }
69        builder.try_build()
70    }
71
72    /// Return output hash if this `Outputs` is fixed.
73    pub fn as_fixed_output_hash(&self) -> Option<&OutputHash> {
74        if let OutputsInner::Fixed { output_hash, .. } = &self.0 {
75            Some(output_hash)
76        } else {
77            None
78        }
79    }
80
81    /// Return output hash and store path if this `Outputs` is fixed.
82    pub fn as_fixed_output(&self) -> Option<(&OutputHash, &StorePath)> {
83        if let OutputsInner::Fixed {
84            output_hash,
85            store_path,
86        } = &self.0
87        {
88            Some((output_hash, store_path))
89        } else {
90            None
91        }
92    }
93
94    /// Convert this `Outputs` into an `OutputsBuilder` with the same type and output names.
95    pub fn into_builder(self) -> OutputsBuilder {
96        match self.0 {
97            OutputsInner::Fixed { output_hash, .. } => OutputsBuilder::Fixed(output_hash),
98            OutputsInner::InputAddressed(outputs) => {
99                let output_names = outputs.into_keys().collect();
100                OutputsBuilder::InputAddressed(output_names)
101            }
102        }
103    }
104
105    /// Convert this `Outputs` into an `OutputsBuilder` with the same type and output names.
106    pub fn into_unverified_builder(self) -> UnverifiedOutputsBuilder {
107        UnverifiedOutputsBuilder(self.0)
108    }
109
110    /// Returns `true` if the outputs contains an output with the specified `name`.
111    #[must_use]
112    pub fn contains_key(&self, name: &OutputName) -> bool {
113        self.0.contains_key(name)
114    }
115
116    /// Returns a reference to the [`StorePath`] corresponding to the provided `name`.
117    pub fn get(&self, name: &OutputName) -> Option<&StorePath> {
118        self.0.get(name)
119    }
120
121    /// Gets an iterator over the entries of the outputs, sorted by name.
122    pub fn iter(&self) -> Iter<'_> {
123        self.0.iter()
124    }
125
126    /// Gets an iterator over the names of the outputs, in sorted order.
127    ///
128    /// # Examples
129    /// ```
130    /// use nix_compat::derivation::{Outputs, OutputName};
131    /// use nix_compat::store_path::StorePath;
132    ///
133    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
134    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
135    /// let a = Outputs::input_addressed_from_iter([
136    ///     (OutputName::out(), out_sp),
137    ///     (OutputName::from_static("bin").unwrap(), bin_sp),
138    /// ]).expect("multiple outputs");
139    ///
140    /// let names: Vec<OutputName> = a.names().cloned().collect();
141    /// assert_eq!(names, [
142    ///     OutputName::from_static("bin").unwrap(),
143    ///     OutputName::out(),
144    /// ]);
145    /// ```
146    pub fn names(&self) -> OutputNames<'_> {
147        self.0.names()
148    }
149
150    /// Convert this `Outputs` to an iterator over the names of the outputs, in sorted order.
151    pub fn into_names(self) -> IntoOutputNames {
152        self.0.into_names()
153    }
154
155    /// Gets an iterator over the [`StorePath`] values of the outputs, in order by name.
156    ///
157    /// # Examples
158    /// ```
159    /// use nix_compat::derivation::{Outputs, OutputName};
160    /// use nix_compat::store_path::StorePath;
161    ///
162    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
163    /// let a = Outputs::input_addressed_from_iter([(OutputName::out(), out_sp.clone())]).unwrap();
164    ///
165    /// let values: Vec<StorePath> = a.store_paths().cloned().collect();
166    /// assert_eq!(values, [out_sp]);
167    /// ```
168    pub fn store_paths(&self) -> impl Iterator<Item = &StorePath> {
169        self.iter().map(|(_, path)| path)
170    }
171
172    /// Returns the number of outputs
173    ///
174    /// # Examples
175    /// ```
176    /// use nix_compat::derivation::{Outputs, OutputName};
177    /// use nix_compat::store_path::StorePath;
178    ///
179    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
180    /// let a = Outputs::input_addressed_from_iter([(OutputName::out(), out_sp.clone())]).unwrap();
181    /// assert_eq!(a.len(), 1);
182    ///
183    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
184    /// let b = Outputs::input_addressed_from_iter([
185    ///     (OutputName::out(), out_sp),
186    ///     (OutputName::from_static("bin").unwrap(), bin_sp),
187    /// ]).expect("multiple outputs");
188    /// assert_eq!(b.len(), 2);
189    /// ```
190    #[expect(clippy::len_without_is_empty)]
191    pub fn len(&self) -> usize {
192        self.0.len()
193    }
194
195    /// Returns `true` if this contains only a single output.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use nix_compat::derivation::{Outputs, OutputName};
201    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
202    /// # use nix_compat::nixhash::NixHash;
203    /// use nix_compat::store_path::StorePath;
204    ///
205    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
206    ///
207    /// # const DIGEST_SHA256: [u8; 32] =
208    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
209    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
210    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
211    /// let a = Outputs::fixed_output(output_hash, out_sp.clone());
212    /// assert!(a.is_single());
213    ///
214    /// let b = Outputs::input_addressed_from_iter([(OutputName::out(), out_sp.clone())]).unwrap();
215    /// assert!(b.is_single());
216    ///
217    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
218    /// let c = Outputs::input_addressed_from_iter([
219    ///     (OutputName::out(), out_sp),
220    ///     (OutputName::from_static("bin").unwrap(), bin_sp),
221    /// ]).unwrap();
222    /// assert!(!c.is_single());
223    /// ```
224    #[must_use]
225    pub fn is_single(&self) -> bool {
226        self.0.is_single()
227    }
228
229    /// Returns `true` if this is a single fixed-output.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use nix_compat::derivation::{Outputs, OutputName};
235    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
236    /// # use nix_compat::nixhash::NixHash;
237    /// use nix_compat::store_path::StorePath;
238    ///
239    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
240    /// # const DIGEST_SHA256: [u8; 32] =
241    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
242    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
243    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
244    /// let a = Outputs::fixed_output(output_hash, out_sp.clone());
245    /// assert!(a.is_fixed());
246    ///
247    /// let b = Outputs::input_addressed_from_iter([(OutputName::out(), out_sp.clone())]).unwrap();
248    /// assert!(!b.is_fixed());
249    ///
250    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
251    /// let c = Outputs::input_addressed_from_iter([
252    ///     (OutputName::out(), out_sp),
253    ///     (OutputName::from_static("bin").unwrap(), bin_sp),
254    /// ]).unwrap();
255    /// assert!(!c.is_fixed());
256    /// ```
257    #[must_use]
258    pub fn is_fixed(&self) -> bool {
259        self.0.is_fixed()
260    }
261
262    /// Returns `true` if this is a set of input addressed outputs.
263    #[must_use]
264    pub fn is_input_addressed(&self) -> bool {
265        self.0.is_input_addressed()
266    }
267}
268
269impl From<Outputs> for OutputsBuilder {
270    fn from(value: Outputs) -> Self {
271        value.into_builder()
272    }
273}
274
275impl<'a> IntoIterator for &'a Outputs {
276    type Item = (&'a OutputName, &'a StorePath);
277
278    type IntoIter = Iter<'a>;
279
280    fn into_iter(self) -> Self::IntoIter {
281        self.iter()
282    }
283}
284
285impl IntoIterator for Outputs {
286    type Item = (OutputName, StorePath);
287
288    type IntoIter = IntoIter;
289
290    fn into_iter(self) -> Self::IntoIter {
291        self.0.into_iter_internal()
292    }
293}
294
295#[cfg(feature = "serde")]
296impl<'de> serde::Deserialize<'de> for Outputs {
297    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298    where
299        D: serde::Deserializer<'de>,
300    {
301        struct OutputsVisitor;
302        impl<'de> serde::de::Visitor<'de> for OutputsVisitor {
303            type Value = Outputs;
304
305            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
306                f.write_str("derivation outputs")
307            }
308
309            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
310            where
311                A: serde::de::MapAccess<'de>,
312            {
313                use data_encoding::HEXLOWER;
314                use serde::de::Error;
315                #[derive(serde::Deserialize)]
316                struct Output<'o> {
317                    path: StorePath,
318                    #[serde(rename = "hashAlgo")]
319                    #[serde(default)]
320                    hash_algo: &'o str,
321                    #[serde(default)]
322                    hash: &'o str,
323                }
324                fn extract<'o, E: serde::de::Error>(
325                    output: Output<'o>,
326                ) -> Result<(StorePath, Option<OutputHash>), E> {
327                    if output.hash.is_empty() && output.hash_algo.is_empty() {
328                        Ok((output.path, None))
329                    } else {
330                        let digest = HEXLOWER.decode(output.hash.as_bytes()).map_err(E::custom)?;
331                        let output_hash =
332                            OutputHash::from_mode_algo_and_digest(output.hash_algo, digest)
333                                .map_err(E::custom)?;
334                        Ok((output.path, Some(output_hash)))
335                    }
336                }
337
338                let Some((output_name, output)) = map.next_entry::<OutputName, Output>()? else {
339                    return Err(A::Error::invalid_length(0, &"non-empty derivation outputs"));
340                };
341                let (store_path, output_hash) = extract(output)?;
342                let mut builder = UnverifiedOutputsBuilder::new();
343                builder
344                    .try_insert(output_name, store_path, output_hash)
345                    .map_err(A::Error::custom)?;
346
347                while let Some((output_name, output)) = map.next_entry::<OutputName, Output>()? {
348                    let (store_path, output_hash) = extract(output)?;
349                    builder
350                        .try_insert(output_name, store_path, output_hash)
351                        .map_err(A::Error::custom)?;
352                }
353
354                builder.try_build().map_err(A::Error::custom)
355            }
356        }
357
358        deserializer.deserialize_map(OutputsVisitor)
359    }
360}
361
362#[cfg(feature = "serde")]
363impl serde::Serialize for Outputs {
364    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
365    where
366        S: serde::Serializer,
367    {
368        use serde::ser::SerializeMap as _;
369        #[derive(serde::Serialize)]
370        struct OutputRef<'b> {
371            path: &'b StorePath,
372            #[serde(rename = "hashAlgo")]
373            #[serde(skip_serializing_if = "str::is_empty")]
374            hash_algo: &'b str,
375            #[serde(skip_serializing_if = "String::is_empty")]
376            hash: String,
377        }
378        let mut map = serializer.serialize_map(Some(self.len()))?;
379        if let Some(output_hash) = self.as_fixed_output_hash() {
380            use data_encoding::HEXLOWER;
381            let digest = HEXLOWER.encode(output_hash.hash.digest_as_bytes());
382            for (output_name, path) in self {
383                map.serialize_entry(
384                    output_name,
385                    &OutputRef {
386                        path,
387                        hash_algo: output_hash.as_mode_and_algo_str(),
388                        hash: digest.clone(),
389                    },
390                )?;
391            }
392        } else {
393            for (output_name, path) in self {
394                map.serialize_entry(
395                    output_name,
396                    &OutputRef {
397                        path,
398                        hash_algo: "",
399                        hash: String::new(),
400                    },
401                )?;
402            }
403        }
404        map.end()
405    }
406}
407
408/// Builder for making unverified [`Outputs`].
409///
410/// Even when not verifying the output path of a set of outputs, there are still [invariants] that
411/// must be upheld. But rather than checking all of them in one go, it is useful during construction
412/// to check each output as they are added and then do a final check when building the `Outputs`.
413///
414/// # Examples
415///
416/// ```
417/// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
418/// use nix_compat::store_path::StorePath;
419///
420/// let mut builder = UnverifiedOutputsBuilder::new();
421/// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
422/// builder.try_insert(OutputName::out(), out_sp, None).unwrap();
423/// let outputs = builder.try_build().unwrap();
424/// ```
425///
426/// [invariants]: Outputs#invariants
427#[derive(Clone, Debug, Default, PartialEq, Eq)]
428pub struct UnverifiedOutputsBuilder(OutputsInner);
429
430impl UnverifiedOutputsBuilder {
431    /// Return an empty `UnverifiedOutputsBuilder`.
432    pub const fn new() -> Self {
433        Self(OutputsInner::new())
434    }
435
436    /// Returns the number of outputs
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
442    /// use nix_compat::store_path::StorePath;
443    ///
444    /// let mut builder = UnverifiedOutputsBuilder::new();
445    /// assert_eq!(builder.len(), 0);
446    ///
447    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
448    /// builder.try_insert(OutputName::out(), out_sp, None).unwrap();
449    /// assert_eq!(builder.len(), 1);
450    /// ```
451    pub fn len(&self) -> usize {
452        self.0.len()
453    }
454
455    /// Returns `true` if there are not outputs defined in this builder.
456    ///
457    /// # Examples
458    ///
459    /// ```
460    /// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
461    /// use nix_compat::store_path::StorePath;
462    ///
463    /// let mut builder = UnverifiedOutputsBuilder::new();
464    /// assert!(builder.is_empty());
465    ///
466    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
467    /// builder.try_insert(OutputName::out(), out_sp, None).unwrap();
468    /// assert!(!builder.is_empty());
469    /// ```
470    #[must_use]
471    pub fn is_empty(&self) -> bool {
472        self.0.len() == 0
473    }
474
475    /// Returns `true` if this is a set of input addressed outputs.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
481    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
482    /// # use nix_compat::nixhash::NixHash;
483    /// use nix_compat::store_path::StorePath;
484    ///
485    /// let mut a = UnverifiedOutputsBuilder::new();
486    /// assert!(!a.is_input_addressed());
487    ///
488    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
489    /// a.try_insert(OutputName::out(), out_sp, None).unwrap();
490    /// assert!(a.is_input_addressed());
491    ///
492    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
493    /// a.try_insert(OutputName::from_static("bin").unwrap(), bin_sp, None).unwrap();
494    /// assert!(a.is_input_addressed());
495    ///
496    /// let mut b = UnverifiedOutputsBuilder::new();
497    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
498    /// # const DIGEST_SHA256: [u8; 32] =
499    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
500    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
501    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
502    /// b.try_insert(OutputName::out(), out_sp, Some(output_hash)).unwrap();
503    /// assert!(!b.is_input_addressed());
504    /// ```
505    #[must_use]
506    pub fn is_input_addressed(&self) -> bool {
507        self.0.is_input_addressed()
508    }
509
510    /// Returns `true` if this builder contains a single output called "out"
511    ///
512    /// # Examples
513    ///
514    /// ```
515    /// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
516    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
517    /// # use nix_compat::nixhash::NixHash;
518    /// use nix_compat::store_path::StorePath;
519    ///
520    /// let mut a = UnverifiedOutputsBuilder::new();
521    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
522    /// a.try_insert(OutputName::out(), out_sp, None).unwrap();
523    /// assert!(a.is_single());
524    ///
525    /// let bin_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyib-has-multi-out-bin".parse().unwrap();
526    /// a.try_insert(OutputName::from_static("bin").unwrap(), bin_sp, None).unwrap();
527    /// assert!(!a.is_single());
528    ///
529    /// let mut b = UnverifiedOutputsBuilder::new();
530    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
531    /// # const DIGEST_SHA256: [u8; 32] =
532    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
533    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
534    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
535    /// b.try_insert(OutputName::out(), out_sp, Some(output_hash)).unwrap();
536    /// assert!(b.is_single());
537    /// ```
538    #[must_use]
539    pub fn is_single(&self) -> bool {
540        self.0.is_single()
541    }
542
543    /// Returns `true` if this is a set of input addressed outputs.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use nix_compat::derivation::{OutputName, UnverifiedOutputsBuilder};
549    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
550    /// # use nix_compat::nixhash::NixHash;
551    /// use nix_compat::store_path::StorePath;
552    ///
553    /// let mut a = UnverifiedOutputsBuilder::new();
554    /// assert!(!a.is_fixed());
555    ///
556    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
557    /// a.try_insert(OutputName::out(), out_sp, None).unwrap();
558    /// assert!(!a.is_fixed());
559    ///
560    /// let mut b = UnverifiedOutputsBuilder::new();
561    /// let out_sp : StorePath = "2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out".parse().unwrap();
562    /// # const DIGEST_SHA256: [u8; 32] =
563    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
564    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
565    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
566    /// b.try_insert(OutputName::out(), out_sp, Some(output_hash)).unwrap();
567    /// assert!(b.is_fixed());
568    /// ```
569    #[must_use]
570    pub fn is_fixed(&self) -> bool {
571        self.0.is_fixed()
572    }
573
574    /// Returns `true` if the outputs contains an output with the specified `name`.
575    #[must_use]
576    pub fn contains_key(&self, name: &OutputName) -> bool {
577        self.0.contains_key(name)
578    }
579
580    /// Returns a reference to the [`StorePath`] corresponding to the provided `name`.
581    pub fn get(&self, name: &OutputName) -> Option<&StorePath> {
582        self.0.get(name)
583    }
584
585    /// Gets an iterator over the names of the outputs, in sorted order.
586    pub fn names(&self) -> OutputNames<'_> {
587        self.0.names()
588    }
589
590    /// Consume this builder and return an iterator over the names of the outputs, in sorted order.
591    pub fn into_names(self) -> IntoOutputNames {
592        self.0.into_names()
593    }
594
595    /// Gets an iterator over the entries of the outputs, sorted by name.
596    pub fn iter(&self) -> Iter<'_> {
597        self.0.iter()
598    }
599
600    /// Gets an iterator over the [`StorePath`] values of the outputs, in order by name.
601    pub fn store_paths(&self) -> impl Iterator<Item = &StorePath> {
602        self.iter().map(|(_, path)| path)
603    }
604
605    /// Try to insert a new output with the provided values and an error
606    /// if this violates the [invariants].
607    ///
608    /// [invariants]: #invariants
609    pub fn try_insert(
610        &mut self,
611        output_name: OutputName,
612        output_path: StorePath,
613        output_hash: Option<OutputHash>,
614    ) -> Result<(), OutputsError> {
615        if self.is_empty() {
616            if let Some(output_hash) = output_hash {
617                self.0 = OutputsInner::Fixed {
618                    output_hash,
619                    store_path: output_path,
620                };
621            } else {
622                self.0 =
623                    OutputsInner::InputAddressed(BTreeMap::from_iter([(output_name, output_path)]));
624            }
625            return Ok(());
626        }
627
628        if self.is_fixed() || output_hash.is_some() {
629            return Err(OutputsError::MoreThanOneOutputButFixed());
630        }
631
632        match std::mem::take(&mut self.0) {
633            OutputsInner::Fixed { .. } => {
634                return Err(OutputsError::MoreThanOneOutputButFixed());
635            }
636            OutputsInner::InputAddressed(mut outputs) => {
637                if outputs.insert(output_name.clone(), output_path).is_some() {
638                    return Err(OutputsError::DuplicateOutputName(output_name));
639                }
640                self.0 = OutputsInner::InputAddressed(outputs);
641            }
642        }
643        Ok(())
644    }
645
646    /// Consume this builder and return the built [`Outputs`] or an error if not possible.
647    pub fn try_build(self) -> Result<Outputs, OutputsError> {
648        if self.is_empty() {
649            return Err(OutputsError::NoOutputs());
650        }
651
652        if self.len() == 1 && !self.is_single() {
653            return Err(OutputsError::InvalidOutputName(
654                self.names().next().unwrap().to_string(),
655            ));
656        }
657
658        Ok(Outputs(self.0))
659    }
660}
661
662impl From<Outputs> for UnverifiedOutputsBuilder {
663    fn from(value: Outputs) -> Self {
664        value.into_unverified_builder()
665    }
666}
667
668impl<'a> IntoIterator for &'a UnverifiedOutputsBuilder {
669    type Item = (&'a OutputName, &'a StorePath);
670
671    type IntoIter = Iter<'a>;
672
673    fn into_iter(self) -> Self::IntoIter {
674        self.iter()
675    }
676}
677
678impl IntoIterator for UnverifiedOutputsBuilder {
679    type Item = (OutputName, StorePath);
680
681    type IntoIter = IntoIter;
682
683    fn into_iter(self) -> Self::IntoIter {
684        self.0.into_iter_internal()
685    }
686}
687
688#[derive(Clone, Debug, PartialEq, Eq)]
689enum OutputsInner {
690    Fixed {
691        output_hash: OutputHash,
692        store_path: StorePath,
693    },
694    InputAddressed(BTreeMap<OutputName, StorePath>),
695}
696
697impl OutputsInner {
698    pub const fn new() -> Self {
699        OutputsInner::InputAddressed(BTreeMap::new())
700    }
701
702    pub fn len(&self) -> usize {
703        match self {
704            OutputsInner::Fixed { .. } => 1,
705            OutputsInner::InputAddressed(outputs) => outputs.len(),
706        }
707    }
708
709    pub fn is_fixed(&self) -> bool {
710        matches!(self, OutputsInner::Fixed { .. })
711    }
712
713    pub fn is_input_addressed(&self) -> bool {
714        matches!(self, OutputsInner::InputAddressed(outputs) if !outputs.is_empty())
715    }
716
717    pub fn is_single(&self) -> bool {
718        self.len() == 1
719    }
720
721    pub fn contains_key(&self, name: &OutputName) -> bool {
722        match self {
723            OutputsInner::Fixed { .. } => *name == OutputName::out(),
724            OutputsInner::InputAddressed(outputs) => outputs.contains_key(name),
725        }
726    }
727
728    pub fn get(&self, name: &OutputName) -> Option<&StorePath> {
729        match self {
730            OutputsInner::Fixed { store_path, .. } if *name == OutputName::out() => {
731                Some(store_path)
732            }
733            OutputsInner::InputAddressed(outputs) => outputs.get(name),
734            _ => None,
735        }
736    }
737
738    pub fn names(&self) -> OutputNames<'_> {
739        match self {
740            OutputsInner::Fixed { .. } => {
741                const OUT: &OutputName = &OutputName::out();
742                OutputNames(OutputNameInner::Single(std::iter::once(OUT)))
743            }
744            OutputsInner::InputAddressed(outputs) => {
745                OutputNames(OutputNameInner::BTreeMap(outputs.keys()))
746            }
747        }
748    }
749
750    pub fn into_names(self) -> IntoOutputNames {
751        match self {
752            OutputsInner::Fixed { .. } => IntoOutputNames(IntoOutputNameInner::Single(
753                std::iter::once(OutputName::out()),
754            )),
755            OutputsInner::InputAddressed(outputs) => {
756                IntoOutputNames(IntoOutputNameInner::BTreeMap(outputs.into_keys()))
757            }
758        }
759    }
760
761    /// Gets an iterator over the entries of the outputs, sorted by name.
762    pub fn iter(&self) -> Iter<'_> {
763        match self {
764            OutputsInner::Fixed { store_path, .. } => {
765                const OUT: &OutputName = &OutputName::out();
766                Iter(IterI::Fixed(std::iter::once((OUT, store_path))))
767            }
768            OutputsInner::InputAddressed(outputs) => Iter(IterI::InputAddressed(outputs.iter())),
769        }
770    }
771
772    fn into_iter_internal(self) -> IntoIter {
773        match self {
774            OutputsInner::Fixed { store_path, .. } => IntoIter(IntoIterI::Fixed(std::iter::once(
775                (OutputName::out(), store_path),
776            ))),
777            OutputsInner::InputAddressed(outputs) => {
778                IntoIter(IntoIterI::InputAddressed(outputs.into_iter()))
779            }
780        }
781    }
782}
783
784impl Default for OutputsInner {
785    fn default() -> Self {
786        Self::new()
787    }
788}
789
790/// A builder for outputs.
791#[derive(Clone, Debug, PartialEq, Eq)]
792#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(from = "Outputs"))]
793pub enum OutputsBuilder {
794    /// A fixed ouput
795    Fixed(OutputHash),
796    /// Input addressed outputs
797    InputAddressed(BTreeSet<OutputName>),
798}
799
800impl OutputsBuilder {
801    /// Gets an iterator over the names of the outputs, in sorted order.
802    pub fn names(&self) -> OutputNames<'_> {
803        const OUT: &OutputName = &OutputName::out();
804        match self {
805            OutputsBuilder::Fixed(_) => OutputNames(OutputNameInner::Single(std::iter::once(OUT))),
806            OutputsBuilder::InputAddressed(outputs) => {
807                OutputNames(OutputNameInner::BTreeSet(outputs.iter()))
808            }
809        }
810    }
811
812    /// Convert this `OutputsBuilder` to an iterator over the names of the outputs, in sorted order.
813    pub fn into_names(self) -> IntoOutputNames {
814        match self {
815            OutputsBuilder::Fixed(_) => IntoOutputNames(IntoOutputNameInner::Single(
816                std::iter::once(OutputName::out()),
817            )),
818            OutputsBuilder::InputAddressed(outputs) => {
819                IntoOutputNames(IntoOutputNameInner::BTreeSet(outputs.into_iter()))
820            }
821        }
822    }
823
824    /// Return count of names in this `OutputsBuilder`.
825    pub fn len(&self) -> usize {
826        match self {
827            OutputsBuilder::Fixed(_) => 1,
828            OutputsBuilder::InputAddressed(outputs) => outputs.len(),
829        }
830    }
831
832    /// Returns `true` if the builder contains no outputs.
833    pub fn is_empty(&self) -> bool {
834        self.len() == 0
835    }
836
837    /// Returns `true` if the builder contains a single output.
838    pub fn is_single(&self) -> bool {
839        self.len() == 1
840    }
841
842    /// Returns `true` if the builder contains a fixed output.
843    pub fn is_fixed(&self) -> bool {
844        matches!(self, Self::Fixed(_))
845    }
846
847    /// Returns `true` if the builder contains an output with the provided `name`.
848    pub fn contains(&self, name: &OutputName) -> bool {
849        match self {
850            OutputsBuilder::Fixed(_) => *name == OutputName::out(),
851            OutputsBuilder::InputAddressed(output_names) => output_names.contains(name),
852        }
853    }
854}
855
856impl Default for OutputsBuilder {
857    fn default() -> Self {
858        Self::InputAddressed(BTreeSet::from_iter([OutputName::out()]))
859    }
860}
861
862impl PartialEq<Outputs> for OutputsBuilder {
863    fn eq(&self, other: &Outputs) -> bool {
864        self.is_fixed() == other.is_fixed() && self.names().eq(other.names())
865    }
866}
867
868impl PartialEq<OutputsBuilder> for Outputs {
869    fn eq(&self, other: &OutputsBuilder) -> bool {
870        self.is_fixed() == other.is_fixed() && self.names().eq(other.names())
871    }
872}
873
874impl<'a> IntoIterator for &'a OutputsBuilder {
875    type Item = &'a OutputName;
876
877    type IntoIter = OutputNames<'a>;
878
879    fn into_iter(self) -> Self::IntoIter {
880        self.names()
881    }
882}
883
884impl IntoIterator for OutputsBuilder {
885    type Item = OutputName;
886
887    type IntoIter = IntoOutputNames;
888
889    fn into_iter(self) -> Self::IntoIter {
890        self.into_names()
891    }
892}
893
894impl FromIterator<OutputName> for OutputsBuilder {
895    fn from_iter<T: IntoIterator<Item = OutputName>>(iter: T) -> Self {
896        OutputsBuilder::InputAddressed(iter.into_iter().collect())
897    }
898}
899
900/// Errors that can occur during the creation and validation of [`Outputs`].
901#[derive(Debug, PartialEq, thiserror::Error)]
902#[allow(missing_docs)]
903pub enum OutputsError {
904    #[error("no outputs defined")]
905    NoOutputs(),
906    #[error("outputs failed verification")]
907    VerificationError(),
908    #[error("invalid output name: {0}")]
909    InvalidOutputName(String),
910    #[error("duplicate output name: {0}")]
911    DuplicateOutputName(OutputName),
912    #[error("encountered fixed-output derivation, but more than 1 output in total")]
913    MoreThanOneOutputButFixed(),
914    #[error("invalid output name for fixed-output derivation: {0}")]
915    InvalidOutputNameForFixed(String),
916    #[error("invalid calculated output derivation path name: {0}")]
917    InvalidOutputDerivationPath(String, #[source] store_path::ParseStorePathError),
918}
919
920/// An iterator over the entries of `Outputs`.
921///
922/// This `struct` is created by the [`iter`] method on [`Outputs`]. See its
923/// documentation for more.
924///
925/// [`iter`]: Outputs::iter
926pub struct Iter<'a>(IterI<'a>);
927impl fmt::Debug for Iter<'_> {
928    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
929        f.write_str("Iter")
930    }
931}
932
933enum IterI<'a> {
934    Fixed(std::iter::Once<(&'a OutputName, &'a StorePath)>),
935    InputAddressed(std::collections::btree_map::Iter<'a, OutputName, StorePath>),
936}
937
938impl<'a> Iterator for Iter<'a> {
939    type Item = (&'a OutputName, &'a StorePath);
940
941    fn next(&mut self) -> Option<Self::Item> {
942        match &mut self.0 {
943            IterI::Fixed(it) => it.next(),
944            IterI::InputAddressed(it) => it.next(),
945        }
946    }
947    fn size_hint(&self) -> (usize, Option<usize>) {
948        match &self.0 {
949            IterI::Fixed(it) => it.size_hint(),
950            IterI::InputAddressed(it) => it.size_hint(),
951        }
952    }
953}
954
955impl<'a> ExactSizeIterator for Iter<'a> {}
956
957/// An iterator over the entries of `Outputs`.
958///
959/// This `struct` is created by the [`into_iter`] method on [`Outputs`]. See its
960/// documentation for more.
961///
962/// [`into_iter`]: Outputs::into_iter
963pub struct IntoIter(IntoIterI);
964
965impl fmt::Debug for IntoIter {
966    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
967        f.write_str("IntoIter")
968    }
969}
970
971enum IntoIterI {
972    Fixed(std::iter::Once<(OutputName, StorePath)>),
973    InputAddressed(std::collections::btree_map::IntoIter<OutputName, StorePath>),
974}
975
976impl Iterator for IntoIter {
977    type Item = (OutputName, StorePath);
978
979    fn next(&mut self) -> Option<Self::Item> {
980        match &mut self.0 {
981            IntoIterI::Fixed(it) => it.next(),
982            IntoIterI::InputAddressed(it) => it.next(),
983        }
984    }
985    fn size_hint(&self) -> (usize, Option<usize>) {
986        match &self.0 {
987            IntoIterI::Fixed(it) => it.size_hint(),
988            IntoIterI::InputAddressed(it) => it.size_hint(),
989        }
990    }
991}
992
993impl ExactSizeIterator for IntoIter {}
994
995/// An iterator over the names of outputs.
996///
997/// This `struct` is created by [`OutputsBuilder::names`] and [`Outputs::names`].
998/// See their documentation for more.
999pub struct OutputNames<'a>(OutputNameInner<'a>);
1000
1001impl fmt::Debug for OutputNames<'_> {
1002    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1003        f.write_str("OutputNames")
1004    }
1005}
1006
1007impl<'a> Iterator for OutputNames<'a> {
1008    type Item = &'a OutputName;
1009
1010    fn next(&mut self) -> Option<Self::Item> {
1011        match &mut self.0 {
1012            OutputNameInner::Single(it) => it.next(),
1013            OutputNameInner::BTreeSet(it) => it.next(),
1014            OutputNameInner::BTreeMap(it) => it.next(),
1015        }
1016    }
1017    fn size_hint(&self) -> (usize, Option<usize>) {
1018        match &self.0 {
1019            OutputNameInner::Single(it) => it.size_hint(),
1020            OutputNameInner::BTreeSet(it) => it.size_hint(),
1021            OutputNameInner::BTreeMap(it) => it.size_hint(),
1022        }
1023    }
1024}
1025impl<'a> ExactSizeIterator for OutputNames<'a> {}
1026
1027enum OutputNameInner<'a> {
1028    Single(std::iter::Once<&'a OutputName>),
1029    BTreeSet(std::collections::btree_set::Iter<'a, OutputName>),
1030    BTreeMap(std::collections::btree_map::Keys<'a, OutputName, StorePath>),
1031}
1032
1033/// An iterator over the owned names of outputs.
1034///
1035/// This `struct` is created by [`OutputsBuilder::into_names`] and [`Outputs::into_names`].
1036/// See their documentation for more.
1037pub struct IntoOutputNames(IntoOutputNameInner);
1038impl fmt::Debug for IntoOutputNames {
1039    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1040        f.write_str("IntoOutputNames")
1041    }
1042}
1043
1044impl Iterator for IntoOutputNames {
1045    type Item = OutputName;
1046
1047    fn next(&mut self) -> Option<Self::Item> {
1048        match &mut self.0 {
1049            IntoOutputNameInner::Single(it) => it.next(),
1050            IntoOutputNameInner::BTreeSet(it) => it.next(),
1051            IntoOutputNameInner::BTreeMap(it) => it.next(),
1052        }
1053    }
1054    fn size_hint(&self) -> (usize, Option<usize>) {
1055        match &self.0 {
1056            IntoOutputNameInner::Single(it) => it.size_hint(),
1057            IntoOutputNameInner::BTreeSet(it) => it.size_hint(),
1058            IntoOutputNameInner::BTreeMap(it) => it.size_hint(),
1059        }
1060    }
1061}
1062impl ExactSizeIterator for IntoOutputNames {}
1063
1064enum IntoOutputNameInner {
1065    Single(std::iter::Once<OutputName>),
1066    BTreeSet(std::collections::btree_set::IntoIter<OutputName>),
1067    BTreeMap(std::collections::btree_map::IntoKeys<OutputName, StorePath>),
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use std::sync::LazyLock;
1073
1074    use rstest::rstest;
1075
1076    use crate::{
1077        derivation::{OutputHash, OutputHashMode, OutputName, Outputs, OutputsBuilder},
1078        nixhash::NixHash,
1079        store_path::StorePath,
1080    };
1081
1082    const DIGEST_SHA256: [u8; 32] =
1083        hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
1084    const OUTPUT_HASH: OutputHash = OutputHash {
1085        mode: OutputHashMode::Flat,
1086        hash: NixHash::Sha256(DIGEST_SHA256),
1087    };
1088    static STORE_PATH: LazyLock<StorePath> = LazyLock::new(|| {
1089        StorePath::from_bytes(b"2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib").unwrap()
1090    });
1091
1092    const FOD_OUTPUTS_BUILDER: OutputsBuilder = OutputsBuilder::Fixed(OutputHash {
1093        mode: OutputHashMode::Flat,
1094        hash: NixHash::Sha256(DIGEST_SHA256),
1095    });
1096    static SINGLE_OUTPUTS_BUILDER: LazyLock<OutputsBuilder> =
1097        LazyLock::new(|| OutputsBuilder::from_iter([OutputName::out()]));
1098    static SINGLE_NON_OUT_OUTPUTS_BUILDER: LazyLock<OutputsBuilder> =
1099        LazyLock::new(|| OutputsBuilder::from_iter([OutputName::from_static("bin").unwrap()]));
1100    static MULTIPLE_OUTPUTS_BUILDER: LazyLock<OutputsBuilder> = LazyLock::new(|| {
1101        OutputsBuilder::from_iter([
1102            OutputName::from_static("dev").unwrap(),
1103            OutputName::from_static("bin").unwrap(),
1104        ])
1105    });
1106
1107    static FOD_OUTPUTS: LazyLock<Outputs> =
1108        LazyLock::new(|| Outputs::fixed_output(OUTPUT_HASH.clone(), STORE_PATH.clone()));
1109    static TRY_SINGLE_OUTPUTS: LazyLock<Outputs> = LazyLock::new(|| {
1110        Outputs::input_addressed_from_iter([(OutputName::out(), STORE_PATH.clone())])
1111            .expect("single output")
1112    });
1113    static TRY_SINGLE_NON_OUT_OUTPUTS: LazyLock<Outputs> = LazyLock::new(|| {
1114        Outputs::input_addressed_from_iter([(
1115            OutputName::from_static("bin").unwrap(),
1116            STORE_PATH.clone(),
1117        )])
1118        .expect("single output")
1119    });
1120    static TRY_FOD_OUTPUTS: LazyLock<Outputs> = LazyLock::new(|| -> Outputs {
1121        Outputs::try_from_iter([(
1122            OutputName::out(),
1123            STORE_PATH.clone(),
1124            Some(OUTPUT_HASH.clone()),
1125        )])
1126        .expect("single fod")
1127    });
1128    static TRY_MULTIPLE_OUTPUTS: LazyLock<Outputs> = LazyLock::new(|| -> Outputs {
1129        Outputs::input_addressed_from_iter([
1130            (OutputName::from_static("dev").unwrap(), STORE_PATH.clone()),
1131            (OutputName::from_static("bin").unwrap(), STORE_PATH.clone()),
1132        ])
1133        .expect("multiple")
1134    });
1135
1136    mod outputs_builder {
1137        use super::*;
1138        use rstest::rstest;
1139
1140        #[rstest]
1141        #[case::single(&SINGLE_OUTPUTS_BUILDER)]
1142        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER)]
1143        #[case::fod(&FOD_OUTPUTS_BUILDER)]
1144        #[case::default(&Default::default())]
1145        fn is_single(#[case] value: &OutputsBuilder) {
1146            assert!(value.is_single())
1147        }
1148
1149        #[rstest]
1150        #[case::multiple(&MULTIPLE_OUTPUTS_BUILDER)]
1151        fn is_not_single(#[case] value: &OutputsBuilder) {
1152            assert!(!value.is_single())
1153        }
1154
1155        #[rstest]
1156        #[case::fod(&FOD_OUTPUTS_BUILDER)]
1157        fn is_fixed(#[case] value: &OutputsBuilder) {
1158            assert!(value.is_fixed())
1159        }
1160
1161        #[rstest]
1162        #[case::single(&SINGLE_OUTPUTS_BUILDER)]
1163        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER)]
1164        #[case::multiple(&MULTIPLE_OUTPUTS_BUILDER)]
1165        #[case::default(&Default::default())]
1166        fn is_not_fixed(#[case] value: &OutputsBuilder) {
1167            assert!(!value.is_fixed())
1168        }
1169
1170        #[rstest]
1171        #[case::single(&SINGLE_OUTPUTS_BUILDER, 1)]
1172        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER, 1)]
1173        #[case::fod(&FOD_OUTPUTS_BUILDER, 1)]
1174        #[case::multiple(&MULTIPLE_OUTPUTS_BUILDER, 2)]
1175        #[case::default(&Default::default(), 1)]
1176        fn len(#[case] value: &OutputsBuilder, #[case] expected: usize) {
1177            assert_eq!(value.len(), expected)
1178        }
1179
1180        #[rstest]
1181        #[case::single(&SINGLE_OUTPUTS_BUILDER, OutputName::out())]
1182        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER, "bin")]
1183        #[case::fod(&FOD_OUTPUTS_BUILDER, OutputName::out())]
1184        #[case::multiple_bin(&MULTIPLE_OUTPUTS_BUILDER, "bin")]
1185        #[case::default(&Default::default(), OutputName::out())]
1186        fn contains(#[case] value: &OutputsBuilder, #[case] name: OutputName) {
1187            assert!(value.contains(&name))
1188        }
1189
1190        #[rstest]
1191        #[case::single(&SINGLE_OUTPUTS_BUILDER, "bin")]
1192        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER, "out")]
1193        #[case::fod(&FOD_OUTPUTS_BUILDER, "bin")]
1194        #[case::multiple_out(&MULTIPLE_OUTPUTS_BUILDER, OutputName::out())]
1195        fn does_not_contain(#[case] value: &OutputsBuilder, #[case] name: OutputName) {
1196            assert!(!value.contains(&name))
1197        }
1198
1199        #[rstest]
1200        #[case::single(&SINGLE_OUTPUTS_BUILDER, vec![OutputName::out()])]
1201        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER, vec![OutputName::from_static("bin").unwrap()])]
1202        #[case::fod(&FOD_OUTPUTS_BUILDER, vec![OutputName::out()])]
1203        #[case::multiple(&MULTIPLE_OUTPUTS_BUILDER, vec![
1204            OutputName::from_static("bin").unwrap(),
1205            OutputName::from_static("dev").unwrap(),
1206        ])]
1207        #[case::default(&Default::default(), vec![OutputName::out()])]
1208        fn names(#[case] value: &OutputsBuilder, #[case] expected: Vec<OutputName>) {
1209            let actual: Vec<_> = value.names().cloned().collect();
1210            assert_eq!(actual, expected);
1211        }
1212
1213        #[rstest]
1214        #[case::single(&SINGLE_OUTPUTS_BUILDER, vec![OutputName::out()])]
1215        #[case::single_non_out(&SINGLE_NON_OUT_OUTPUTS_BUILDER, vec![OutputName::from_static("bin").unwrap()])]
1216        #[case::fod(&FOD_OUTPUTS_BUILDER, vec![OutputName::out()])]
1217        #[case::multiple(&MULTIPLE_OUTPUTS_BUILDER, vec![
1218            OutputName::from_static("bin").unwrap(),
1219            OutputName::from_static("dev").unwrap(),
1220        ])]
1221        #[case::default(&Default::default(), vec![OutputName::out()])]
1222        fn into_names(#[case] value: &OutputsBuilder, #[case] expected: Vec<OutputName>) {
1223            let actual: Vec<_> = value.clone().into_names().collect();
1224            assert_eq!(actual, expected);
1225        }
1226    }
1227
1228    #[rstest]
1229    #[should_panic(expected = "no outputs defined")]
1230    #[case::empty(&[])]
1231    #[should_panic(expected = "duplicate output name: bin")]
1232    #[case::duplicate(&[
1233        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone(), None),
1234        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone(), None),
1235    ])]
1236    #[should_panic(
1237        expected = "encountered fixed-output derivation, but more than 1 output in total"
1238    )]
1239    #[case::mixed(&[
1240        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone(), Some(OUTPUT_HASH.clone())),
1241        (OutputName::from_static("dev").unwrap(), STORE_PATH.clone(), None),
1242    ])]
1243    fn try_from_iter_failure(#[case] it: &[(OutputName, StorePath, Option<OutputHash>)]) {
1244        panic!(
1245            "{}",
1246            Outputs::try_from_iter(it.iter().cloned()).expect_err("try_from_iter succeeded")
1247        );
1248    }
1249
1250    #[rstest]
1251    #[should_panic(expected = "no outputs defined")]
1252    #[case::empty(&[])]
1253    #[should_panic(expected = "duplicate output name: bin")]
1254    #[case::duplicate(&[
1255        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone()),
1256        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone()),
1257    ])]
1258    fn input_addressed_from_iter_failure(#[case] it: &[(OutputName, StorePath)]) {
1259        panic!(
1260            "{}",
1261            Outputs::input_addressed_from_iter(it.iter().cloned())
1262                .expect_err("try_from_iter succeeded")
1263        );
1264    }
1265
1266    #[rstest]
1267    #[case::fod(&FOD_OUTPUTS)]
1268    #[case::try_single(&TRY_SINGLE_OUTPUTS)]
1269    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS)]
1270    #[case::try_fod(&TRY_FOD_OUTPUTS)]
1271    fn is_single(#[case] value: &Outputs) {
1272        assert!(value.is_single())
1273    }
1274
1275    #[rstest]
1276    #[case::multiple(&TRY_MULTIPLE_OUTPUTS)]
1277    fn is_not_single(#[case] value: &Outputs) {
1278        assert!(!value.is_single())
1279    }
1280
1281    #[rstest]
1282    #[case::fod(&FOD_OUTPUTS)]
1283    #[case::try_fod(&TRY_FOD_OUTPUTS)]
1284    fn is_fixed(#[case] value: &Outputs) {
1285        assert!(value.is_fixed())
1286    }
1287
1288    #[rstest]
1289    #[case::try_single(&TRY_SINGLE_OUTPUTS)]
1290    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS)]
1291    #[case::multiple(&TRY_MULTIPLE_OUTPUTS)]
1292    fn is_not_fixed(#[case] value: &Outputs) {
1293        assert!(!value.is_fixed())
1294    }
1295
1296    #[rstest]
1297    #[case::fod(&FOD_OUTPUTS, 1)]
1298    #[case::try_fod(&TRY_FOD_OUTPUTS, 1)]
1299    #[case::try_single(&TRY_SINGLE_OUTPUTS, 1)]
1300    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, 1)]
1301    #[case::multiple(&TRY_MULTIPLE_OUTPUTS, 2)]
1302    fn len(#[case] value: &Outputs, #[case] expected: usize) {
1303        assert_eq!(value.len(), expected)
1304    }
1305
1306    #[rstest]
1307    #[case::fod(&FOD_OUTPUTS, OutputName::out(), Some(&*STORE_PATH))]
1308    #[case::try_fod(&TRY_FOD_OUTPUTS, OutputName::out(), Some(&*STORE_PATH))]
1309    #[case::try_single(&TRY_SINGLE_OUTPUTS, OutputName::out(), Some(&*STORE_PATH))]
1310    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, OutputName::from_static("bin").unwrap(), Some(&*STORE_PATH))]
1311    #[case::multiple_out(&TRY_MULTIPLE_OUTPUTS, OutputName::out(), None)]
1312    #[case::multiple_bin(&TRY_MULTIPLE_OUTPUTS, OutputName::from_static("bin").unwrap(), Some(&*STORE_PATH))]
1313    fn get(
1314        #[case] value: &Outputs,
1315        #[case] name: OutputName,
1316        #[case] expected: Option<&StorePath>,
1317    ) {
1318        assert_eq!(value.get(&name), expected)
1319    }
1320
1321    #[rstest]
1322    #[case::fod(&FOD_OUTPUTS, OutputName::out())]
1323    #[case::try_fod(&TRY_FOD_OUTPUTS, OutputName::out())]
1324    #[case::try_single(&TRY_SINGLE_OUTPUTS, OutputName::out())]
1325    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, OutputName::from_static("bin").unwrap())]
1326    #[case::multiple_bin(&TRY_MULTIPLE_OUTPUTS, OutputName::from_static("bin").unwrap())]
1327    fn contains_key(#[case] value: &Outputs, #[case] name: OutputName) {
1328        assert!(value.contains_key(&name))
1329    }
1330
1331    #[rstest]
1332    #[case::multiple_out(&TRY_MULTIPLE_OUTPUTS, OutputName::out())]
1333    fn does_not_contain_key(#[case] value: &Outputs, #[case] name: OutputName) {
1334        assert!(!value.contains_key(&name))
1335    }
1336
1337    #[rstest]
1338    #[case::fod(&FOD_OUTPUTS, vec![(OutputName::out(), STORE_PATH.clone())])]
1339    #[case::try_fod(&TRY_FOD_OUTPUTS, vec![(OutputName::out(), STORE_PATH.clone())])]
1340    #[case::try_single(&TRY_SINGLE_OUTPUTS, vec![(OutputName::out(), STORE_PATH.clone())])]
1341    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, vec![(OutputName::from_static("bin").unwrap(), STORE_PATH.clone())])]
1342    #[case::multiple(&TRY_MULTIPLE_OUTPUTS, vec![
1343        (OutputName::from_static("bin").unwrap(), STORE_PATH.clone()),
1344        (OutputName::from_static("dev").unwrap(), STORE_PATH.clone()),
1345    ])]
1346    fn iter(#[case] value: &Outputs, #[case] expected: Vec<(OutputName, StorePath)>) {
1347        let actual: Vec<_> = value
1348            .iter()
1349            .map(|(name, output)| (name.clone(), output.clone()))
1350            .collect();
1351        assert_eq!(actual, expected);
1352    }
1353
1354    #[rstest]
1355    #[case::fod(&FOD_OUTPUTS, vec![OutputName::out()])]
1356    #[case::try_fod(&TRY_FOD_OUTPUTS, vec![OutputName::out()])]
1357    #[case::try_single(&TRY_SINGLE_OUTPUTS, vec![OutputName::out()])]
1358    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, vec![OutputName::from_static("bin").unwrap()])]
1359    #[case::multiple(&TRY_MULTIPLE_OUTPUTS, vec![
1360        OutputName::from_static("bin").unwrap(),
1361        OutputName::from_static("dev").unwrap(),
1362    ])]
1363    fn names(#[case] value: &Outputs, #[case] expected: Vec<OutputName>) {
1364        let actual: Vec<_> = value.names().cloned().collect();
1365        assert_eq!(actual, expected);
1366    }
1367
1368    #[rstest]
1369    #[case::fod(&FOD_OUTPUTS, vec![OutputName::out()])]
1370    #[case::try_fod(&TRY_FOD_OUTPUTS, vec![OutputName::out()])]
1371    #[case::try_single(&TRY_SINGLE_OUTPUTS, vec![OutputName::out()])]
1372    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, vec![OutputName::from_static("bin").unwrap()])]
1373    #[case::multiple(&TRY_MULTIPLE_OUTPUTS, vec![
1374        OutputName::from_static("bin").unwrap(),
1375        OutputName::from_static("dev").unwrap(),
1376    ])]
1377    fn into_names(#[case] value: &Outputs, #[case] expected: Vec<OutputName>) {
1378        let actual: Vec<_> = value.clone().into_names().collect();
1379        assert_eq!(actual, expected);
1380    }
1381
1382    #[rstest]
1383    #[case::fod(&FOD_OUTPUTS, vec![STORE_PATH.clone()])]
1384    #[case::try_fod(&TRY_FOD_OUTPUTS, vec![STORE_PATH.clone()])]
1385    #[case::try_single(&TRY_SINGLE_OUTPUTS, vec![STORE_PATH.clone()])]
1386    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS, vec![STORE_PATH.clone()])]
1387    #[case::multiple(&TRY_MULTIPLE_OUTPUTS, vec![STORE_PATH.clone(), STORE_PATH.clone()])]
1388    fn values(#[case] value: &Outputs, #[case] expected: Vec<StorePath>) {
1389        let actual: Vec<_> = value.store_paths().cloned().collect();
1390        assert_eq!(actual, expected);
1391    }
1392
1393    #[cfg(feature = "serde")]
1394    #[rstest]
1395    #[case::fod(&FOD_OUTPUTS)]
1396    #[case::try_fod(&TRY_FOD_OUTPUTS)]
1397    #[case::try_single(&TRY_SINGLE_OUTPUTS)]
1398    #[case::try_single_non_out(&TRY_SINGLE_NON_OUT_OUTPUTS)]
1399    #[case::multiple(&TRY_MULTIPLE_OUTPUTS)]
1400    fn serde(#[case] value: &Outputs) {
1401        let serialize = serde_json::to_string_pretty(&value).unwrap();
1402        let actual: Outputs = serde_json::from_str(&serialize).unwrap();
1403        assert_eq!(&actual, value);
1404    }
1405
1406    /// This ensures that a potentially valid input addressed
1407    /// output is deserialized as a non-fixed output.
1408    #[cfg(feature = "serde")]
1409    #[test]
1410    fn deserialize_valid_input_addressed_output() {
1411        let json_bytes = r#"
1412        {
1413            "out": {
1414                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
1415            }
1416        }"#;
1417        let output: Outputs = serde_json::from_str(json_bytes).expect("must parse");
1418
1419        assert!(!output.is_fixed());
1420    }
1421
1422    /// This ensures that a potentially valid fixed output
1423    /// output deserializes fine as a fixed output.
1424    #[cfg(feature = "serde")]
1425    #[test]
1426    fn deserialize_valid_fixed_output() {
1427        let json_bytes = r#"
1428        {
1429            "out": {
1430                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1431                "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
1432                "hashAlgo": "r:sha256"
1433            }
1434        }"#;
1435        let output: Outputs = serde_json::from_str(json_bytes).expect("must parse");
1436
1437        assert!(output.is_fixed());
1438    }
1439
1440    /// This ensures that parsing an input with the invalid hash encoding
1441    /// will result in a parsing failure.
1442    #[cfg(feature = "serde")]
1443    #[test]
1444    fn deserialize_with_error_invalid_hash_encoding_fixed_output() {
1445        let json_bytes = r#"
1446        {
1447            "out": {
1448                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1449                "hash": "IAMNOTVALIDNIXBASE32",
1450                "hashAlgo": "r:sha256"
1451            }
1452        }"#;
1453        let output: Result<Outputs, _> = serde_json::from_str(json_bytes);
1454
1455        assert!(output.is_err());
1456    }
1457
1458    /// This ensures that parsing an input with the wrong hash algo
1459    /// will result in a parsing failure.
1460    #[cfg(feature = "serde")]
1461    #[test]
1462    fn deserialize_with_error_invalid_hash_algo_fixed_output() {
1463        let json_bytes = r#"
1464        {
1465            "out": {
1466                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1467                "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
1468                "hashAlgo": "r:sha1024"
1469            }
1470        }"#;
1471        let output: Result<Outputs, _> = serde_json::from_str(json_bytes);
1472
1473        assert!(output.is_err());
1474    }
1475
1476    /// This ensures that parsing an input with the missing hash algo but present hash will result in a
1477    /// parsing failure.
1478    #[cfg(feature = "serde")]
1479    #[test]
1480    fn deserialize_with_error_missing_hash_algo_fixed_output() {
1481        let json_bytes = r#"
1482        {
1483            "out": {
1484                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1485                "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
1486            }
1487        }"#;
1488        let output: Result<Outputs, _> = serde_json::from_str(json_bytes);
1489
1490        assert!(output.is_err());
1491    }
1492
1493    /// This ensures that parsing an input with the missing hash but present hash algo will result in a
1494    /// parsing failure.
1495    #[cfg(feature = "serde")]
1496    #[test]
1497    fn deserialize_with_error_missing_hash_fixed_output() {
1498        let json_bytes = r#"
1499        {
1500            "out": {
1501                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1502                "hashAlgo": "r:sha1024"
1503            }
1504        }"#;
1505        let output: Result<Outputs, _> = serde_json::from_str(json_bytes);
1506
1507        assert!(output.is_err());
1508    }
1509
1510    #[cfg(feature = "serde")]
1511    #[test]
1512    fn serialize_deserialize() {
1513        let json_bytes = r#"
1514        {
1515            "out": {
1516                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
1517            }
1518        }"#;
1519        let output: Outputs = serde_json::from_str(json_bytes).expect("must parse");
1520
1521        let s = serde_json::to_string(&output).expect("Serialize");
1522        let output2: Outputs = serde_json::from_str(&s).expect("must parse again");
1523
1524        assert_eq!(output, output2);
1525    }
1526
1527    #[cfg(feature = "serde")]
1528    #[test]
1529    fn serialize_deserialize_fixed() {
1530        let json_bytes = r#"
1531        {
1532            "out": {
1533                "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
1534                "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
1535                "hashAlgo": "r:sha256"
1536            }
1537        }"#;
1538        let output: Outputs = serde_json::from_str(json_bytes).expect("must parse");
1539
1540        let s = serde_json::to_string_pretty(&output).expect("Serialize");
1541        let output2: Outputs = serde_json::from_str(&s).expect("must parse again");
1542
1543        assert_eq!(output, output2);
1544    }
1545}