scratch_file/
lib.rs

1use anyhow::{Error, Result};
2use lazy_static::lazy_static;
3use serde::de::Visitor;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6use std::collections::hash_map::DefaultHasher;
7use std::convert::{TryFrom, TryInto};
8use std::fmt::{Debug, Display, Formatter};
9use std::hash::{Hash, Hasher};
10
11pub type HashMap<K, V> = std::collections::HashMap<K, V, fnv::FnvBuildHasher>;
12pub type HashSet<V> = std::collections::HashSet<V, fnv::FnvBuildHasher>;
13
14/// Represents the data inside a Scratch 3.0 (`.sb3`) file.
15///
16/// [.sb3 format documentation](https://en.scratch-wiki.info/wiki/Scratch_File_Format)
17#[derive(PartialEq, Clone, Default, Debug)]
18pub struct ScratchFile {
19    pub project: Project,
20
21    /// Maps filename to image
22    pub images: HashMap<String, Image>,
23}
24
25impl ScratchFile {
26    /// Parses a Scratch file to create a ScratchFile.
27    pub fn parse<R>(file: R) -> Result<ScratchFile>
28    where
29        R: std::io::Read + std::io::Seek,
30    {
31        use std::io::Read;
32
33        let mut archive = zip::ZipArchive::new(file)?;
34        let project: Project = serde_json::from_reader(archive.by_name("project.json")?)?;
35
36        let mut image_names: Vec<String> = Vec::new();
37        for name in archive.file_names() {
38            if name.ends_with(".svg") | name.ends_with(".png") {
39                image_names.push(name.to_string());
40            }
41        }
42
43        let mut images: HashMap<String, Image> = HashMap::default();
44        for name in &image_names {
45            let mut b: Vec<u8> = Vec::new();
46            archive.by_name(name).unwrap().read_to_end(&mut b)?;
47            let image = if name.ends_with(".svg") {
48                Image::SVG(b)
49            } else if name.ends_with(".png") {
50                Image::PNG(b)
51            } else {
52                return Err(Error::msg("unrecognized file extension"));
53            };
54            images.insert(name.clone(), image);
55        }
56
57        Ok(Self { project, images })
58    }
59}
60
61#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Project {
64    pub targets: Vec<Target>,
65    pub monitors: Vec<Monitor>,
66    pub extensions: Vec<String>,
67    pub meta: Meta,
68}
69
70/// Represents a Sprite.
71#[derive(Clone, Debug, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct Target {
74    /// true if background sprite
75    pub is_stage: bool,
76    pub name: String,
77    pub variables: HashMap<String, Variable>,
78    pub blocks: HashMap<BlockID, Block>,
79    pub costumes: Vec<Costume>,
80    /// Lowest number = back, highest number = front
81    #[serde(default)]
82    pub layer_order: usize,
83    /// This uses sprite coordinates.
84    /// Left = -240, right = +240
85    #[serde(default)]
86    pub x: f64,
87    /// Top = +180, bottom = -180
88    #[serde(default)]
89    pub y: f64,
90    #[serde(default)]
91    pub size: f64,
92    #[serde(default)]
93    pub visible: bool,
94}
95
96impl Default for Target {
97    fn default() -> Self {
98        Self {
99            is_stage: false,
100            name: String::new(),
101            variables: HashMap::default(),
102            blocks: HashMap::default(),
103            costumes: Vec::new(),
104            layer_order: 0,
105            x: 0.0,
106            y: 0.0,
107            size: 0.0,
108            visible: true,
109        }
110    }
111}
112
113impl Hash for Target {
114    fn hash<H: Hasher>(&self, state: &mut H) {
115        self.is_stage.hash(state);
116        self.name.hash(state);
117        sorted_entries(&self.variables).hash(state);
118        sorted_entries(&self.blocks).hash(state);
119        self.costumes.hash(state);
120        self.x.to_bits().hash(state);
121        self.y.to_bits().hash(state);
122        self.size.to_bits().hash(state);
123    }
124}
125
126impl PartialEq for Target {
127    fn eq(&self, other: &Self) -> bool {
128        equal_hash(&self, other)
129    }
130}
131
132#[derive(Clone, Default, Debug, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct Variable {
135    pub id: String,
136    pub value: Value,
137    #[serde(default)]
138    pub i_dont_know_what_this_does: bool,
139}
140
141impl Hash for Variable {
142    fn hash<H: Hasher>(&self, state: &mut H) {
143        self.id.hash(state);
144        hash_value(&self.value, state);
145        self.i_dont_know_what_this_does.hash(state);
146    }
147}
148
149impl PartialEq for Variable {
150    fn eq(&self, other: &Self) -> bool {
151        equal_hash(&self, other)
152    }
153}
154
155fn equal_hash<A, B>(a: A, b: B) -> bool
156where
157    A: Hash,
158    B: Hash,
159{
160    let mut hasher_a = DefaultHasher::new();
161    a.hash(&mut hasher_a);
162    let mut hasher_b = DefaultHasher::new();
163    b.hash(&mut hasher_b);
164    hasher_a.finish() == hasher_b.finish()
165}
166
167/// Blocks are the rectangle and oval code blocks.
168#[derive(Clone, Default, Debug, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct Block {
171    pub opcode: String,
172    /// Block attached below this block
173    pub next: Option<BlockID>,
174    /// Inputs are the oval holes in blocks where you can drop oval blocks into
175    pub inputs: HashMap<String, Value>,
176    /// Fields are the drop downs in blocks and therefore can only take constant strings
177    pub fields: HashMap<String, Vec<Option<String>>>,
178    /// Top most block in a stack of connected blocks
179    pub top_level: bool,
180}
181
182impl Hash for Block {
183    fn hash<H: Hasher>(&self, state: &mut H) {
184        self.opcode.hash(state);
185        self.next.hash(state);
186
187        for entry in sorted_entries(&self.inputs) {
188            entry.0.hash(state);
189            hash_value(&entry.1, state);
190        }
191
192        sorted_entries(&self.fields).hash(state);
193
194        self.top_level.hash(state);
195    }
196}
197
198impl PartialEq for Block {
199    fn eq(&self, other: &Self) -> bool {
200        equal_hash(&self, other)
201    }
202}
203
204fn sorted_entries<K, V>(map: &HashMap<K, V>) -> Vec<(&K, &V)>
205where
206    K: std::cmp::Ord,
207{
208    let mut result: Vec<(&K, &V)> = map.iter().collect();
209    result.sort_unstable_by(|a, b| a.0.cmp(b.0));
210    result
211}
212
213fn hash_value<H>(value: &Value, state: &mut H)
214where
215    H: Hasher,
216{
217    value.to_string().hash(state)
218}
219
220/// Sprite costume
221#[derive(Clone, Debug, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct Costume {
224    pub name: String,
225    pub md5ext: Option<String>,
226    pub asset_id: String,
227    /// Center of this costume with coordinates local to the image.
228    pub rotation_center_x: f64,
229    pub rotation_center_y: f64,
230    #[serde(default)]
231    pub bitmap_resolution: f64,
232}
233
234impl Default for Costume {
235    fn default() -> Self {
236        Self {
237            name: String::new(),
238            md5ext: None,
239            asset_id: String::new(),
240            rotation_center_x: 0.0,
241            rotation_center_y: 0.0,
242            bitmap_resolution: 1.0,
243        }
244    }
245}
246
247impl Hash for Costume {
248    fn hash<H: Hasher>(&self, state: &mut H) {
249        self.name.hash(state);
250        self.md5ext.hash(state);
251        self.rotation_center_x.to_bits().hash(state);
252        self.rotation_center_y.to_bits().hash(state);
253    }
254}
255
256impl PartialEq for Costume {
257    fn eq(&self, other: &Self) -> bool {
258        equal_hash(&self, other)
259    }
260}
261
262/// A monitor is the grey and orange rectangle that outputs the variable value.
263#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct Monitor {
266    pub id: String,
267    pub mode: String,
268    pub opcode: String,
269    pub params: MonitorParams,
270    pub sprite_name: Option<String>,
271    pub value: Value,
272    pub x: f64,
273    pub y: f64,
274    pub visible: bool,
275    pub slider_min: f64,
276    pub slider_max: f64,
277    pub is_discrete: bool,
278}
279
280#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct MonitorParams {
283    #[serde(rename = "VARIABLE")]
284    pub variable: String,
285}
286
287#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct Meta {
290    pub semver: String,
291    pub vm: String,
292    pub agent: String,
293}
294
295/// Contains the raw bytes of the image format.
296#[derive(PartialEq, Eq, Clone)]
297pub enum Image {
298    SVG(Vec<u8>),
299    PNG(Vec<u8>),
300}
301
302impl Debug for Image {
303    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
304        let vec_len = match self {
305            Image::SVG(v) => {
306                write!(f, "SVG(")?;
307                v.len()
308            }
309            Image::PNG(v) => {
310                write!(f, "PNG(")?;
311                v.len()
312            }
313        };
314
315        if vec_len > 0 {
316            write!(f, "[...]")?;
317        } else {
318            write!(f, "[]")?;
319        }
320
321        write!(f, ")")
322    }
323}
324
325/// Unique ID for each block.
326#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Default, Hash)]
327pub struct BlockID {
328    id: [u8; 20],
329}
330
331lazy_static! {
332    static ref PSEUDO_ID: BlockID = BlockID::try_from("                    ").unwrap();
333}
334
335impl BlockID {
336    pub fn new(id: [u8; 20]) -> Self {
337        Self { id }
338    }
339
340    /// Indicates that the block that did not come from the .sb3 file, such as ValueNumber.
341    pub fn pseudo_id() -> BlockID {
342        *PSEUDO_ID
343    }
344}
345
346impl Debug for BlockID {
347    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
348        f.write_str("BlockID { ")?;
349        Display::fmt(self, f)?;
350        f.write_str(" }")
351    }
352}
353
354impl Display for BlockID {
355    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
356        f.write_str(std::str::from_utf8(&self.id[..10]).map_err(|_| std::fmt::Error {})?)
357    }
358}
359
360impl TryFrom<&str> for BlockID {
361    type Error = Error;
362
363    fn try_from(s: &str) -> Result<Self> {
364        let mut id: [u8; 20] = [0; 20];
365        let s_bytes = s.as_bytes();
366        if s_bytes.len() == id.len() {
367            id.copy_from_slice(s_bytes);
368            Ok(Self { id })
369        } else {
370            Err(Error::msg("invalid string"))
371        }
372    }
373}
374
375impl Serialize for BlockID {
376    fn serialize<S>(
377        &self,
378        serializer: S,
379    ) -> std::result::Result<<S as Serializer>::Ok, <S as Serializer>::Error>
380    where
381        S: Serializer,
382    {
383        serializer.serialize_str(&self.to_string())
384    }
385}
386
387impl<'de> Deserialize<'de> for BlockID {
388    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, <D as Deserializer<'de>>::Error>
389    where
390        D: Deserializer<'de>,
391    {
392        struct BytesVisitor;
393
394        impl<'de> Visitor<'de> for BytesVisitor {
395            type Value = BlockID;
396
397            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
398                formatter.write_str("string")
399            }
400
401            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
402            where
403                E: serde::de::Error,
404            {
405                v.try_into().map_err(serde::de::Error::custom)
406            }
407        }
408
409        deserializer.deserialize_str(BytesVisitor)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_savefile() {
419        let file = std::fs::File::open("test_saves/say.sb3").unwrap();
420        let savefile = ScratchFile::parse(&file).unwrap();
421        let target = &savefile.project.targets[1];
422        assert_eq!(target.name, "Sprite1");
423    }
424
425    mod block_id {
426        use super::*;
427
428        #[test]
429        fn test_from_str() {
430            {
431                assert!(BlockID::try_from("").is_err());
432            }
433            {
434                assert!(BlockID::try_from("a").is_err());
435            }
436            {
437                let s = "G@pZX]3ynBGB)L`_LJk8";
438                let id = BlockID::try_from(s).unwrap();
439                assert_eq!(&id.to_string(), "G@pZX]3ynB");
440            }
441        }
442    }
443}