Skip to main content

pfx_mod/
manifest.rs

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}