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 #[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 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}