1use crate::{tar, Digest, DigestWriter, Identifier};
2
3use core::ops::Deref;
4
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use anyhow::Context;
9use futures::io::sink;
10use serde::{Deserialize, Serialize};
11use url::Url;
12
13#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
15pub enum EntrySource {
16 #[serde(rename = "url")]
18 Url(Url),
19 #[serde(rename = "path")]
21 Path(PathBuf),
22}
23
24#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
26pub struct Entry {
27 #[serde(flatten)]
29 pub source: EntrySource,
30 #[serde(flatten)]
32 pub digest: Digest,
33}
34
35impl Entry {
36 #[must_use]
38 pub fn new(source: EntrySource, digest: Digest) -> Self {
39 Self { source, digest }
40 }
41
42 pub async fn from_url(url: Url, path: impl AsRef<Path>) -> anyhow::Result<Self> {
48 let digest = Self::digest(path)
49 .await
50 .context("failed to compute digest")?;
51 Ok(Self::new(EntrySource::Url(url), digest))
52 }
53
54 pub async fn from_path(src: PathBuf, dst: impl AsRef<Path>) -> anyhow::Result<Self> {
60 let digest = Self::digest(dst)
61 .await
62 .context("failed to compute digest")?;
63 Ok(Self::new(EntrySource::Path(src), digest))
64 }
65
66 pub async fn digest(path: impl AsRef<Path>) -> std::io::Result<Digest> {
72 tar(path, DigestWriter::from(sink())).await.map(Into::into)
73 }
74}
75
76#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
78pub struct Lock(BTreeMap<Identifier, Entry>);
79
80impl Deref for Lock {
81 type Target = BTreeMap<Identifier, Entry>;
82
83 fn deref(&self) -> &Self::Target {
84 &self.0
85 }
86}
87
88impl FromIterator<(Identifier, Entry)> for Lock {
89 fn from_iter<T: IntoIterator<Item = (Identifier, Entry)>>(iter: T) -> Self {
90 Self(BTreeMap::from_iter(iter))
91 }
92}
93
94impl Extend<(Identifier, Entry)> for Lock {
95 fn extend<T: IntoIterator<Item = (Identifier, Entry)>>(&mut self, iter: T) {
96 self.0.extend(iter);
97 }
98}
99
100impl<const N: usize> From<[(Identifier, Entry); N]> for Lock {
101 fn from(entries: [(Identifier, Entry); N]) -> Self {
102 Self::from_iter(entries)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 use anyhow::{ensure, Context};
111 use hex::FromHex;
112
113 const FOO_URL: &str = "https://example.com/baz";
114 const FOO_SHA256: &str = "9f86d081884c7d658a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
115 const FOO_SHA512: &str = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff";
116
117 #[test]
118 fn decode() -> anyhow::Result<()> {
119 fn assert_lock(lock: Lock) -> anyhow::Result<Lock> {
120 ensure!(
121 lock == Lock::from([(
122 "foo".parse().expect("failed to `foo` parse identifier"),
123 Entry {
124 source: EntrySource::Url(
125 FOO_URL.parse().expect("failed to parse `foo` URL")
126 ),
127 digest: Digest {
128 sha256: FromHex::from_hex(FOO_SHA256)
129 .expect("failed to decode `foo` sha256"),
130 sha512: FromHex::from_hex(FOO_SHA512)
131 .expect("failed to decode `foo` sha512"),
132 },
133 }
134 )])
135 );
136 Ok(lock)
137 }
138
139 let lock = toml::from_str(&format!(
140 r#"
141foo = {{ url = "{FOO_URL}", sha256 = "{FOO_SHA256}", sha512 = "{FOO_SHA512}" }}
142"#
143 ))
144 .context("failed to decode lock")
145 .and_then(assert_lock)?;
146
147 let lock = toml::to_string(&lock).context("failed to encode lock")?;
148 toml::from_str(&lock)
149 .context("failed to decode lock")
150 .and_then(assert_lock)?;
151
152 Ok(())
153 }
154}