depit/
lock.rs

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/// Source of this dependency
14#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
15pub enum EntrySource {
16    /// URL
17    #[serde(rename = "url")]
18    Url(Url),
19    /// Local path
20    #[serde(rename = "path")]
21    Path(PathBuf),
22}
23
24/// WIT dependency [Lock] entry
25#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
26pub struct Entry {
27    /// Resource source
28    #[serde(flatten)]
29    pub source: EntrySource,
30    /// Resource digest
31    #[serde(flatten)]
32    pub digest: Digest,
33}
34
35impl Entry {
36    /// Create a new entry given a dependency source and path containing it
37    #[must_use]
38    pub fn new(source: EntrySource, digest: Digest) -> Self {
39        Self { source, digest }
40    }
41
42    /// Create a new entry given a dependency url and path containing the unpacked contents of it
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if [`Self::digest`] of `path` fails
47    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    /// Create a new entry given a dependency path
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if [`Self::digest`] of `path` fails
59    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    /// Compute the digest of an entry from path
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if tar-encoding the path fails
71    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/// WIT dependency lock mapping [Identifiers](Identifier) to [Entries](Entry)
77#[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}