1use crate::{tar, Digest, DigestWriter, Identifier};
2
3use core::ops::{Deref, DerefMut};
4
5use std::collections::{BTreeMap, BTreeSet};
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)]
15#[serde(untagged)]
16pub enum EntrySource {
17 Url {
19 url: Url,
21 #[serde(default, skip_serializing_if = "str::is_empty")]
23 prefix: Box<str>,
24 },
25 Path {
27 path: PathBuf,
29 },
30}
31
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct Entry {
35 #[serde(flatten)]
37 pub source: Option<EntrySource>,
38 #[serde(flatten)]
40 pub digest: Digest,
41 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
43 pub deps: BTreeSet<Identifier>,
44}
45
46impl Entry {
47 #[must_use]
49 pub fn new(source: Option<EntrySource>, digest: Digest, deps: BTreeSet<Identifier>) -> Self {
50 Self {
51 source,
52 digest,
53 deps,
54 }
55 }
56
57 pub async fn from_url(
63 url: Url,
64 path: impl AsRef<Path>,
65 deps: BTreeSet<Identifier>,
66 prefix: impl Into<Box<str>>,
67 ) -> anyhow::Result<Self> {
68 let digest = Self::digest(path)
69 .await
70 .context("failed to compute digest")?;
71 Ok(Self::new(
72 Some(EntrySource::Url {
73 url,
74 prefix: prefix.into(),
75 }),
76 digest,
77 deps,
78 ))
79 }
80
81 pub async fn from_path(
87 src: PathBuf,
88 dst: impl AsRef<Path>,
89 deps: BTreeSet<Identifier>,
90 ) -> anyhow::Result<Self> {
91 let digest = Self::digest(dst)
92 .await
93 .context("failed to compute digest")?;
94 Ok(Self::new(
95 Some(EntrySource::Path { path: src }),
96 digest,
97 deps,
98 ))
99 }
100
101 pub async fn from_transitive_path(dst: impl AsRef<Path>) -> anyhow::Result<Self> {
107 let digest = Self::digest(dst)
108 .await
109 .context("failed to compute digest")?;
110 Ok(Self::new(None, digest, BTreeSet::default()))
111 }
112
113 pub async fn digest(path: impl AsRef<Path>) -> std::io::Result<Digest> {
119 tar(path, DigestWriter::from(sink())).await.map(Into::into)
120 }
121}
122
123#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
125pub struct Lock(BTreeMap<Identifier, Entry>);
126
127impl Deref for Lock {
128 type Target = BTreeMap<Identifier, Entry>;
129
130 fn deref(&self) -> &Self::Target {
131 &self.0
132 }
133}
134
135impl DerefMut for Lock {
136 fn deref_mut(&mut self) -> &mut Self::Target {
137 &mut self.0
138 }
139}
140
141impl FromIterator<(Identifier, Entry)> for Lock {
142 fn from_iter<T: IntoIterator<Item = (Identifier, Entry)>>(iter: T) -> Self {
143 Self(BTreeMap::from_iter(iter))
144 }
145}
146
147impl Extend<(Identifier, Entry)> for Lock {
148 fn extend<T: IntoIterator<Item = (Identifier, Entry)>>(&mut self, iter: T) {
149 self.0.extend(iter);
150 }
151}
152
153impl<const N: usize> From<[(Identifier, Entry); N]> for Lock {
154 fn from(entries: [(Identifier, Entry); N]) -> Self {
155 Self::from_iter(entries)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 use anyhow::{ensure, Context};
164 use hex::FromHex;
165
166 const FOO_URL: &str = "https://example.com/baz";
167 const FOO_SHA256: &str = "9f86d081884c7d658a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
168 const FOO_SHA512: &str = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff";
169
170 #[test]
171 fn decode() -> anyhow::Result<()> {
172 fn assert_lock(lock: Lock) -> anyhow::Result<Lock> {
173 ensure!(
174 lock == Lock::from([(
175 "foo".parse().expect("failed to `foo` parse identifier"),
176 Entry {
177 source: Some(EntrySource::Url {
178 url: FOO_URL.parse().expect("failed to parse `foo` URL"),
179 prefix: "".into(),
180 }),
181 digest: Digest {
182 sha256: FromHex::from_hex(FOO_SHA256)
183 .expect("failed to decode `foo` sha256"),
184 sha512: FromHex::from_hex(FOO_SHA512)
185 .expect("failed to decode `foo` sha512"),
186 },
187 deps: BTreeSet::default(),
188 }
189 )])
190 );
191 Ok(lock)
192 }
193
194 let lock = toml::from_str(&format!(
195 r#"
196foo = {{ url = "{FOO_URL}", sha256 = "{FOO_SHA256}", sha512 = "{FOO_SHA512}" }}
197"#
198 ))
199 .context("failed to decode lock")
200 .and_then(assert_lock)?;
201
202 let lock = toml::to_string(&lock).context("failed to encode lock")?;
203 toml::from_str(&lock)
204 .context("failed to decode lock")
205 .and_then(assert_lock)?;
206
207 Ok(())
208 }
209}