1use std::cell::Cell;
39use std::fs::File;
40use std::io::Read;
41use std::path::{Path, PathBuf};
42
43use serde_json::{Map, Value};
44
45use crate::error::MicroError;
46use crate::image::sniff;
47use crate::model::{
48 AssetKind, CellSize, ImageFormat, ImageRef, Layer, MicroAsset, Origin, PackageLocation,
49 Variants,
50};
51use crate::name::check_name;
52
53pub const MANIFEST: &str = "manifest.json";
55pub const PACK_INDEX: &str = "pack.json";
57pub const SCHEMA_VERSION: u64 = 1;
59
60#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Limits {
64 pub max_archive_bytes: u64,
66 pub max_file_bytes: u64,
68 pub max_manifest_bytes: u64,
70 pub max_entries: usize,
72 pub max_total_bytes: u64,
74 pub max_dimension: u32,
76 pub max_frames: u32,
78 pub max_decoded_bytes: u64,
80 pub max_packages: usize,
82}
83
84impl Default for Limits {
85 fn default() -> Self {
86 Limits {
87 max_archive_bytes: 8 << 20,
88 max_file_bytes: 2 << 20,
89 max_manifest_bytes: 64 << 10,
90 max_entries: 512,
91 max_total_bytes: 16 << 20,
92 max_dimension: 256,
93 max_frames: 240,
94 max_decoded_bytes: 32 << 20,
95 max_packages: 1024,
96 }
97 }
98}
99
100#[derive(Clone, Debug)]
103pub struct Pack {
104 pub name: String,
105 pub version: Option<String>,
106 pub assets: Vec<MicroAsset>,
107 pub rejected: Vec<(String, MicroError)>,
109}
110
111#[derive(Clone, Debug)]
113pub enum Loaded {
114 Package(Box<MicroAsset>),
115 Pack(Pack),
116}
117
118pub fn safe_relative(path: &str) -> Result<String, MicroError> {
123 let unsafe_path = |why: &str| MicroError::UnsafePath(format!("unsafe path {path:?}: {why}"));
124 if path.is_empty() {
125 return Err(unsafe_path("empty"));
126 }
127 if path.starts_with('/') {
128 return Err(unsafe_path("absolute"));
129 }
130 if path.contains('\\') || path.contains('\0') || path.contains(':') {
131 return Err(unsafe_path("backslashes, colons and NULs are not allowed"));
132 }
133 if path.chars().any(char::is_control) {
136 return Err(unsafe_path("control characters are not allowed"));
137 }
138 let trimmed = path.strip_suffix('/').unwrap_or(path);
139 for segment in trimmed.split('/') {
140 match segment {
141 "" | "." => return Err(unsafe_path("empty or `.` segment")),
142 ".." => return Err(unsafe_path("`..` leads out of the package")),
143 _ => {}
144 }
145 }
146 Ok(trimmed.to_string())
147}
148
149fn io_error(path: &Path, error: impl std::fmt::Display) -> MicroError {
150 MicroError::Io(format!("{}: {error}", path.display()))
151}
152
153fn not_a_file(path: &Path) -> MicroError {
155 MicroError::Io(format!(
156 "{} is not a regular file or a directory",
157 path.display()
158 ))
159}
160
161fn read_bounded(mut reader: impl Read, limit: u64, what: &str) -> Result<Vec<u8>, MicroError> {
163 let mut out = Vec::new();
164 reader
165 .by_ref()
166 .take(limit + 1)
167 .read_to_end(&mut out)
168 .map_err(|e| MicroError::Io(format!("{what}: {e}")))?;
169 if out.len() as u64 > limit {
170 return Err(MicroError::Limit(format!(
171 "{what} is larger than {limit} bytes"
172 )));
173 }
174 Ok(out)
175}
176
177enum Source {
179 Directory {
180 root: PathBuf,
181 canonical: PathBuf,
182 },
183 Archive {
184 path: PathBuf,
185 archive: Box<zip::ZipArchive<File>>,
186 read_total: Cell<u64>,
187 },
188 Embedded {
190 root: String,
191 },
192}
193
194impl Source {
195 fn directory(root: &Path) -> Result<Self, MicroError> {
196 let canonical = root.canonicalize().map_err(|e| io_error(root, e))?;
197 Ok(Source::Directory {
198 root: root.to_path_buf(),
199 canonical,
200 })
201 }
202
203 fn archive(path: &Path, limits: &Limits) -> Result<Self, MicroError> {
206 let metadata = std::fs::metadata(path).map_err(|e| io_error(path, e))?;
207 if !metadata.is_file() {
208 return Err(not_a_file(path));
209 }
210 if metadata.len() > limits.max_archive_bytes {
211 return Err(MicroError::Limit(format!(
212 "{} is larger than {} bytes",
213 path.display(),
214 limits.max_archive_bytes
215 )));
216 }
217 let file = File::open(path).map_err(|e| io_error(path, e))?;
218 if !file.metadata().is_ok_and(|m| m.is_file()) {
220 return Err(not_a_file(path));
221 }
222 let mut archive = zip::ZipArchive::new(file).map_err(|e| io_error(path, e))?;
223 if archive.len() > limits.max_entries {
224 return Err(MicroError::Limit(format!(
225 "{} has more than {} entries",
226 path.display(),
227 limits.max_entries
228 )));
229 }
230 let mut seen = std::collections::HashSet::new();
231 for index in 0..archive.len() {
232 let entry = archive.by_index_raw(index).map_err(|e| io_error(path, e))?;
233 let name = entry.name().map_err(|e| io_error(path, e))?.into_owned();
234 let normal = safe_relative(&name)
235 .map_err(|e| MicroError::UnsafePath(format!("{}: {e}", path.display())))?;
236 if entry.is_symlink() {
237 return Err(MicroError::UnsafePath(format!(
238 "{}: {name:?} is a symbolic link",
239 path.display()
240 )));
241 }
242 if entry.encrypted() {
243 return Err(MicroError::Manifest(format!(
244 "{}: {name:?} is encrypted",
245 path.display()
246 )));
247 }
248 if !entry.is_dir() && entry.size() > limits.max_file_bytes {
249 return Err(MicroError::Limit(format!(
250 "{}: {name:?} is larger than {} bytes",
251 path.display(),
252 limits.max_file_bytes
253 )));
254 }
255 if !seen.insert(normal) {
256 return Err(MicroError::Manifest(format!(
257 "{}: {name:?} appears twice",
258 path.display()
259 )));
260 }
261 }
262 Ok(Source::Archive {
263 path: path.to_path_buf(),
264 archive: Box::new(archive),
265 read_total: Cell::new(0),
266 })
267 }
268
269 fn display(&self, relative: &str) -> String {
270 match self {
271 Source::Directory { root, .. } => root.join(relative).display().to_string(),
272 Source::Archive { path, .. } => format!("{}!{relative}", path.display()),
273 Source::Embedded { root } => format!("builtin:{}", join(root, relative)),
274 }
275 }
276
277 fn exists(&mut self, relative: &str) -> bool {
278 match self {
279 Source::Directory { root, .. } => root.join(relative).is_file(),
280 Source::Archive { archive, .. } => archive.index_for_name(relative).is_some(),
281 Source::Embedded { root } => crate::builtin::file(&join(root, relative)).is_some(),
282 }
283 }
284
285 fn is_dir(&mut self, relative: &str) -> bool {
286 match self {
287 Source::Directory { root, .. } => root.join(relative).is_dir(),
288 Source::Archive { archive, .. } => {
289 let prefix = format!("{relative}/");
290 archive
291 .file_names()
292 .any(|name| name.is_ok_and(|name| name.starts_with(&prefix)))
293 }
294 Source::Embedded { root } => crate::builtin::is_dir(&join(root, relative)),
295 }
296 }
297
298 fn read(&mut self, relative: &str, limit: u64, limits: &Limits) -> Result<Vec<u8>, MicroError> {
300 let shown = self.display(relative);
301 match self {
302 Source::Directory { root, canonical } => {
303 let path = root.join(relative);
304 let resolved = path.canonicalize().map_err(|e| io_error(&path, e))?;
305 if !resolved.starts_with(&*canonical) {
306 return Err(MicroError::UnsafePath(format!(
307 "{shown} links out of the package"
308 )));
309 }
310 let metadata = std::fs::metadata(&resolved).map_err(|e| io_error(&path, e))?;
311 if !metadata.is_file() {
312 return Err(MicroError::Io(format!("{shown} is not a file")));
313 }
314 if metadata.len() > limit {
315 return Err(MicroError::Limit(format!(
316 "{shown} is larger than {limit} bytes"
317 )));
318 }
319 let file = File::open(&resolved).map_err(|e| io_error(&path, e))?;
320 read_bounded(file, limit, &shown)
321 }
322 Source::Archive {
323 archive,
324 read_total,
325 ..
326 } => {
327 let entry = archive
328 .by_name(relative)
329 .map_err(|e| MicroError::Io(format!("{shown}: {e}")))?;
330 if !entry.is_file() {
331 return Err(MicroError::Io(format!("{shown} is not a file")));
332 }
333 let left = limits.max_total_bytes.saturating_sub(read_total.get());
334 let bytes = read_bounded(entry, limit.min(left), &shown).map_err(|e| {
335 if limit > left {
336 MicroError::Limit(format!(
337 "{shown}: the archive holds more than {} bytes uncompressed",
338 limits.max_total_bytes
339 ))
340 } else {
341 e
342 }
343 })?;
344 read_total.set(read_total.get() + bytes.len() as u64);
345 Ok(bytes)
346 }
347 Source::Embedded { root } => {
348 let bytes = crate::builtin::file(&join(root, relative))
349 .ok_or_else(|| MicroError::Io(format!("{shown}: no such file")))?;
350 if bytes.len() as u64 > limit {
351 return Err(MicroError::Limit(format!(
352 "{shown} is larger than {limit} bytes"
353 )));
354 }
355 Ok(bytes.to_vec())
356 }
357 }
358 }
359
360 fn location(&self, prefix: &str) -> PackageLocation {
361 match self {
362 Source::Directory { root, .. } => PackageLocation::Directory(if prefix.is_empty() {
363 root.clone()
364 } else {
365 root.join(prefix)
366 }),
367 Source::Archive { path, .. } => PackageLocation::Archive {
368 path: path.clone(),
369 prefix: prefix.to_string(),
370 },
371 Source::Embedded { root } => PackageLocation::BuiltIn(join(root, prefix)),
372 }
373 }
374}
375
376fn join(prefix: &str, relative: &str) -> String {
377 if prefix.is_empty() {
378 relative.to_string()
379 } else {
380 format!("{prefix}/{relative}")
381 }
382}
383
384pub(crate) fn read_file(
387 location: &PackageLocation,
388 relative: &str,
389 limits: &Limits,
390) -> Result<Vec<u8>, MicroError> {
391 let relative = safe_relative(relative)?;
392 match location {
393 PackageLocation::Directory(root) => {
394 Source::directory(root)?.read(&relative, limits.max_file_bytes, limits)
395 }
396 PackageLocation::Archive { path, prefix } => Source::archive(path, limits)?.read(
397 &join(prefix, &relative),
398 limits.max_file_bytes,
399 limits,
400 ),
401 PackageLocation::BuiltIn(prefix) => Source::Embedded {
402 root: prefix.clone(),
403 }
404 .read(&relative, limits.max_file_bytes, limits),
405 }
406}
407
408pub(crate) fn load_embedded_pack(
410 root: &str,
411 layer: Layer,
412 limits: &Limits,
413) -> Result<Pack, MicroError> {
414 let mut source = Source::Embedded {
415 root: root.to_string(),
416 };
417 read_pack(&mut source, None, layer, limits)
418}
419
420fn parse_json(bytes: &[u8], what: &str) -> Result<Map<String, Value>, MicroError> {
421 match serde_json::from_slice::<Value>(bytes) {
422 Ok(Value::Object(map)) => Ok(map),
423 Ok(_) => Err(MicroError::Manifest(format!("{what}: not a JSON object"))),
424 Err(e) => Err(MicroError::Manifest(format!("{what}: invalid JSON: {e}"))),
425 }
426}
427
428fn check_schema(map: &Map<String, Value>, what: &str) -> Result<(), MicroError> {
429 match map.get("schema_version") {
430 Some(Value::Number(n)) if n.as_u64() == Some(SCHEMA_VERSION) => Ok(()),
431 Some(other) => Err(MicroError::Manifest(format!(
432 "{what}: unsupported schema_version {other}; this release reads {SCHEMA_VERSION}"
433 ))),
434 None => Err(MicroError::Manifest(format!(
435 "{what}: missing schema_version"
436 ))),
437 }
438}
439
440fn check_keys(map: &Map<String, Value>, allowed: &[&str], what: &str) -> Result<(), MicroError> {
441 match map.keys().find(|key| !allowed.contains(&key.as_str())) {
442 Some(key) => Err(MicroError::Manifest(format!(
443 "{what}: unknown field {key:?}"
444 ))),
445 None => Ok(()),
446 }
447}
448
449fn string<'a>(
450 map: &'a Map<String, Value>,
451 key: &str,
452 what: &str,
453) -> Result<Option<&'a str>, MicroError> {
454 match map.get(key) {
455 None | Some(Value::Null) => Ok(None),
456 Some(Value::String(s)) => Ok(Some(s)),
457 Some(_) => Err(MicroError::Manifest(format!(
458 "{what}: {key:?} must be a string"
459 ))),
460 }
461}
462
463fn plain<'a>(
467 map: &'a Map<String, Value>,
468 key: &str,
469 what: &str,
470) -> Result<Option<&'a str>, MicroError> {
471 let value = string(map, key, what)?;
472 if value.is_some_and(|value| value.chars().any(char::is_control)) {
473 return Err(MicroError::Manifest(format!(
474 "{what}: {key:?} holds control characters"
475 )));
476 }
477 Ok(value)
478}
479
480fn required<'a>(map: &'a Map<String, Value>, key: &str, what: &str) -> Result<&'a str, MicroError> {
481 string(map, key, what)?.ok_or_else(|| MicroError::Manifest(format!("{what}: missing {key:?}")))
482}
483
484fn strings(map: &Map<String, Value>, key: &str, what: &str) -> Result<Vec<String>, MicroError> {
485 match map.get(key) {
486 None | Some(Value::Null) => Ok(Vec::new()),
487 Some(Value::Array(items)) => items
488 .iter()
489 .map(|item| match item {
490 Value::String(s) => Ok(s.clone()),
491 _ => Err(MicroError::Manifest(format!(
492 "{what}: {key:?} must be a list of strings"
493 ))),
494 })
495 .collect(),
496 Some(_) => Err(MicroError::Manifest(format!(
497 "{what}: {key:?} must be a list of strings"
498 ))),
499 }
500}
501
502fn size(map: &Map<String, Value>, what: &str) -> Result<CellSize, MicroError> {
503 let in_manifest = |e: MicroError| MicroError::InvalidSize(format!("{what}: {e}"));
504 match map.get("size") {
505 None | Some(Value::Null) => Ok(CellSize::default()),
506 Some(Value::String(s)) => s.parse().map_err(in_manifest),
507 Some(Value::Object(size)) => {
508 let get = |key: &str| match size.get(key) {
509 Some(Value::Number(n)) => n
510 .as_u64()
511 .and_then(|n| u16::try_from(n).ok())
512 .ok_or_else(|| {
513 MicroError::InvalidSize(format!("{what}: size.{key} is out of range"))
514 }),
515 None if key == "rows" => Ok(1),
516 _ => Err(MicroError::InvalidSize(format!(
517 "{what}: size.{key} must be a number"
518 ))),
519 };
520 CellSize::new(get("cols")?, get("rows")?).map_err(in_manifest)
521 }
522 Some(_) => Err(MicroError::InvalidSize(format!(
523 "{what}: size must be \"COLSxROWS\" or {{\"cols\": …, \"rows\": …}}"
524 ))),
525 }
526}
527
528const MANIFEST_KEYS: &[&str] = &[
529 "schema_version",
530 "name",
531 "kind",
532 "size",
533 "alt",
534 "fallback",
535 "static",
536 "animation",
537 "frames",
538 "aliases",
539 "version",
540 "license",
541 "author",
542];
543
544fn read_package(
546 source: &mut Source,
547 prefix: &str,
548 origin: Origin,
549 limits: &Limits,
550) -> Result<MicroAsset, MicroError> {
551 let manifest_path = join(prefix, MANIFEST);
552 let what = source.display(&manifest_path);
553 let bytes = source.read(&manifest_path, limits.max_manifest_bytes, limits)?;
554 let map = parse_json(&bytes, &what)?;
555 check_schema(&map, &what)?;
556 check_keys(&map, MANIFEST_KEYS, &what)?;
557
558 let name = required(&map, "name", &what)?;
559 let alt = required(&map, "alt", &what)?;
560 let in_manifest = |e: MicroError| match e {
561 MicroError::InvalidName(_) => e,
562 other => MicroError::Manifest(format!("{what}: {other}")),
563 };
564 let mut asset = MicroAsset::new(name, alt)
565 .map_err(in_manifest)?
566 .with_size(size(&map, &what)?)
567 .map_err(in_manifest)?;
568 match map.get("fallback") {
569 None | Some(Value::Null) => {}
570 Some(Value::Object(fallback)) => {
571 check_keys(fallback, &["emoji", "text"], &format!("{what}: fallback"))?;
572 if let Some(emoji) = string(fallback, "emoji", &what)? {
573 asset = asset.with_emoji(emoji).map_err(in_manifest)?;
574 }
575 if let Some(text) = string(fallback, "text", &what)? {
576 asset = asset.with_text(text).map_err(in_manifest)?;
577 }
578 }
579 Some(_) => {
580 return Err(MicroError::Manifest(format!(
581 "{what}: fallback must be an object with \"emoji\" and/or \"text\""
582 )));
583 }
584 }
585 for alias in strings(&map, "aliases", &what)? {
586 asset = asset.with_alias(&alias).map_err(in_manifest)?;
587 }
588 if let Some(version) = plain(&map, "version", &what)? {
589 asset = asset.with_version(version);
590 }
591 if let Some(license) = plain(&map, "license", &what)? {
592 asset = asset.with_license(license);
593 }
594 if let Some(author) = plain(&map, "author", &what)? {
595 asset = asset.with_author(author);
596 }
597
598 let mut load = |relative: &str, animation: bool| -> Result<ImageRef, MicroError> {
600 let relative = safe_relative(relative)?;
601 let full = join(prefix, &relative);
602 let bytes = source.read(&full, limits.max_file_bytes, limits)?;
603 let shown = source.display(&full);
604 let info = sniff(&bytes, limits.max_frames)
605 .map_err(|e| MicroError::Image(format!("{shown}: {e}")))?;
606 if info.width > limits.max_dimension || info.height > limits.max_dimension {
607 return Err(MicroError::Limit(format!(
608 "{shown} is {}x{} pixels; the limit is {} on each side",
609 info.width, info.height, limits.max_dimension
610 )));
611 }
612 if info.frames > limits.max_frames {
613 return Err(MicroError::Limit(format!(
614 "{shown} has more than {} frames",
615 limits.max_frames
616 )));
617 }
618 if !animation && info.frames > 1 {
619 return Err(MicroError::Image(format!(
620 "{shown} is animated; the static image and frames must be still images"
621 )));
622 }
623 if animation && info.format == ImageFormat::Png {
624 return Err(MicroError::Image(format!(
625 "{shown} is a still PNG; an animation is a GIF, APNG or WebP"
626 )));
627 }
628 Ok(ImageRef {
629 path: relative,
630 info,
631 bytes: bytes.len() as u64,
632 })
633 };
634 let static_path = required(&map, "static", &what)?;
635 let static_image = load(static_path, false)?;
636 let animation = string(&map, "animation", &what)?
637 .map(|path| load(path, true))
638 .transpose()?;
639 let frame_paths = strings(&map, "frames", &what)?;
640 if frame_paths.len() as u64 > limits.max_frames as u64 {
641 return Err(MicroError::Limit(format!(
642 "{what}: more than {} frames",
643 limits.max_frames
644 )));
645 }
646 let frames = frame_paths
647 .iter()
648 .map(|path| load(path, false))
649 .collect::<Result<Vec<_>, _>>()?;
650 let variants = Variants {
651 static_image: Some(static_image),
652 animation,
653 frames,
654 };
655 let total_frames: u64 = variants
656 .iter()
657 .map(|image| image.info.frames as u64)
658 .sum::<u64>()
659 - 1;
660 if total_frames > limits.max_frames as u64 {
661 return Err(MicroError::Limit(format!(
662 "{what}: more than {} frames in all",
663 limits.max_frames
664 )));
665 }
666 let decoded: u64 = variants
667 .iter()
668 .map(|image| image.info.decoded_bytes())
669 .sum();
670 if decoded > limits.max_decoded_bytes {
671 return Err(MicroError::Limit(format!(
672 "{what}: its images take {decoded} bytes decoded; the limit is {}",
673 limits.max_decoded_bytes
674 )));
675 }
676
677 let kind = match string(&map, "kind", &what)? {
678 Some(kind) => kind.parse().map_err(in_manifest)?,
679 None if variants.animation.is_some() || !variants.frames.is_empty() => AssetKind::Animated,
680 None => AssetKind::Static,
681 };
682 let asset = asset
683 .with_kind(kind)
684 .with_variants(variants)
685 .with_origin(Origin {
686 location: Some(source.location(prefix)),
687 ..origin
688 });
689 asset.validate().map_err(in_manifest)?;
690 Ok(asset)
691}
692
693const PACK_KEYS: &[&str] = &[
694 "schema_version",
695 "name",
696 "version",
697 "description",
698 "license",
699 "author",
700 "packages",
701];
702
703fn read_pack(
705 source: &mut Source,
706 pack_dir: Option<&Path>,
707 layer: Layer,
708 limits: &Limits,
709) -> Result<Pack, MicroError> {
710 let what = source.display(PACK_INDEX);
711 let bytes = source.read(PACK_INDEX, limits.max_manifest_bytes, limits)?;
712 let map = parse_json(&bytes, &what)?;
713 check_schema(&map, &what)?;
714 check_keys(&map, PACK_KEYS, &what)?;
715 let name = required(&map, "name", &what)?;
716 check_name(name)?;
717 for key in ["description", "license", "author"] {
718 plain(&map, key, &what)?;
719 }
720 let packages = strings(&map, "packages", &what)?;
721 if packages.len() > limits.max_packages {
722 return Err(MicroError::Limit(format!(
723 "{what}: more than {} packages",
724 limits.max_packages
725 )));
726 }
727 let mut pack = Pack {
728 name: name.to_string(),
729 version: plain(&map, "version", &what)?.map(str::to_string),
730 assets: Vec::new(),
731 rejected: Vec::new(),
732 };
733 for package in packages {
734 let origin = Origin {
735 layer,
736 pack: Some(pack.name.clone()),
737 location: None,
738 };
739 let result = safe_relative(&package).and_then(|relative| {
740 if source.is_dir(&relative) {
741 return read_package(source, &relative, origin, limits);
742 }
743 match (pack_dir, source.exists(&relative)) {
746 (Some(dir), true) => {
747 let path = dir.join(&relative);
748 let canonical_dir = dir.canonicalize().map_err(|e| io_error(dir, e))?;
750 let resolved = path.canonicalize().map_err(|e| io_error(&path, e))?;
751 if !resolved.starts_with(&canonical_dir) {
752 return Err(MicroError::UnsafePath(format!(
753 "{} links out of the pack",
754 path.display()
755 )));
756 }
757 let mut inner = Source::archive(&path, limits)?;
758 read_package(&mut inner, "", origin, limits)
759 }
760 _ => Err(MicroError::Io(format!(
761 "{}: no such package",
762 source.display(&relative)
763 ))),
764 }
765 });
766 match result {
767 Ok(asset) => pack.assets.push(asset),
768 Err(error) => pack.rejected.push((package, error)),
769 }
770 }
771 Ok(pack)
772}
773
774pub fn load_package(path: &Path, layer: Layer, limits: &Limits) -> Result<MicroAsset, MicroError> {
776 match load(path, layer, limits)? {
777 Loaded::Package(asset) => Ok(*asset),
778 Loaded::Pack(_) => Err(MicroError::Manifest(format!(
779 "{} is a pack, not a package",
780 path.display()
781 ))),
782 }
783}
784
785pub fn load_pack(path: &Path, layer: Layer, limits: &Limits) -> Result<Pack, MicroError> {
787 match load(path, layer, limits)? {
788 Loaded::Pack(pack) => Ok(pack),
789 Loaded::Package(_) => Err(MicroError::Manifest(format!(
790 "{} is a package, not a pack",
791 path.display()
792 ))),
793 }
794}
795
796pub fn load(path: &Path, layer: Layer, limits: &Limits) -> Result<Loaded, MicroError> {
799 let metadata = std::fs::symlink_metadata(path).map_err(|e| io_error(path, e))?;
800 let metadata = if metadata.file_type().is_symlink() {
801 std::fs::metadata(path).map_err(|e| io_error(path, e))?
802 } else {
803 metadata
804 };
805 let is_dir = metadata.is_dir();
806 if !is_dir && !metadata.is_file() {
809 return Err(not_a_file(path));
810 }
811 let mut source = if is_dir {
812 Source::directory(path)?
813 } else {
814 Source::archive(path, limits)?
815 };
816 let origin = Origin {
817 layer,
818 pack: None,
819 location: None,
820 };
821 if source.exists(MANIFEST) {
822 read_package(&mut source, "", origin, limits).map(|asset| Loaded::Package(Box::new(asset)))
823 } else if source.exists(PACK_INDEX) {
824 read_pack(&mut source, is_dir.then_some(path), layer, limits).map(Loaded::Pack)
825 } else {
826 Err(MicroError::Manifest(format!(
827 "{}: no {MANIFEST} or {PACK_INDEX}",
828 path.display()
829 )))
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn relative_paths() {
839 assert_eq!(safe_relative("a/b.png").unwrap(), "a/b.png");
840 assert_eq!(safe_relative("frames/").unwrap(), "frames");
841 for bad in [
842 "",
843 "/etc/passwd",
844 "../x",
845 "a/../../x",
846 "a/./b",
847 "a//b",
848 "C:/x",
849 "a\\b",
850 "a\0",
851 "a\u{1b}]0;title\u{7}",
852 "a\u{7f}",
853 "a\u{9b}2J",
854 "a\u{85}b",
855 "a\tb",
856 ] {
857 assert!(
858 matches!(safe_relative(bad), Err(MicroError::UnsafePath(_))),
859 "{bad:?}"
860 );
861 }
862 }
863}