1use serde::Deserialize;
2
3use crate::error::Refusal;
4
5pub const MANIFEST: &str = "mod.toml";
6pub const ID_CHARS: usize = 64;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct Api {
10 pub world: String,
11 pub version: String,
12 pub imports: Vec<String>,
13}
14
15impl Api {
16 pub fn new(world: &str, version: &str) -> Self {
17 Self {
18 world: world.to_string(),
19 version: version.to_string(),
20 imports: Vec::new(),
21 }
22 }
23
24 pub fn import(mut self, name: &str) -> Self {
25 self.imports.push(name.to_string());
26 self
27 }
28
29 pub fn accepts(&self, wanted: &str) -> Result<(), Refusal> {
30 let refuse = || Refusal::Api {
31 wanted: wanted.to_string(),
32 world: self.world.clone(),
33 version: self.version.clone(),
34 };
35 let game = semver::Version::parse(&self.version).map_err(|_| refuse())?;
36 let need = semver::Version::parse(wanted).map_err(|_| refuse())?;
37 let req = semver::VersionReq::parse(&format!("^{need}")).map_err(|_| refuse())?;
38 if req.matches(&game) {
39 Ok(())
40 } else {
41 Err(refuse())
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct Manifest {
49 pub id: String,
50 pub version: String,
51 pub api: String,
52 pub entry: String,
53}
54
55impl Manifest {
56 pub fn parse(text: &str) -> Result<Self, Refusal> {
57 let manifest: Self =
58 toml::from_str(text).map_err(|e| Refusal::Manifest(e.message().to_string()))?;
59 if !valid_id(&manifest.id) {
60 return Err(Refusal::Id { found: manifest.id });
61 }
62 if semver::Version::parse(&manifest.version).is_err() {
63 return Err(Refusal::Manifest(format!(
64 "version `{}` is not a semantic version such as 1.0.0",
65 manifest.version
66 )));
67 }
68 if semver::Version::parse(&manifest.api).is_err() {
69 return Err(Refusal::Manifest(format!(
70 "api `{}` is not a semantic version such as 1.0.0",
71 manifest.api
72 )));
73 }
74 if !valid_entry(&manifest.entry) {
75 return Err(Refusal::Entry {
76 found: manifest.entry,
77 });
78 }
79 Ok(manifest)
80 }
81}
82
83pub fn valid_id(id: &str) -> bool {
84 let mut chars = id.chars();
85 let Some(first) = chars.next() else {
86 return false;
87 };
88 id.len() <= ID_CHARS
89 && (first.is_ascii_lowercase() || first.is_ascii_digit())
90 && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
91}
92
93pub fn valid_entry(entry: &str) -> bool {
94 let Some(stem) = entry.strip_suffix(".wasm") else {
95 return false;
96 };
97 !stem.is_empty()
98 && entry.len() <= ID_CHARS + 5
99 && stem
100 .chars()
101 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
102 && !stem.starts_with('.')
103}