haste_fhir_package/
tarball.rs1use crate::{Manifest, Package, PackageError, PackageFile};
2use flate2::read::GzDecoder;
3use std::{
4 io::{self, Read},
5 path::{Component, Path},
6};
7
8#[derive(Debug, Clone, Copy)]
11pub struct Limits {
12 pub max_uncompressed_bytes: u64,
15 pub max_files: usize,
17}
18
19struct Bounded<R> {
22 inner: R,
23 remaining: u64,
24 exceeded: bool,
25}
26
27impl<R: Read> Read for Bounded<R> {
28 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
29 if self.remaining == 0 {
30 let mut probe = [0u8; 1];
32 if self.inner.read(&mut probe)? == 0 {
33 return Ok(0);
34 }
35 self.exceeded = true;
36 return Err(io::Error::other("package exceeds the size limit"));
37 }
38 let max = usize::try_from(self.remaining)
39 .unwrap_or(usize::MAX)
40 .min(buf.len());
41 let read = self.inner.read(&mut buf[..max])?;
42 self.remaining -= read as u64;
43 Ok(read)
44 }
45}
46
47fn components(path: &Path) -> Option<Vec<String>> {
51 let mut parts = Vec::new();
52 for component in path.components() {
53 match component {
54 Component::Normal(part) => parts.push(part.to_string_lossy().into_owned()),
55 Component::CurDir => {}
56 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
57 }
58 }
59 Some(parts)
60}
61
62fn unscoped(name: &str) -> &str {
66 let name = name.trim();
67 match name
68 .strip_prefix('@')
69 .and_then(|scoped| scoped.split_once('/'))
70 {
71 Some((_, name)) => name,
72 None => name,
73 }
74}
75
76pub fn read(bytes: &[u8], limits: &Limits) -> Result<Package, PackageError> {
88 let gzipped = bytes.starts_with(&[0x1f, 0x8b]);
89 let decompressed: Box<dyn Read + '_> = if gzipped {
90 Box::new(GzDecoder::new(bytes))
91 } else {
92 Box::new(bytes)
93 };
94 let mut bounded = Bounded {
95 inner: decompressed,
96 remaining: limits.max_uncompressed_bytes,
97 exceeded: false,
98 };
99
100 let json_files = {
101 let mut archive = tar::Archive::new(&mut bounded);
102 collect_json_files(&mut archive, limits)
103 };
104 let json_files = match json_files {
105 Ok(files) => files,
106 Err(_) if bounded.exceeded => {
107 return Err(PackageError::TooLargeUnpacked(
108 limits.max_uncompressed_bytes,
109 ));
110 }
111 Err(error) => return Err(error),
112 };
113 let unpacked_bytes = limits.max_uncompressed_bytes - bounded.remaining;
114
115 let root = json_files
117 .iter()
118 .filter(|(parts, _)| parts.last().is_some_and(|name| name == "package.json"))
119 .map(|(parts, _)| parts[..parts.len() - 1].to_vec())
120 .min_by_key(Vec::len)
121 .ok_or(PackageError::MissingManifest)?;
122
123 let manifest_path = [root.as_slice(), &["package.json".to_string()]].concat();
124 let mut manifest = json_files
125 .iter()
126 .find(|(parts, _)| *parts == manifest_path)
127 .map(|(_, content)| serde_json::from_slice::<Manifest>(content))
128 .ok_or(PackageError::MissingManifest)?
129 .map_err(|e| PackageError::InvalidManifest(e.to_string()))?;
130 manifest.name = unscoped(&manifest.name).to_string();
131
132 let example_folder = manifest
133 .directories
134 .example
135 .clone()
136 .unwrap_or_else(|| "example".to_string());
137 let example_folder = components(Path::new(&example_folder)).unwrap_or_default();
138 let example_root = [root.as_slice(), example_folder.as_slice()].concat();
139
140 let mut resources = Vec::new();
141 let mut examples = Vec::new();
142 for (parts, content) in json_files {
143 let Some((name, folder)) = parts.split_last() else {
144 continue;
145 };
146 if folder == root.as_slice() {
147 if name == "package.json" || name.starts_with('.') {
148 continue;
149 }
150 resources.push(PackageFile {
151 path: name.clone(),
152 content,
153 });
154 } else if !example_folder.is_empty() && folder == example_root.as_slice() {
155 if name.starts_with('.') {
156 continue;
157 }
158 examples.push(PackageFile {
159 path: [example_folder.as_slice(), std::slice::from_ref(name)]
160 .concat()
161 .join("/"),
162 content,
163 });
164 }
165 }
166
167 Ok(Package {
168 manifest,
169 resources,
170 examples,
171 unpacked_bytes,
172 })
173}
174
175type ArchiveFile = (Vec<String>, Vec<u8>);
177
178fn collect_json_files<R: Read>(
182 archive: &mut tar::Archive<R>,
183 limits: &Limits,
184) -> Result<Vec<ArchiveFile>, PackageError> {
185 const MAX_DEPTH: usize = 4;
187
188 let unreadable = |e: io::Error| PackageError::Unreadable(e.to_string());
189 let mut files = Vec::new();
190 for (count, entry) in archive.entries().map_err(unreadable)?.enumerate() {
191 if count >= limits.max_files {
192 return Err(PackageError::TooManyFiles(limits.max_files));
193 }
194 let mut entry = entry.map_err(unreadable)?;
195 if !entry.header().entry_type().is_file() {
196 continue;
197 }
198 let Some(parts) = components(&entry.path().map_err(unreadable)?) else {
199 continue;
200 };
201 if parts.len() > MAX_DEPTH
202 || !parts
203 .last()
204 .is_some_and(|name| name.to_ascii_lowercase().ends_with(".json"))
205 {
206 continue;
207 }
208 let mut content = Vec::with_capacity(
209 usize::try_from(entry.header().size().unwrap_or(0))
210 .unwrap_or(0)
211 .min(1 << 20),
212 );
213 entry.read_to_end(&mut content).map_err(unreadable)?;
214 files.push((parts, content));
215 }
216 Ok(files)
217}
218
219pub fn pack(folder: &Path) -> Result<Vec<u8>, PackageError> {
228 let unreadable = |e: io::Error| PackageError::Unreadable(e.to_string());
229 let manifest = std::fs::read(folder.join("package.json")).map_err(|e| match e.kind() {
230 io::ErrorKind::NotFound => PackageError::MissingManifest,
231 _ => unreadable(e),
232 })?;
233 let manifest = serde_json::from_slice::<Manifest>(&manifest)
234 .map_err(|e| PackageError::InvalidManifest(e.to_string()))?;
235 let example_folder = manifest
236 .directories
237 .example
238 .unwrap_or_else(|| "example".to_string());
239
240 let json_files = |relative: &Path| -> Result<Vec<(String, std::path::PathBuf)>, PackageError> {
241 let directory = folder.join(relative);
242 if !directory.is_dir() {
243 return Ok(Vec::new());
244 }
245 let mut files = Vec::new();
246 for entry in std::fs::read_dir(&directory).map_err(unreadable)? {
247 let entry = entry.map_err(unreadable)?;
248 let name = entry.file_name().to_string_lossy().into_owned();
249 if entry.file_type().map_err(unreadable)?.is_file()
250 && name.to_ascii_lowercase().ends_with(".json")
251 {
252 let packed = Path::new("package").join(relative).join(&name);
253 files.push((packed.to_string_lossy().replace('\\', "/"), entry.path()));
254 }
255 }
256 files.sort();
257 Ok(files)
258 };
259 let mut files = json_files(Path::new(""))?;
260 files.extend(json_files(Path::new(&example_folder))?);
261
262 let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
263 Vec::new(),
264 flate2::Compression::default(),
265 ));
266 for (packed, path) in files {
267 let content = std::fs::read(&path).map_err(unreadable)?;
268 let mut header = tar::Header::new_gnu();
269 header.set_size(content.len() as u64);
270 header.set_mode(0o600);
271 builder
272 .append_data(&mut header, packed, content.as_slice())
273 .map_err(unreadable)?;
274 }
275 builder
276 .into_inner()
277 .and_then(flate2::write::GzEncoder::finish)
278 .map_err(unreadable)
279}