Skip to main content

swrs/parser/
mod.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use thiserror::Error;
4use crate::CryptoError;
5use crate::parser::file::{FileParseError, FileReconstructionError};
6use crate::parser::library::{LibraryParseError, LibraryReconstructionError};
7use crate::parser::logic::{LogicParseError, LogicReconstructionError};
8use crate::parser::resource::{ResourceParseError, ResourceReconstructionError};
9use crate::parser::view::{ViewParseError, ViewReconstructionError};
10
11pub mod project;
12pub mod file;
13pub mod library;
14pub mod resource;
15pub mod view;
16pub mod logic;
17pub(crate) mod serde_util;
18
19/// Represents a parsable (and possibly re-construct-able) object
20pub trait Parsable
21where Self: Sized {
22    type ParseError;
23    type ReconstructionError;
24
25    /// Parses a decrypted content of itself and returns an instance of itself wrapped around a [`Result`]
26    fn parse(decrypted_content: &str) -> Result<Self, Self::ParseError>;
27
28    /// Reconstructs itself into a string form wrapped around a [`Result`]
29    /// by default, if not implemented, this will panic ([`unimplemented!()`])
30    fn reconstruct(&self) -> Result<String, Self::ReconstructionError> {
31        unimplemented!()
32    }
33}
34
35/// Represents a raw (un-parsed) sketchware project
36#[derive(Debug, Clone, PartialEq)]
37pub struct RawSketchwareProject {
38    pub project: String,
39    pub file: String,
40    pub library: String,
41    pub resource: String,
42    pub view: String,
43    pub logic: String,
44
45    /// A list of resource files that belongs to this project
46    ///
47    /// `None` means to automatically assign missing resources with a random id
48    pub resource_files: Option<Vec<ResourceFileWrapper>>
49}
50
51impl RawSketchwareProject {
52    /// Creates a RawSketchwareProject with the specified fields
53    pub fn new(
54        project: String,
55        file: String,
56        library: String,
57        resource: String,
58        view: String,
59        logic: String,
60        resource_files: Vec<ResourceFileWrapper>
61    ) -> Self {
62        RawSketchwareProject { project, file, library, resource, view, logic, resource_files: Some(resource_files) }
63    }
64
65    /// Creates a RawSketchwareProject with the specified fields without the resource files, they
66    /// will all be assigned to random ids
67    pub fn new_wo_res(
68        project: String,
69        file: String,
70        library: String,
71        resource: String,
72        view: String,
73        logic: String,
74    ) -> Self {
75        RawSketchwareProject { project, file, library, resource, view, logic, resource_files: None }
76    }
77
78    pub fn from_encrypted(
79        project: Vec<u8>,
80        file: Vec<u8>,
81        library: Vec<u8>,
82        resource: Vec<u8>,
83        view: Vec<u8>,
84        logic: Vec<u8>,
85        resource_files: Vec<ResourceFileWrapper>
86    ) -> Result<Self, CryptoError> {
87        macro_rules! decrypt {
88            ($name_ident:ident, $name:expr) => {
89                String::from_utf8(super::decrypt_sw_encrypted(&$name_ident)?)
90                    .map_err(CryptoError::FromUtf8Error)?
91            }
92        }
93
94        Ok(RawSketchwareProject {
95            project: decrypt!(project, "project"),
96            file: decrypt!(file, "file"),
97            library: decrypt!(library, "library"),
98            resource: decrypt!(resource, "resource"),
99            view: decrypt!(view, "view"),
100            logic: decrypt!(logic, "logic"),
101            resource_files: Some(resource_files)
102        })
103    }
104
105    /// Creates a RawSketchwareProject from encrypted sketchware project data without the resource
106    /// files, they will all be assigned to random ids
107    pub fn from_encrypted_wo_res(
108        project: Vec<u8>,
109        file: Vec<u8>,
110        library: Vec<u8>,
111        resource: Vec<u8>,
112        view: Vec<u8>,
113        logic: Vec<u8>,
114    ) -> Result<Self, CryptoError> {
115        macro_rules! decrypt {
116            ($name_ident:ident, $name:expr) => {
117                String::from_utf8(super::decrypt_sw_encrypted(&$name_ident)?)
118                    .map_err(CryptoError::FromUtf8Error)?
119            }
120        }
121
122        Ok(RawSketchwareProject {
123            project: decrypt!(project, "project"),
124            file: decrypt!(file, "file"),
125            library: decrypt!(library, "library"),
126            resource: decrypt!(resource, "resource"),
127            view: decrypt!(view, "view"),
128            logic: decrypt!(logic, "logic"),
129            resource_files: None
130        })
131    }
132}
133
134/// Represents a parsed sketchware project that contains
135/// [`project::Project`], [`file::File`], [`library::Library`], [`resource::Resource`],
136/// [`view::View`], and [`logic::Logic`]
137#[derive(Debug, Clone, PartialEq)]
138pub struct SketchwareProject {
139    pub project: project::Project,
140    pub file: file::File,
141    pub library: library::Library,
142    pub resource: resource::Resource,
143    pub view: view::View,
144    pub logic: logic::Logic,
145
146    /// The resource files attached to this project. If None, that means the resource files are
147    /// ignored
148    pub resource_files: Option<ResourceFiles>,
149}
150
151impl SketchwareProject {
152    /// Parses a [`RawSketchwareProject`] into [`SketchwareProject`]
153    pub fn parse_from(raw_swproj: RawSketchwareProject) -> Result<Self, SketchwareProjectParseError> {
154        Ok(SketchwareProject {
155            project: project::Project::parse(raw_swproj.project.as_str())
156                .map_err(SketchwareProjectParseError::ProjectParseError)?,
157
158            file: file::File::parse(raw_swproj.file.as_str())
159                .map_err(SketchwareProjectParseError::FileParseError)?,
160
161            library: library::Library::parse(raw_swproj.library.as_str())
162                .map_err(SketchwareProjectParseError::LibraryParseError)?,
163
164            resource: resource::Resource::parse(raw_swproj.resource.as_str())
165                .map_err(SketchwareProjectParseError::ResourceParseError)?,
166
167            view: view::View::parse(raw_swproj.view.as_str())
168                .map_err(SketchwareProjectParseError::ViewParseError)?,
169
170            logic: logic::Logic::parse(raw_swproj.logic.as_str())
171                .map_err(SketchwareProjectParseError::LogicParseError)?,
172
173            resource_files: raw_swproj.resource_files
174                .map(|r| r.try_into())
175                .transpose()
176                .map_err(SketchwareProjectParseError::ResourceFilesParseError)?,
177        })
178    }
179
180    /// Parses a list of project data into [`SketchwareProject`]
181    pub fn parse(project: String, file: String, library: String, resource: String, view: String,
182                 logic: String, resource_files: Vec<ResourceFileWrapper>)
183        -> Result<Self, SketchwareProjectParseError> {
184
185        SketchwareProject::parse_from(RawSketchwareProject {
186            project,
187            file,
188            library,
189            resource,
190            view,
191            logic,
192            resource_files: Some(resource_files)
193        })
194    }
195
196    /// Parses a list of project data into [`SketchwareProject`] without resource files (they'll be
197    /// ignored on API)
198    pub fn parse_wo_res(project: String, file: String, library: String, resource: String,
199                        view: String, logic: String)
200                 -> Result<Self, SketchwareProjectParseError> {
201
202        SketchwareProject::parse_from(RawSketchwareProject {
203            project,
204            file,
205            library,
206            resource,
207            view,
208            logic,
209            resource_files: None
210        })
211    }
212}
213
214impl TryInto<RawSketchwareProject> for SketchwareProject {
215    type Error = SketchwareProjectReconstructionError;
216
217    fn try_into(self) -> Result<RawSketchwareProject, Self::Error> {
218        Ok(RawSketchwareProject {
219            project: self.project.reconstruct()
220                .map_err(SketchwareProjectReconstructionError::ProjectReconstructionError)?,
221
222            file: self.file.reconstruct()
223                .map_err(SketchwareProjectReconstructionError::FileReconstructionError)?,
224
225            library: self.library.reconstruct()
226                .map_err(SketchwareProjectReconstructionError::LibraryReconstructionError)?,
227
228            resource: self.resource.reconstruct()
229                .map_err(SketchwareProjectReconstructionError::ResourceReconstructionError)?,
230
231            view: self.view.reconstruct()
232                .map_err(SketchwareProjectReconstructionError::ViewReconstructionError)?,
233
234            logic: self.logic.reconstruct()
235                .map_err(SketchwareProjectReconstructionError::LogicReconstructionError)?,
236
237            resource_files: self.resource_files.map(|r| r.into())
238        })
239    }
240}
241
242#[derive(Error, Debug)]
243pub enum SketchwareProjectParseError {
244    #[error("failed to parse the data file `project`")]
245    ProjectParseError(#[from] serde_json::Error),
246
247    #[error("failed to parse the data file `file`")]
248    FileParseError(#[from] FileParseError),
249
250    #[error("failed to parse the data file `library`")]
251    LibraryParseError(#[from] LibraryParseError),
252
253    #[error("failed to parse the data file `resource`")]
254    ResourceParseError(#[from] ResourceParseError),
255
256    #[error("failed to parse the data file `view`")]
257    ViewParseError(#[from] ViewParseError),
258
259    #[error("failed to parse the data file `logic`")]
260    LogicParseError(#[from] LogicParseError),
261
262    #[error("failed retrieve resource files")]
263    ResourceFilesParseError(#[from] ResourceFilesParseError)
264}
265
266// these names might be too long lol, should i shorten them to something like SwProjectReconError?
267#[derive(Error, Debug)]
268pub enum SketchwareProjectReconstructionError {
269    #[error("failed to reconstruct the data file `project`")]
270    ProjectReconstructionError(#[from] serde_json::Error),
271
272    #[error("failed to reconstruct the data file `file`")]
273    FileReconstructionError(#[from] FileReconstructionError),
274
275    #[error("failed to reconstruct the data file `library`")]
276    LibraryReconstructionError(#[from] LibraryReconstructionError),
277
278    #[error("failed to reconstruct the data file `resource`")]
279    ResourceReconstructionError(#[from] ResourceReconstructionError),
280
281    #[error("failed to reconstruct the data file `view`")]
282    ViewReconstructionError(#[from] ViewReconstructionError),
283
284    #[error("failed to reconstruct the data file `logic`")]
285    LogicReconstructionError(#[from] LogicReconstructionError),
286}
287
288/// A wrapper to a real or imaginary resource file, a "wrapper" that can be either an id (string or
289/// u32) or a real file in the filesystem
290///
291/// If you made an imaginary resource file, make sure for the filename of a file that corresponds
292/// to imaginary resource file to have the same name as the id's `res_full_name`
293///
294/// This enum is made so that swrs is portable and can be used across platforms with very little to
295/// no tweaking
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub enum ResourceFileWrapper {
298    /// A real path to a real file in the filesystem. swrs will use its path to determine what type
299    /// of resource this is, filename as the resource name. and swrs will do a check if this file
300    /// exists
301    Path(PathBuf),
302
303    /// An imaginary file that is identified with a string
304    StringId {
305        id: String,
306
307        /// The resource file name, with its extension. This is used to match with the resources
308        /// used within this sketchware project.
309        ///
310        /// please make sure the filename of the file that corresponds to this matches with this
311        res_full_name: String,
312        res_type: ResourceType
313    },
314
315    /// An imaginary file that is identified with an unsigned 32-bit integer
316    U32Id {
317        id: u32,
318
319        /// The resource file name, with its extension. This is used to match with the resources
320        /// used within this sketchware project
321        ///
322        /// please make sure the filename of the file that corresponds to this matches with this
323        res_full_name: String,
324        res_type: ResourceType
325    },
326}
327
328impl ResourceFileWrapper {
329    pub fn get_full_name(&self) -> String {
330        match &self {
331            ResourceFileWrapper::Path(path) => {
332                path.file_name().unwrap().to_str().unwrap().to_string() /* should never fail */
333            }
334
335            ResourceFileWrapper::StringId { res_full_name, .. } => { res_full_name.to_owned() }
336            ResourceFileWrapper::U32Id { res_full_name, .. } => { res_full_name.to_owned() }
337        }
338    }
339
340    /// Generates a random ResourceFileWrapper::U32Id with the provided resource name and type
341    pub fn make_random_id(res_full_name: String, res_type: ResourceType) -> ResourceFileWrapper {
342        ResourceFileWrapper::U32Id {
343            id: rand::random(),
344            res_full_name,
345            res_type
346        }
347    }
348}
349
350#[derive(Debug, Copy, Clone, PartialEq, Eq)]
351pub enum ResourceType { Image, Sound, Font, CustomIcon }
352
353/// A struct that stores all the resources of a sketchware project its attached to
354///
355/// Filled with HashMaps with keys of resource full names
356#[derive(Debug, Clone, PartialEq)]
357pub struct ResourceFiles {
358    pub custom_icon: Option<ResourceFileWrapper>,
359    pub images: HashMap<String, ResourceFileWrapper>,
360    pub sounds: HashMap<String, ResourceFileWrapper>,
361    pub fonts: HashMap<String, ResourceFileWrapper>,
362}
363
364impl TryFrom<Vec<ResourceFileWrapper>> for ResourceFiles {
365    type Error = ResourceFilesParseError;
366
367    fn try_from(value: Vec<ResourceFileWrapper>) -> Result<Self, Self::Error> {
368        let mut images = HashMap::new();
369        let mut sounds = HashMap::new();
370        let mut fonts = HashMap::new();
371        let mut custom_icon = None;
372
373        for path in value {
374            match &path {
375                ResourceFileWrapper::Path(file) => {
376                    if !file.exists() {
377                        return Err(ResourceFilesParseError::FileDoesntExist { path: file.clone() });
378                    }
379
380                    // its path should be
381                    //
382                    // xxx/.sketchware/resources/(images|sounds|fonts|icons)/{project id}/file.extension
383                    //
384                    // the subfolder after /resources/ determines what type of resource it is
385                    let res_type = file
386                        .parent().ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?
387                        .parent().ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?
388                        .file_name().ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?
389                        .to_str().ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?;
390
391                    let res_full_name = file.file_name()
392                        .ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?
393                        .to_str().ok_or_else(|| ResourceFilesParseError::InvalidPath { path: file.clone() })?
394                        .to_string();
395
396                    match res_type {
397                        "images" => { images.insert(res_full_name, path); },
398                        "sounds" => { sounds.insert(res_full_name, path); },
399                        "fonts" => { fonts.insert(res_full_name, path); },
400                        "icons" => { custom_icon = Some(path); },
401
402                        _ => Err(ResourceFilesParseError::InvalidPath { path: file.clone() })?
403                    }
404                }
405
406                ResourceFileWrapper::StringId { res_type, res_full_name, .. } => {
407                    match res_type {
408                        ResourceType::Image => { images.insert(res_full_name.to_owned(), path); },
409                        ResourceType::Sound => { sounds.insert(res_full_name.to_owned(), path); },
410                        ResourceType::Font => { fonts.insert(res_full_name.to_owned(), path); },
411                        ResourceType::CustomIcon => { custom_icon = Some(path); }
412                    }
413                }
414
415                ResourceFileWrapper::U32Id { res_type, res_full_name, .. } => {
416                    match res_type {
417                        ResourceType::Image => { images.insert(res_full_name.to_owned(), path); },
418                        ResourceType::Sound => { sounds.insert(res_full_name.to_owned(), path); },
419                        ResourceType::Font => { fonts.insert(res_full_name.to_owned(), path); },
420                        ResourceType::CustomIcon => { custom_icon = Some(path); }
421                    }
422                }
423            }
424        }
425
426        Ok(ResourceFiles { custom_icon, images, sounds, fonts })
427    }
428}
429
430#[derive(Error, Debug)]
431pub enum ResourceFilesParseError {
432    #[error("file `{path:?}` does not exist")]
433    FileDoesntExist {
434        path: PathBuf
435    },
436    #[error("path given `{path:?}` is invalid (are you sure its pointing to a sketchware's resources folder?)")]
437    InvalidPath {
438        path: PathBuf
439    }
440}
441
442impl Into<Vec<ResourceFileWrapper>> for ResourceFiles {
443    fn into(self) -> Vec<ResourceFileWrapper> {
444        let mut result = Vec::new();
445
446        if let Some(custom_icon) = self.custom_icon {
447            result.push(custom_icon);
448        }
449
450        result.append(&mut self.images.into_values().collect());
451        result.append(&mut self.sounds.into_values().collect());
452        result.append(&mut self.fonts.into_values().collect());
453
454        result
455    }
456}
457
458impl Default for ResourceFiles {
459    fn default() -> Self {
460        ResourceFiles {
461            custom_icon: None,
462            images: HashMap::new(),
463            sounds: HashMap::new(),
464            fonts: HashMap::new()
465        }
466    }
467}