mr_bundle/
bundle.rs

1use crate::{
2    error::{BundleError, MrBundleResult},
3    location::Location,
4    manifest::Manifest,
5    resource::ResourceBytes,
6};
7use holochain_util::ffs;
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use std::{
10    borrow::Cow,
11    collections::{BTreeMap, HashMap, HashSet},
12    path::{Path, PathBuf},
13};
14
15pub type ResourceMap = BTreeMap<PathBuf, ResourceBytes>;
16
17/// A Manifest bundled together, optionally, with the Resources that it describes.
18/// This is meant to be serialized for standalone distribution, and deserialized
19/// by the receiver.
20///
21/// The manifest may describe locations of resources not included in the Bundle.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Bundle<M>
24where
25    M: Manifest,
26{
27    /// The manifest describing the resources that compose this bundle.
28    #[serde(bound(deserialize = "M: DeserializeOwned"))]
29    manifest: M,
30
31    /// The full or partial resource data. Each entry must correspond to one
32    /// of the Bundled Locations specified by the Manifest. Bundled Locations
33    /// are always relative paths (relative to the root_dir).
34    resources: ResourceMap,
35
36    /// Since the Manifest may contain local paths referencing unbundled files,
37    /// on the local filesystem, we must have an absolute path at runtime for
38    /// normalizing those locations.
39    ///
40    /// Passing None is a runtime assertion that the manifest contains only
41    /// absolute local paths. If this assertion fails,
42    /// **resource resolution will panic!**
43    //
44    // MAYBE: Represent this with types more solidly, perhaps breaking this
45    //        struct into two versions for each case.
46    #[serde(skip)]
47    root_dir: Option<PathBuf>,
48}
49
50impl<M> Bundle<M>
51where
52    M: Manifest,
53{
54    /// Creates a bundle containing a manifest and a collection of resources to
55    /// be bundled together with the manifest.
56    ///
57    /// The paths paired with each resource must correspond to the set of
58    /// `Location::Bundle`s specified in the `Manifest::location()`, or else
59    /// this is not a valid bundle.
60    ///
61    /// A base directory must also be supplied so that relative paths can be
62    /// resolved into absolute ones.
63    pub fn new<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
64        manifest: M,
65        resources: R,
66        root_dir: PathBuf,
67    ) -> MrBundleResult<Self> {
68        Self::from_parts(manifest, resources, Some(root_dir))
69    }
70
71    /// Create a bundle, asserting that all paths in the Manifest are absolute.
72    pub fn new_unchecked<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
73        manifest: M,
74        resources: R,
75    ) -> MrBundleResult<Self> {
76        Self::from_parts(manifest, resources, None)
77    }
78
79    fn from_parts<R: IntoIterator<Item = (PathBuf, ResourceBytes)>>(
80        manifest: M,
81        resources: R,
82        root_dir: Option<PathBuf>,
83    ) -> MrBundleResult<Self> {
84        let resources: ResourceMap = resources.into_iter().collect();
85        let manifest_paths: HashSet<_> = manifest
86            .locations()
87            .into_iter()
88            .filter_map(|loc| match loc {
89                Location::Bundled(path) => Some(path),
90                _ => None,
91            })
92            .collect();
93
94        // Validate that each resource path is contained in the manifest
95        for (resource_path, _) in resources.iter() {
96            if !manifest_paths.contains(resource_path) {
97                return Err(BundleError::BundledPathNotInManifest(resource_path.clone()).into());
98            }
99        }
100
101        let resources = resources.into_iter().collect();
102        Ok(Self {
103            manifest,
104            resources,
105            root_dir,
106        })
107    }
108
109    /// Accessor for the Manifest
110    pub fn manifest(&self) -> &M {
111        &self.manifest
112    }
113
114    /// Return a new Bundle with an updated manifest, subject to the same
115    /// validation constraints as creating a new Bundle from scratch.
116    pub fn update_manifest(self, manifest: M) -> MrBundleResult<Self> {
117        Self::from_parts(manifest, self.resources, self.root_dir)
118    }
119
120    /// Load a Bundle into memory from a file
121    pub async fn read_from_file(path: &Path) -> MrBundleResult<Self> {
122        Self::decode(&ffs::read(path).await?)
123    }
124
125    /// Write a Bundle to a file
126    pub async fn write_to_file(&self, path: &Path) -> MrBundleResult<()> {
127        Ok(ffs::write(path, &self.encode()?).await?)
128    }
129
130    /// Retrieve the bytes for a resource at a Location, downloading it if
131    /// necessary
132    pub async fn resolve(&self, location: &Location) -> MrBundleResult<Cow<'_, ResourceBytes>> {
133        let bytes = match &location.normalize(self.root_dir.as_ref())? {
134            Location::Bundled(path) => Cow::Borrowed(
135                self.resources
136                    .get(path)
137                    .ok_or_else(|| BundleError::BundledResourceMissing(path.clone()))?,
138            ),
139            Location::Path(path) => Cow::Owned(crate::location::resolve_local(path).await?),
140            Location::Url(url) => Cow::Owned(crate::location::resolve_remote(url).await?),
141        };
142        Ok(bytes)
143    }
144
145    /// Return the full set of resources specified by this bundle's manifest.
146    /// References to bundled resources can be returned directly, while all
147    /// others will be fetched from the filesystem or the network.
148    pub async fn resolve_all(&self) -> MrBundleResult<HashMap<Location, Cow<'_, ResourceBytes>>> {
149        futures::future::join_all(
150            self.manifest.locations().into_iter().map(|loc| async move {
151                MrBundleResult::Ok((loc.clone(), self.resolve(&loc).await?))
152            }),
153        )
154        .await
155        .into_iter()
156        .collect::<MrBundleResult<HashMap<Location, Cow<'_, ResourceBytes>>>>()
157    }
158
159    /// Resolve all resources, but with fully owned references
160    pub async fn resolve_all_cloned(&self) -> MrBundleResult<HashMap<Location, ResourceBytes>> {
161        Ok(self
162            .resolve_all()
163            .await?
164            .into_iter()
165            .map(|(k, v)| (k, v.into_owned()))
166            .collect())
167    }
168
169    /// Access the map of resources included in this bundle
170    /// Bundled resources are also accessible via `resolve` or `resolve_all`,
171    /// but using this method prevents a Clone
172    pub fn bundled_resources(&self) -> &ResourceMap {
173        &self.resources
174    }
175
176    /// An arbitrary and opaque encoding of the bundle data into a byte array
177    pub fn encode(&self) -> MrBundleResult<Vec<u8>> {
178        crate::encode(self)
179    }
180
181    /// Decode bytes produced by [`encode`](Bundle::encode)
182    pub fn decode(bytes: &[u8]) -> MrBundleResult<Self> {
183        crate::decode(bytes)
184    }
185
186    /// Given that the Manifest is located at the given absolute `path`, find
187    /// the absolute root directory for the "unpacked" Bundle directory.
188    /// Useful when "imploding" a directory into a bundle to determine the
189    /// default location of the generated Bundle file.
190    ///
191    /// This will only be different than the Manifest path itself if the
192    /// Manifest::path impl specifies a nested path.
193    ///
194    /// Will return None if the `path` does not actually end with the
195    /// manifest relative path, meaning that either the manifest file is
196    /// misplaced within the unpacked directory, or an incorrect path was
197    /// supplied.
198    #[cfg(feature = "packing")]
199    pub fn find_root_dir(&self, path: &Path) -> MrBundleResult<PathBuf> {
200        crate::util::prune_path(path.into(), M::path()).map_err(Into::into)
201    }
202}
203
204/// A manifest bundled together, optionally, with the Resources that it describes.
205/// The manifest may be of any format. This is useful for deserializing a bundle of
206/// an outdated format, so that it may be modified to fit the supported format.
207#[derive(Debug, PartialEq, Eq, Deserialize)]
208pub struct RawBundle<M> {
209    /// The manifest describing the resources that compose this bundle.
210    #[serde(bound(deserialize = "M: DeserializeOwned"))]
211    pub manifest: M,
212
213    /// The full or partial resource data. Each entry must correspond to one
214    /// of the Bundled Locations specified by the Manifest. Bundled Locations
215    /// are always relative paths (relative to the root_dir).
216    pub resources: ResourceMap,
217}
218
219impl<M: serde::de::DeserializeOwned> RawBundle<M> {
220    /// Load a Bundle into memory from a file
221    pub async fn read_from_file(path: &Path) -> MrBundleResult<Self> {
222        crate::decode(&ffs::read(path).await?)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use crate::error::MrBundleError;
229
230    use super::*;
231
232    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233    struct TestManifest(Vec<Location>);
234
235    impl Manifest for TestManifest {
236        fn locations(&self) -> Vec<Location> {
237            self.0.clone()
238        }
239
240        #[cfg(feature = "packing")]
241        fn path() -> PathBuf {
242            unimplemented!()
243        }
244
245        #[cfg(feature = "packing")]
246        fn bundle_extension() -> &'static str {
247            unimplemented!()
248        }
249    }
250
251    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252    struct Thing(u32);
253
254    #[tokio::test]
255    async fn bundle_validation() {
256        let manifest = TestManifest(vec![
257            Location::Bundled("1.thing".into()),
258            Location::Bundled("2.thing".into()),
259        ]);
260        assert!(
261            Bundle::new_unchecked(manifest.clone(), vec![("1.thing".into(), vec![1].into())])
262                .is_ok()
263        );
264
265        matches::assert_matches!(
266            Bundle::new_unchecked(manifest, vec![("3.thing".into(), vec![3].into())]),
267            Err(MrBundleError::BundleError(BundleError::BundledPathNotInManifest(path))) if path == PathBuf::from("3.thing")
268        );
269    }
270}