wit_deps/
lock.rs

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/// Source of this dependency
22#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
23#[serde(untagged)]
24pub enum EntrySource {
25    /// URL
26    Url {
27        /// URL
28        url: Url,
29        /// Subdirectory containing WIT definitions within the tarball
30        #[serde(default = "default_subdir", skip_serializing_if = "is_default_subdir")]
31        subdir: Box<str>,
32    },
33    /// Local path
34    Path {
35        /// Local path
36        path: PathBuf,
37    },
38}
39
40/// WIT dependency [Lock] entry
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42pub struct Entry {
43    /// Resource source, [None] if the dependency is transitive
44    #[serde(flatten)]
45    pub source: Option<EntrySource>,
46    /// Resource digest
47    #[serde(flatten)]
48    pub digest: Digest,
49    /// Transitive dependency identifiers
50    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
51    pub deps: BTreeSet<Identifier>,
52}
53
54impl Entry {
55    /// Create a new entry given a dependency source and path containing it
56    #[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    /// Create a new entry given a dependency url and path containing the unpacked contents of it
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if [`Self::digest`] of `path` fails
70    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    /// Create a new entry given a dependency path
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if [`Self::digest`] of `path` fails
94    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    /// Create a new entry given a transitive dependency path
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if [`Self::digest`] of `path` fails
114    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    /// Compute the digest of an entry from path
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if tar-encoding the path fails
126    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/// WIT dependency lock mapping [Identifiers](Identifier) to [Entries](Entry)
132#[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}