eml_validate/
lib.rs

1use serde::{Deserialize, Serialize};
2use anyhow::{anyhow, Result};
3use anyhow::Error;
4use std::{ffi::OsStr, fs, fs::File, io::{Read, Write}, path::{Path, PathBuf}};
5
6const ALLOWED_GAMES: [&str; 3] = ["EM1", "EM2", "EMR"];
7const ALLOWED_PLATFORMS: [&str; 2] = ["WII", "PC"];
8const BANNED_EXTENSIONS: [&str; 6] = ["dll", "so", "exe", "sh", "bat", "scr"]; // not technically
9                                                                               // banned, but will
10                                                                               // require analysis
11                                                                               // by a moderator
12const BANNED_PAK_FILES: [&str; 5] = [
13    "global.utoc",
14    "global.ucas",
15    "recolored-WindowsNoEditor.pak",
16    "recolored-WindowsNoEditor.ucas",
17    "recolored-WindowsNoEditor.utoc",
18];
19pub fn validate(path: &PathBuf, strict: bool) -> Result<ModInfo, Error> {
20    let mut final_mod_info: ModInfo = ModInfo {
21        name: "".to_string(),
22        game: "".to_string(),
23        platform: "".to_string(),
24        description: "".to_string(),
25        short_description: "".to_string(),
26        dependencies: Vec::new(),
27        custom_textures_path: "".to_string(),
28        custom_game_files_path: "".to_string(),
29        scripts_path: "".to_string(),
30        icon_path: "".to_string(),
31        auto_generated_tags: Vec::new(),
32    };
33    println!("{}", &path.display());
34    let mut mod_info_path = path.clone();
35    mod_info_path.push("mod.json");
36
37    let mut mod_description_path = path.clone();
38    mod_description_path.push("description.md");
39
40    if !mod_info_path.exists() {
41        return Err(anyhow!("mod.json does not exist."));
42    }
43    let mut mod_info_file = File::open(mod_info_path)?;
44    let mut mod_info_buffer = String::new();
45    mod_info_file.read_to_string(&mut mod_info_buffer)?;
46    let mod_info: serde_json::Map<String, serde_json::Value> =
47        serde_json::from_str(&mod_info_buffer)?;
48
49    let name = mod_info.get("name").unwrap().as_str().unwrap();
50    println!("{}", name);
51    if name.trim().is_empty() {
52        return Err(anyhow!("mod name is empty."));
53    }
54
55    final_mod_info.name = name.to_string();
56
57    let short_description_value = mod_info.get("shortdescription");
58    let mut no_short_description = false;
59
60    match short_description_value {
61        Some(x) => {
62            let short_description = x.as_str().unwrap().trim().to_string();
63            final_mod_info.short_description = short_description;
64        }
65        None => no_short_description = true,
66    }
67
68    if mod_description_path.exists() {
69        let mut mod_description_file = File::open(mod_description_path)?;
70        let mut mod_description = String::new();
71
72        mod_description_file.read_to_string(&mut mod_description)?;
73
74        if mod_description.trim().is_empty() {
75            return Err(anyhow!("mod description is empty."));
76       }
77
78        final_mod_info.description = mod_description.trim().to_string();
79
80        if no_short_description {
81            final_mod_info.short_description = "clone".to_string();
82        }
83    }
84
85    let mut game = match mod_info.get("game") {
86        Some(x) => x.as_str().unwrap().to_string().to_uppercase(),
87        None => {
88            "".to_string()
89        }
90    };
91    let mut platform = match mod_info.get("platform") {
92        Some(x) => x.as_str().unwrap().to_string().to_uppercase(),
93        None => {
94            "".to_string()
95        }
96    };
97
98    if platform.trim().is_empty() {
99        if strict {
100            return Err(anyhow!("mod platform is empty."));
101        }
102        else {
103            platform = "WII".to_string();
104        }
105    }
106
107    if game.trim().is_empty() {
108        return Err(anyhow!("mod game type is empty."));
109    }
110
111    println!("{}", platform);
112
113    final_mod_info.game = game.to_string();
114    final_mod_info.platform = platform.to_string();
115
116    println!("{}", game);
117
118    if !ALLOWED_GAMES.contains(&game.as_str()) {
119        return Err(anyhow!("could not recognize defined game."));
120    }
121
122    if !ALLOWED_PLATFORMS.contains(&platform.as_str()) {
123        return Err(anyhow!("could not recognize defined platform."));
124    }
125
126    if game.to_string() == "EMR" && platform.to_string() == "WII" {
127        return Err(anyhow!("impossible combination (emr/wii)"));
128    }
129
130    if game.to_string() == "EM1" && platform.to_string() == "PC" {
131        return Err(anyhow!("impossible combination (em1/pc)"));
132    }
133
134    let mut no_custom_textures = false;
135    let mut no_custom_files = false;
136    let mut no_scripts = false;
137
138    let custom_textures_path = match mod_info.get("custom_textures_path") {
139        Some(x) => x.as_str().unwrap().to_string(),
140        None => {
141            no_custom_textures = true;
142            "".to_string()
143        }
144    };
145
146    let custom_game_files_path = match mod_info.get("custom_game_files_path") {
147        Some(x) => x.as_str().unwrap().to_string(),
148        None => {
149            no_custom_files = true;
150            "".to_string()
151        }
152    };
153
154    let scripts_path = match mod_info.get("scripts_path") {
155        Some(x) => x.as_str().unwrap().to_string(),
156        None => {
157            no_scripts = true;
158            "".to_string()
159        }
160    };
161
162    if strict {
163        if platform == "PC" && !no_custom_textures {
164            return Err(anyhow!("custom textures not allowed on pc."));
165        }
166
167        if (platform != "PC" || game != "EMR") && !no_scripts {
168            return Err(anyhow!("custom scripts only available with EMR"));
169        }
170    }
171    else {
172        if platform == "PC" && !no_custom_textures {
173            no_custom_textures = true
174        }
175
176        if (platform != "PC" || game != "EMR") && !no_scripts {
177            no_scripts = true;
178        }
179    }
180
181    final_mod_info.scripts_path = scripts_path.clone();
182    final_mod_info.custom_textures_path = custom_textures_path.clone();
183    final_mod_info.custom_game_files_path = custom_game_files_path.clone();
184
185    if !no_custom_files {
186        if PathBuf::from(&custom_game_files_path).is_absolute() {
187            return Err(anyhow!("you are not allowed to have absolute paths on custom file path."));
188        }
189
190        if strict {
191            if !PathBuf::from(&path).join(&custom_game_files_path).exists() {
192                return Err(anyhow!("custom game files path does not exist."));
193            }
194            if custom_game_files_path.trim().is_empty() {
195                return Err(anyhow!("custom game files path is empty."));
196            }
197        }
198
199        let pak_path = PathBuf::from(&path).join(custom_game_files_path).join("Paks");
200
201        if platform == "PC" && game == "EMR" && pak_path.exists() {
202            for pak in BANNED_PAK_FILES {
203                let path = pak_path.clone().join(pak);
204                if path.exists() {
205                    return Err(anyhow!(format!("you are not allowed to modify any existing/forbidden PAK files. (global.utoc,global.ucas,recolored-WindowsNoEditor.pak,recolored-WindowsNoEditor.ucas,recolored-WindowsNoEditor.utoc) (VIOLATINGFILE={})", path.display())));
206                }
207            }
208        }
209
210        final_mod_info
211            .auto_generated_tags
212            .push("gamefile-mod".to_string())
213    }
214
215    if !no_custom_textures {
216        if PathBuf::from(&custom_textures_path).is_absolute() {
217            return Err(
218                anyhow!("you are not allowed to have absolute paths on custom textures path."),
219            );
220        }
221
222        if strict {
223            if custom_textures_path.trim().is_empty() {
224                return Err(anyhow!("custom textures path is empty."));
225            }
226            if !PathBuf::from(&path).join(&custom_textures_path).exists() {
227                return Err(anyhow!("custom textures path does not exist."));
228            }
229        }
230
231        final_mod_info
232            .auto_generated_tags
233            .push("texture-mod".to_string())
234    }
235
236    if !no_scripts {
237        if scripts_path.trim().is_empty() {
238            return Err(anyhow!("scripts path is empty."));
239        }
240        if PathBuf::from(&scripts_path).is_absolute() {
241            return Err(anyhow!("you are not allowed to have absolute paths on custom script path."));
242        }
243        if !PathBuf::from(&path).join(&scripts_path).exists() {
244            return Err(anyhow!("custom script path does not exist."));
245        }
246
247        final_mod_info
248            .auto_generated_tags
249            .push("script-mod".to_string())
250    }
251    let icon_path = mod_info.get("icon_path").unwrap().as_str().unwrap();
252
253    final_mod_info.icon_path = icon_path.trim().to_string();
254
255    if icon_path.trim().is_empty() {
256        return Err(anyhow!("mod icon path is empty."));
257    }
258
259    if PathBuf::from(&icon_path).is_absolute() {
260        return Err(anyhow!("you are not allowed to have absolute paths on mod icon."));
261    }
262
263    if PathBuf::from(&icon_path).exists() {
264        return Err(anyhow!("mod icon does not exist."));
265    }
266
267    match mod_info.get("dependencies") {
268        Some(x) => {
269            let array = x.as_array().unwrap();
270            for element in array {
271                let dependency = element.as_str().unwrap().to_string();
272                for char in dependency.trim().chars() {
273                    if !char.is_alphanumeric() {
274                        return Err(anyhow!("only alphanumerics are allowed in dependency list."));
275                    }
276                }
277
278                final_mod_info.dependencies.push(dependency.to_string());
279            }
280        }
281        None => {}
282    };
283
284    for entry in walkdir::WalkDir::new(path).into_iter() {
285        let res = entry?;
286        if res.path().is_dir() {
287            continue;
288        }
289
290        let extension = res.path().extension().unwrap_or_else(|| OsStr::new(""));
291
292        if !extension.is_empty() {
293            let formatted_extension = extension.to_str().unwrap().to_string().to_lowercase();
294            if BANNED_EXTENSIONS.contains(&formatted_extension.as_str()) {
295                return Err(anyhow!(format!("mod contains illegal file ({})", formatted_extension)));
296            }
297        }
298    }
299
300    Ok(final_mod_info)
301}
302
303pub fn generate_project(_game: String, _platform: String, name: String, description: String, path: String) -> Result<()> {
304    println!("Generating Mod");
305    let full_path = PathBuf::from(path);
306
307    let mut mod_info: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
308    let mut meta_file = File::create(Path::new(&full_path).join("mod.json"))?;
309
310    let game = _game.to_uppercase();
311    let platform = _platform.to_uppercase();
312
313    if game == "EM1" && platform == "PC" {
314        return Err(anyhow!(
315            "impossible combination (EM1/PC)",
316        ));
317    }
318
319    if game == "EMR" && platform == "WII" {
320        return Err(anyhow!(
321            "impossible combination (EMR/WII)",
322        ));
323    }
324
325    mod_info.insert("name".to_string(), serde_json::Value::String(name.clone()));
326    mod_info.insert("short_description".to_string(), serde_json::Value::String("Generated with EML-Validate".to_string()));
327    mod_info.insert("game".to_string(), serde_json::Value::String(game.clone()));
328    mod_info.insert("platform".to_string(), serde_json::Value::String(platform.clone()));
329    mod_info.insert("custom_game_files".to_string(),  serde_json::Value::String("files".to_string()));
330    mod_info.insert("icon_path".to_string(),  serde_json::Value::String("icon.png".to_string()));
331
332    fs::create_dir_all(Path::new(&full_path).join("files"))?;
333    File::create(&full_path.clone().join("description.md"))?.write_all(description.as_bytes())?;
334
335    if platform == "WII" {
336        mod_info.insert("custom_textures_path".to_string(), serde_json::Value::String("textures".to_string()));
337        fs::create_dir_all(Path::new(&full_path).join("textures"))?;
338    }
339
340    if game == "EMR" {
341        mod_info.insert("scripts_path".to_string(), serde_json::Value::String("scripts".to_string()));
342        fs::create_dir_all(Path::new(&full_path).join("scripts"))?;
343    }
344
345    meta_file.write_all(serde_json::to_string(&mod_info)?.as_bytes())?;
346    println!("Finished generating mod");
347    Ok(())
348}
349
350#[derive(Serialize, Deserialize)]
351pub struct ModInfo {
352    pub name: String,
353    pub game: String,
354    pub platform: String,
355    pub description: String,
356    pub short_description: String,
357    pub dependencies: Vec<String>,
358    pub custom_textures_path: String,
359    pub custom_game_files_path: String,
360    pub scripts_path: String,
361    pub icon_path: String,
362    pub auto_generated_tags: Vec<String>,
363}
364
365impl ModInfo {
366    pub fn new() -> ModInfo {
367        ModInfo {
368            name: "".to_string(),
369            game: "".to_string(),
370            platform: "".to_string(),
371            scripts_path: "".to_string(),
372            custom_game_files_path: "".to_string(),
373            custom_textures_path: "".to_string(),
374            description: "".to_string(),
375            short_description: "".to_string(),
376            dependencies: Vec::new(),
377            icon_path: "".to_string(),
378            auto_generated_tags: Vec::new(),
379        }
380    }
381}