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
13/// Source of this dependency
14#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
15#[serde(untagged)]
16pub enum EntrySource {
17    /// URL
18    Url {
19        /// URL
20        url: Url,
21        /// Prefix containing WIT definitions within the tarball
22        #[serde(default, skip_serializing_if = "str::is_empty")]
23        prefix: Box<str>,
24    },
25    /// Local path
26    Path {
27        /// Local path
28        path: PathBuf,
29    },
30}
31
32/// WIT dependency [Lock] entry
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct Entry {
35    /// Resource source, [None] if the dependency is transitive
36    #[serde(flatten)]
37    pub source: Option<EntrySource>,
38    /// Resource digest
39    #[serde(flatten)]
40    pub digest: Digest,
41    /// Transitive dependency identifiers
42    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
43    pub deps: BTreeSet<Identifier>,
44}
45
46impl Entry {
47    /// Create a new entry given a dependency source and path containing it
48    #[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    /// Create a new entry given a dependency url and path containing the unpacked contents of it
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if [`Self::digest`] of `path` fails
62    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    /// Create a new entry given a dependency path
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if [`Self::digest`] of `path` fails
86    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    /// Create a new entry given a transitive dependency path
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if [`Self::digest`] of `path` fails
106    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    /// Compute the digest of an entry from path
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if tar-encoding the path fails
118    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/// WIT dependency lock mapping [Identifiers](Identifier) to [Entries](Entry)
124#[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}