ferrobridge_validator/package/
mod.rs1mod archive;
15
16use std::collections::BTreeMap;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use serde::Deserialize;
22
23const INDEXED: &[&str] = &["StructureDefinition", "ValueSet", "CodeSystem"];
27
28const MANIFEST: &str = "package.json";
30
31const INDEX: &str = ".index.json";
33
34const BOM: &[u8] = b"\xEF\xBB\xBF";
36
37#[derive(Debug, Clone)]
39pub struct Package {
40 name: String,
41 version: String,
42 dependencies: BTreeMap<String, String>,
43 source: PathBuf,
44 resources: Vec<Resource>,
45}
46
47#[derive(Debug, Clone)]
49pub(crate) struct Resource {
50 pub(crate) file: String,
52 pub(crate) type_name: String,
54 pub(crate) url: Option<String>,
56 pub(crate) version: Option<String>,
58 pub(crate) content: Content,
60}
61
62#[derive(Debug, Clone)]
64pub(crate) enum Content {
65 Bytes(Arc<[u8]>),
67 File(PathBuf),
69}
70
71impl Content {
72 pub(crate) fn read(&self) -> std::io::Result<Arc<[u8]>> {
74 match self {
75 Self::Bytes(bytes) => Ok(Arc::clone(bytes)),
76 Self::File(path) => Ok(Arc::from(strip_bom(&std::fs::read(path)?))),
77 }
78 }
79}
80
81#[derive(Debug, thiserror::Error)]
83pub enum PackageError {
84 #[error("cannot read {path}")]
86 Io {
87 path: PathBuf,
89 #[source]
91 source: std::io::Error,
92 },
93 #[error("{path} is neither a .tgz package nor a directory")]
95 NotAPackage {
96 path: PathBuf,
98 },
99 #[error("{path} holds no package/package.json")]
101 NoManifest {
102 path: PathBuf,
104 },
105 #[error("{file} of the package at {path} is not the JSON a package carries")]
107 Json {
108 path: PathBuf,
110 file: String,
112 #[source]
114 source: serde_json::Error,
115 },
116 #[error(".index.json of the package at {path} names {file}, which the package does not hold")]
118 IndexedFileMissing {
119 path: PathBuf,
121 file: String,
123 },
124}
125
126#[derive(Debug, Deserialize)]
128struct Manifest {
129 name: String,
130 version: String,
131 dependencies: Option<BTreeMap<String, String>>,
132}
133
134#[derive(Debug, Deserialize)]
136struct Index {
137 files: Vec<IndexEntry>,
138}
139
140#[derive(Debug, Deserialize)]
142struct IndexEntry {
143 filename: String,
144 #[serde(rename = "resourceType")]
145 type_name: String,
146 url: Option<String>,
147 version: Option<String>,
148}
149
150#[derive(Debug, Deserialize)]
152struct Header {
153 #[serde(rename = "resourceType")]
154 type_name: Option<String>,
155 url: Option<String>,
156 version: Option<String>,
157}
158
159trait Files {
161 fn read(&mut self, file: &str) -> Result<Option<Vec<u8>>, PackageError>;
163
164 fn content(&mut self, file: &str) -> Result<Option<Content>, PackageError>;
167
168 fn names(&mut self) -> Result<Vec<String>, PackageError>;
170}
171
172impl Package {
173 pub fn read(path: &Path) -> Result<Self, PackageError> {
186 let metadata = std::fs::metadata(path).map_err(|source| PackageError::Io {
187 path: path.to_owned(),
188 source,
189 })?;
190 if metadata.is_dir() {
191 let mut files = Directory {
192 root: path.join("package"),
193 };
194 Self::from_files(path, &mut files)
195 } else if metadata.is_file() {
196 let mut files = archive::Archive::read(path)?;
197 Self::from_files(path, &mut files)
198 } else {
199 Err(PackageError::NotAPackage {
200 path: path.to_owned(),
201 })
202 }
203 }
204
205 fn from_files(path: &Path, files: &mut dyn Files) -> Result<Self, PackageError> {
207 let manifest_bytes = files
208 .read(MANIFEST)?
209 .ok_or_else(|| PackageError::NoManifest {
210 path: path.to_owned(),
211 })?;
212 let manifest: Manifest = parse(path, MANIFEST, &manifest_bytes)?;
213 let resources = match files.read(INDEX)? {
214 Some(bytes) => indexed(path, files, &parse::<Index>(path, INDEX, &bytes)?)?,
215 None => scanned(path, files)?,
216 };
217 Ok(Self {
218 name: manifest.name,
219 version: manifest.version,
220 dependencies: manifest.dependencies.unwrap_or_default(),
221 source: path.to_owned(),
222 resources,
223 })
224 }
225
226 #[must_use]
228 pub fn name(&self) -> &str {
229 &self.name
230 }
231
232 #[must_use]
234 pub fn version(&self) -> &str {
235 &self.version
236 }
237
238 #[must_use]
240 pub const fn dependencies(&self) -> &BTreeMap<String, String> {
241 &self.dependencies
242 }
243
244 #[must_use]
246 pub fn source(&self) -> &Path {
247 &self.source
248 }
249
250 #[must_use]
253 pub fn structure_definitions(&self) -> usize {
254 self.resources
255 .iter()
256 .filter(|resource| resource.type_name == "StructureDefinition")
257 .count()
258 }
259
260 pub(crate) fn resources(&self) -> &[Resource] {
262 &self.resources
263 }
264}
265
266fn indexed(
271 path: &Path,
272 files: &mut dyn Files,
273 index: &Index,
274) -> Result<Vec<Resource>, PackageError> {
275 let mut resources = Vec::new();
276 for entry in &index.files {
277 if !INDEXED.contains(&entry.type_name.as_str()) {
278 continue;
279 }
280 let Some(content) = files.content(&entry.filename)? else {
281 return Err(PackageError::IndexedFileMissing {
282 path: path.to_owned(),
283 file: entry.filename.clone(),
284 });
285 };
286 resources.push(Resource {
287 file: entry.filename.clone(),
288 type_name: entry.type_name.clone(),
289 url: entry.url.clone(),
290 version: entry.version.clone(),
291 content,
292 });
293 }
294 Ok(resources)
295}
296
297fn scanned(path: &Path, files: &mut dyn Files) -> Result<Vec<Resource>, PackageError> {
300 let mut resources = Vec::new();
301 for file in files.names()? {
302 if file == MANIFEST || file == INDEX {
303 continue;
304 }
305 let Some(bytes) = files.read(&file)? else {
306 continue;
307 };
308 let header: Header = parse(path, &file, &bytes)?;
309 let Some(type_name) = header.type_name else {
310 continue;
311 };
312 if !INDEXED.contains(&type_name.as_str()) {
313 continue;
314 }
315 resources.push(Resource {
316 file,
317 type_name,
318 url: header.url,
319 version: header.version,
320 content: Content::Bytes(Arc::from(strip_bom(&bytes))),
321 });
322 }
323 Ok(resources)
324}
325
326pub(crate) fn strip_bom(bytes: &[u8]) -> &[u8] {
331 bytes.strip_prefix(BOM).unwrap_or(bytes)
332}
333
334fn parse<T: serde::de::DeserializeOwned>(
336 path: &Path,
337 file: &str,
338 bytes: &[u8],
339) -> Result<T, PackageError> {
340 serde_json::from_slice(strip_bom(bytes)).map_err(|source| PackageError::Json {
341 path: path.to_owned(),
342 file: file.to_owned(),
343 source,
344 })
345}
346
347struct Directory {
349 root: PathBuf,
350}
351
352impl Files for Directory {
353 fn read(&mut self, file: &str) -> Result<Option<Vec<u8>>, PackageError> {
354 let path = self.root.join(file);
355 match std::fs::read(&path) {
356 Ok(bytes) => Ok(Some(bytes)),
357 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
358 Err(source) => Err(PackageError::Io { path, source }),
359 }
360 }
361
362 fn content(&mut self, file: &str) -> Result<Option<Content>, PackageError> {
363 let path = self.root.join(file);
364 match std::fs::metadata(&path) {
365 Ok(metadata) if !metadata.is_dir() => Ok(Some(Content::File(path))),
366 Ok(_) => Ok(None),
367 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
368 Err(source) => Err(PackageError::Io { path, source }),
369 }
370 }
371
372 fn names(&mut self) -> Result<Vec<String>, PackageError> {
373 let io = |source| PackageError::Io {
374 path: self.root.clone(),
375 source,
376 };
377 let mut names = Vec::new();
378 for entry in std::fs::read_dir(&self.root).map_err(io)? {
379 let entry = entry.map_err(io)?;
380 if entry.file_type().map_err(io)?.is_dir() {
381 continue;
382 }
383 if let Some(name) = entry.file_name().to_str()
384 && Path::new(name)
385 .extension()
386 .is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
387 {
388 names.push(name.to_owned());
389 }
390 }
391 names.sort();
392 Ok(names)
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::strip_bom;
399
400 #[test]
401 fn a_leading_byte_order_mark_is_stripped() {
402 assert_eq!(strip_bom(b"\xEF\xBB\xBF{}"), b"{}");
403 assert_eq!(strip_bom(b"{}"), b"{}");
404 }
405}