use std::io::{Cursor, Read};
use serde::Deserialize;
const MANIFEST_CAP: u64 = 64 * 1024;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Manifest {
#[serde(default)]
pub name: String,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub source_code_url: Option<String>,
}
impl Manifest {
pub fn or_named(mut self, file_name: &str) -> Self {
if self.name.trim().is_empty() {
self.name = file_name.trim_end_matches(".xdc").to_string();
}
self
}
}
pub fn read_manifest(bytes: &[u8]) -> Result<Manifest, String> {
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| format!("Invalid mini app: {e}"))?;
let stored = archive
.file_names()
.find(|n| n.eq_ignore_ascii_case("manifest.toml"))
.map(str::to_string);
let Some(stored) = stored else {
return Ok(Manifest::default());
};
let mut entry = archive.by_name(&stored).map_err(|e| format!("Invalid mini app: {e}"))?;
let mut raw = Vec::new();
(&mut entry).take(MANIFEST_CAP + 1).read_to_end(&mut raw).map_err(|e| e.to_string())?;
if raw.len() as u64 > MANIFEST_CAP {
return Err("Invalid manifest: too large".into());
}
let text = String::from_utf8(raw).map_err(|e| format!("Invalid manifest: {e}"))?;
toml::from_str(&text).map_err(|e| format!("Invalid manifest: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn zip_of(files: &[(&str, &[u8])]) -> Vec<u8> {
let mut out = Cursor::new(Vec::new());
{
let mut z = zip::ZipWriter::new(&mut out);
let opts = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for (name, body) in files {
z.start_file(*name, opts).unwrap();
z.write_all(body).unwrap();
}
z.finish().unwrap();
}
out.into_inner()
}
#[test]
fn it_reads_the_manifest_id_and_name() {
let pkg = zip_of(&[("index.html", b"<html>"), ("manifest.toml", b"name = \"Chess\"\nid = \"chess\"\n")]);
let m = read_manifest(&pkg).unwrap().or_named("fallback.xdc");
assert_eq!((m.name.as_str(), m.id.as_deref()), ("Chess", Some("chess")));
}
#[test]
fn a_package_without_a_name_takes_the_file_name() {
let bare = read_manifest(&zip_of(&[("index.html", b"<html>")])).unwrap();
assert_eq!((bare.name.as_str(), bare.id.as_deref()), ("", None), "the bytes alone name nothing");
assert_eq!(bare.clone().or_named("chess.xdc").name, "chess");
assert_eq!(bare.or_named("notchess.xdc").name, "notchess", "one package, two names");
let unnamed = read_manifest(&zip_of(&[("manifest.toml", b"id = \"chess\"\n")])).unwrap().or_named("c.xdc");
assert_eq!((unnamed.name.as_str(), unnamed.id.as_deref()), ("c", Some("chess")));
}
#[test]
fn an_inflating_manifest_is_refused_without_reading_it_all() {
let bomb = vec![b'#'; 8 * 1024 * 1024];
let pkg = zip_of(&[("manifest.toml", &bomb)]);
assert!(pkg.len() < 64 * 1024, "the bomb is small on the wire");
assert!(read_manifest(&pkg).is_err());
}
#[test]
fn garbage_is_an_error_not_a_panic() {
assert!(read_manifest(b"not a zip").is_err());
}
}