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
13fn default_subdir() -> Box<str> {
14 "wit".into()
15}
16
17fn is_default_subdir(s: &str) -> bool {
18 s == "wit"
19}
20
21#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
23#[serde(untagged)]
24pub enum EntrySource {
25 Url {
27 url: Url,
29 #[serde(default = "default_subdir", skip_serializing_if = "is_default_subdir")]
31 subdir: Box<str>,
32 },
33 Path {
35 path: PathBuf,
37 },
38}
39
40#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct Entry {
43 #[serde(flatten)]
45 pub source: Option<EntrySource>,
46 #[serde(flatten)]
48 pub digest: Digest,
49 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
51 pub deps: BTreeSet<Identifier>,
52}
53
54impl Entry {
55 #[must_use]
57 pub fn new(source: Option<EntrySource>, digest: Digest, deps: BTreeSet<Identifier>) -> Self {
58 Self {
59 source,
60 digest,
61 deps,
62 }
63 }
64
65 pub async fn from_url(
71 url: Url,
72 path: impl AsRef<Path>,
73 deps: BTreeSet<Identifier>,
74 subdir: impl Into<Box<str>>,
75 ) -> anyhow::Result<Self> {
76 let digest = Self::digest(path)
77 .await
78 .context("failed to compute digest")?;
79 Ok(Self::new(
80 Some(EntrySource::Url {
81 url,
82 subdir: subdir.into(),
83 }),
84 digest,
85 deps,
86 ))
87 }
88
89 pub async fn from_path(
95 src: PathBuf,
96 dst: impl AsRef<Path>,
97 deps: BTreeSet<Identifier>,
98 ) -> anyhow::Result<Self> {
99 let digest = Self::digest(dst)
100 .await
101 .context("failed to compute digest")?;
102 Ok(Self::new(
103 Some(EntrySource::Path { path: src }),
104 digest,
105 deps,
106 ))
107 }
108
109 pub async fn from_transitive_path(dst: impl AsRef<Path>) -> anyhow::Result<Self> {
115 let digest = Self::digest(dst)
116 .await
117 .context("failed to compute digest")?;
118 Ok(Self::new(None, digest, BTreeSet::default()))
119 }
120
121 pub async fn digest(path: impl AsRef<Path>) -> std::io::Result<Digest> {
127 tar(path, DigestWriter::from(sink())).await.map(Into::into)
128 }
129}
130
131#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
133pub struct Lock(BTreeMap<Identifier, Entry>);
134
135impl Deref for Lock {
136 type Target = BTreeMap<Identifier, Entry>;
137
138 fn deref(&self) -> &Self::Target {
139 &self.0
140 }
141}
142
143impl DerefMut for Lock {
144 fn deref_mut(&mut self) -> &mut Self::Target {
145 &mut self.0
146 }
147}
148
149impl FromIterator<(Identifier, Entry)> for Lock {
150 fn from_iter<T: IntoIterator<Item = (Identifier, Entry)>>(iter: T) -> Self {
151 Self(BTreeMap::from_iter(iter))
152 }
153}
154
155impl Extend<(Identifier, Entry)> for Lock {
156 fn extend<T: IntoIterator<Item = (Identifier, Entry)>>(&mut self, iter: T) {
157 self.0.extend(iter);
158 }
159}
160
161impl<const N: usize> From<[(Identifier, Entry); N]> for Lock {
162 fn from(entries: [(Identifier, Entry); N]) -> Self {
163 Self::from_iter(entries)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 use anyhow::{ensure, Context};
172 use hex::FromHex;
173
174 const FOO_URL: &str = "https://example.com/baz";
175 const FOO_SHA256: &str = "9f86d081884c7d658a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
176 const FOO_SHA512: &str = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff";
177
178 #[test]
179 fn decode() -> anyhow::Result<()> {
180 fn assert_lock(lock: Lock) -> anyhow::Result<Lock> {
181 ensure!(
182 lock == Lock::from([(
183 "foo".parse().expect("failed to `foo` parse identifier"),
184 Entry {
185 source: Some(EntrySource::Url {
186 url: FOO_URL.parse().expect("failed to parse `foo` URL"),
187 subdir: "wit".into(),
188 }),
189 digest: Digest {
190 sha256: FromHex::from_hex(FOO_SHA256)
191 .expect("failed to decode `foo` sha256"),
192 sha512: FromHex::from_hex(FOO_SHA512)
193 .expect("failed to decode `foo` sha512"),
194 },
195 deps: BTreeSet::default(),
196 }
197 )])
198 );
199 Ok(lock)
200 }
201
202 let lock = toml::from_str(&format!(
203 r#"
204foo = {{ url = "{FOO_URL}", sha256 = "{FOO_SHA256}", sha512 = "{FOO_SHA512}" }}
205"#
206 ))
207 .context("failed to decode lock")
208 .and_then(assert_lock)?;
209
210 let lock = toml::to_string(&lock).context("failed to encode lock")?;
211 toml::from_str(&lock)
212 .context("failed to decode lock")
213 .and_then(assert_lock)?;
214
215 Ok(())
216 }
217}