wapm_to_webc/
unpack.rs

1use std::{
2    collections::{BTreeSet, VecDeque},
3    path::{Path, PathBuf},
4};
5
6use anyhow::Context;
7use clap::Parser;
8use wapm_targz_to_pirita::ConvertError;
9use webc::{
10    v1::{DirOrFileWithBytes, FsEntryType, ParseOptions, Volume, WebC},
11    PathSegments,
12};
13
14use crate::pack::PackOptions;
15
16#[derive(Debug, Parser)]
17pub struct UnpackOptions {
18    #[clap(name = "INFILE")]
19    pub infile: PathBuf,
20    /// Output file name
21    #[clap(name = "OUTPATH")]
22    pub out: PathBuf,
23}
24
25impl UnpackOptions {
26    pub fn run(&self) -> Result<(), anyhow::Error> {
27        let infile = std::fs::read(Path::new(&self.infile))
28            .map_err(|e| ConvertError::Io(self.infile.display().to_string(), e))?;
29
30        match webc::detect(infile.as_slice())? {
31            webc::Version::V1 => {
32                let webc = WebC::parse(&infile, &ParseOptions::default())?;
33                unpack_webc(&webc, &self.out)?;
34            }
35            other => anyhow::bail!("Unsupported WEBC version, {other}"),
36        }
37
38        Ok(())
39    }
40}
41
42pub(crate) fn unpack_volume<'a>(
43    target_dir: &Path,
44    volume: &Volume<'a>,
45) -> Result<(), anyhow::Error> {
46    for atom in volume.get_all_files_and_directories_with_bytes()? {
47        match atom {
48            DirOrFileWithBytes::Dir { path } => {
49                std::fs::create_dir_all(target_dir.join(&path))
50                    .map_err(|e| anyhow::anyhow!("{e}"))
51                    .with_context(|| {
52                        anyhow::anyhow!("failed to create dir {}", target_dir.join(&path).display())
53                    })?;
54            }
55            DirOrFileWithBytes::File { path, bytes } => {
56                let target_path = target_dir.join(path);
57                if let Some(parent) = target_path.parent() {
58                    std::fs::create_dir_all(&parent)
59                        .map_err(|e| anyhow::anyhow!("{e}"))
60                        .with_context(|| {
61                            anyhow::anyhow!("failed to create dir {}", parent.display())
62                        })?;
63                }
64                let bytes_len = bytes.len();
65                std::fs::write(&target_path, bytes)
66                    .map_err(|e| anyhow::anyhow!("{e}"))
67                    .with_context(|| {
68                        anyhow::anyhow!(
69                            "failed to write {bytes_len} bytes into {}",
70                            target_path.display()
71                        )
72                    })?;
73            }
74        }
75    }
76
77    Ok(())
78}
79
80pub(crate) fn unpack_webc<'a>(webc: &WebC<'a>, target_dir: &PathBuf) -> Result<(), anyhow::Error> {
81    if !target_dir.is_dir() {
82        return Err(anyhow::anyhow!(
83            "Output path {} is not a directory",
84            target_dir.display()
85        ));
86    }
87
88    let manifest = serde_json::to_string_pretty(&webc.manifest)?;
89    std::fs::write(target_dir.clone().join("wapm.v1.json"), manifest.as_bytes())?;
90    std::fs::create_dir_all(target_dir.clone().join("atoms"));
91
92    let target_path = target_dir.clone().join("atoms");
93    std::fs::create_dir_all(&target_path)?;
94    unpack_volume(&target_path, &webc.atoms)?;
95
96    for (volume_name, volume) in webc.volumes.iter() {
97        let target_path = target_dir.clone().join(volume_name);
98        std::fs::create_dir_all(&target_path)?;
99        unpack_volume(&target_path, &volume)?;
100    }
101
102    Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107    use std::collections::HashSet;
108
109    use tempfile::TempDir;
110    use webc::{v2::read::OwnedReader, Container};
111
112    use crate::PackOptions;
113
114    use super::*;
115
116    #[test]
117    pub(crate) fn test_pack_unpack() {
118        let file = include_bytes!("../test/fixtures/bindings-package-test-3.webc");
119        let tmp = TempDir::new().unwrap();
120        let tmp_tar_gz_folder = tmp.path();
121        let webc_sourcefile = tmp_tar_gz_folder.join("webc-source.webc");
122        let webc_targetfile = tmp_tar_gz_folder.join("webc-target.webc");
123        let out = tmp_tar_gz_folder.join("unpacked");
124        let _ = std::fs::create_dir_all(&out);
125
126        std::fs::write(&webc_sourcefile, file).unwrap();
127
128        let unpack_options = UnpackOptions {
129            infile: webc_sourcefile.clone(),
130            out: out.clone(),
131        }
132        .run()
133        .unwrap();
134
135        let unpack_options = PackOptions {
136            inpath: out.clone(),
137            out: webc_targetfile.clone(),
138        }
139        .run()
140        .unwrap();
141
142        let loaded_source = std::fs::read(&webc_sourcefile).unwrap();
143        let loaded_target = std::fs::read(&webc_targetfile).unwrap();
144
145        // Note: we can unpack WEBC v1 and v2, but packing will always generate
146        // a WEBC v2.
147        let webc_source = Container::from_disk(&webc_sourcefile).unwrap();
148        let webc_target = Container::from_disk(webc_targetfile).unwrap();
149
150        let source_atoms = webc_source.atoms();
151        let target_atoms = webc_target.atoms();
152
153        pretty_assertions::assert_eq!(
154            source_atoms.keys().collect::<BTreeSet<_>>(),
155            target_atoms.keys().collect::<BTreeSet<_>>()
156        );
157
158        let source_volumes = webc_source.volumes();
159        let target_volumes = webc_target.volumes();
160
161        pretty_assertions::assert_eq!(
162            source_volumes.keys().collect::<BTreeSet<_>>(),
163            target_volumes.keys().collect::<BTreeSet<_>>()
164        );
165
166        for (k, source_volume) in &source_volumes {
167            assert_eq!(
168                file_names(source_volume),
169                file_names(&target_volumes[k]),
170                "Volume {k:?} mismatch",
171            );
172        }
173    }
174}
175
176fn file_names(volume: &webc::compat::Volume) -> BTreeSet<String> {
177    fn read_dir(
178        volume: &webc::compat::Volume,
179        path: &mut PathSegments,
180        filenames: &mut BTreeSet<String>,
181    ) {
182        let entries = volume.read_dir(&*path).unwrap();
183
184        for (segment, meta) in entries {
185            path.push(segment.clone());
186            filenames.insert(path.to_string());
187
188            if let webc::compat::Metadata::Dir = meta {
189                read_dir(volume, path, filenames);
190            }
191
192            path.pop();
193        }
194    }
195
196    let mut filenames = BTreeSet::new();
197    let mut path = PathSegments::ROOT;
198
199    read_dir(volume, &mut path, &mut filenames);
200
201    filenames
202}