diff --git a/index.js b/index.js index 79c48954..310e42a2 100644 --- a/index.js +++ b/index.js @@ -8,19 +8,22 @@ var validate = require('./lib/validate'); * Unpacks, parses, validates, and analyzes Scratch projects. If successful, * will return a valid Scratch project object with appended metadata. * @param {Buffer | string} input Buffer or string representing project + * @param {boolean} isSprite Whether this is a sprite (true) or whole project (false) * @param {Function} callback Returns error or project data */ -module.exports = function (input, callback) { +module.exports = function (input, isSprite, callback) { // First unpack the input (need this outside of the async waterfall so that // unpackedProject can be refered to again) - unpack(input, function (err, unpackedProject) { + unpack(input, isSprite, function (err, unpackedProject) { if (err) return callback(err); async.waterfall([ function (cb) { parse(unpackedProject[0], cb); }, - validate + // TODO is there a better way to pass this arg + // than partially applying this funciton? + validate.bind(null, isSprite) ], function (error, validatedInput) { // One more callback wrapper so that we can re-package everything // with the possible zip returned from unpack diff --git a/lib/parse.js b/lib/parse.js index 1aab4934..db0ce271 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -8,10 +8,11 @@ * @return {void} */ module.exports = function (input, callback) { + var result; try { - var result = JSON.parse(input); - callback(null, result); + result = JSON.parse(input); } catch (e) { return callback(e.toString()); } + return callback(null, result); }; diff --git a/lib/sb2_definitions.json b/lib/sb2_definitions.json new file mode 100644 index 00000000..a6a0966c --- /dev/null +++ b/lib/sb2_definitions.json @@ -0,0 +1,195 @@ +{ + "$id": "https://scratch.mit.edu/sb2_definitions.json", + "$schema": "http://json-schema.org/schema#", + "description": "Scratch 2.0 Project and Sprite Definitions", + "definitions": { + "scripts": { + "type": "array" + }, + "sounds": { + "type": "array", + "properties": { + "soundName": { + "type": "string" + }, + "soundID": { + "type": "number" + }, + "md5": { + "type": "string" + }, + "sampleCount": { + "type": "number" + }, + "rate": { + "type": "number" + }, + "format": { + "type": "string" + } + }, + "required": [ + "soundName", + "soundID", + "md5", + "sampleCount", + "rate", + "format" + ] + }, + "costumes": { + "type": "array", + "properties": { + "costumeName": { + "type": "string" + }, + "baseLayerID": { + "type": "number" + }, + "baseLayerMD5": { + "type": "string" + }, + "bitmapResolution": { + "type": "number" + }, + "rotationCenterX": { + "type": "number" + }, + "rotationCenterY": { + "type": "number" + } + }, + "required": [ + "costumeName", + "baseLayerID", + "baseLayerMD5", + "bitmapResolution", + "rotationCenterX", + "rotationCenterY" + ] + }, + "variables": { + "type": "array", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "isPersistent": { + "type": "boolean" + } + }, + "required": [ + "name", + "value" + ] + }, + "sprite_object": { + "type": "object", + "properties": { + "objName": { + "type": "string" + }, + "variables": {"$ref": "#/definitions/variables"}, + "sounds": {"$ref":"#/definitions/sounds"}, + "costumes": {"$ref":"#/definitions/costumes"}, + "currentCostumeIndex": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": true, + "required": [ + "objName", + "sounds", + "costumes", + "currentCostumeIndex" + ] + }, + "stage_child": { + "type": "object", + "description": "A child of the stage, can be a sprite or a monitor" + }, + "stage_object" : { + "type": "object", + "properties": { + "objName": { + "type": "string" + }, + "variables": {"$ref": "#/definitions/variables"}, + "lists": { + "type": "array", + "properties": { + "listName": { + "type": "string" + }, + "contents": { + "type": "array" + }, + "isPersistent": { + "type": "boolean" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "visible": { + "type": "boolean" + } + }, + "required": [ + "listName", + "contents" + ] + }, + "scripts": {"$ref": "#/definitions/scripts"}, + "sounds": {"$ref": "#/definitions/sounds"}, + "costumes": {"$ref": "#/definitions/costumes"}, + "currentCostumeIndex": { + "type": "number", + "minimum": 0 + }, + "penLayerMD5": { + "oneOf":[ + {"type": "string"}, + {"type": "null"} + ] + }, + "penLayerID": { + "type": "number", + "minimum": -1 + }, + "tempoBPM": { + "type": "number" + }, + "videoAlpha": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "children": { + "type": "array", + "items": {"$ref": "#/definitions/stage_child"} + } + }, + "required": [ + "objName", + "costumes", + "currentCostumeIndex", + "penLayerMD5", + "tempoBPM", + "children" + ] + } + } +} diff --git a/lib/sb2_schema.json b/lib/sb2_schema.json index c868680a..2f7af051 100644 --- a/lib/sb2_schema.json +++ b/lib/sb2_schema.json @@ -1,207 +1,54 @@ { - "$id": "https://scratch.mit.edu", - "description": "Scratch project schema", + "$id": "https://scratch.mit.edu/sb2_schema.json", + "$schema": "http://json-schema.org/schema#", + "description": "Scratch 2.0 project schema", "type": "object", - "properties": { - "objName": { - "type": "string" - }, - "variables": { - "type": "array", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "isPersistent": { - "type": "boolean" - } - }, - "required": [ - "name", - "value" - ] - }, - "lists": { - "type": "array", - "properties": { - "listName": { - "type": "string" - }, - "contents": { - "type": "array" - }, - "isPersistent": { - "type": "boolean" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "width": { - "type": "number" - }, - "height": { - "type": "number" - }, - "visible": { - "type": "boolean" - } - }, - "required": [ - "listName", - "contents" - ] - }, - "scripts": { - "type": "array" - }, - "sounds": { - "type": "array", - "properties": { - "soundName": { - "type": "string" - }, - "soundID": { - "type": "number" - }, - "md5": { - "type": "string" - }, - "sampleCount": { - "type": "number" - }, - "rate": { - "type": "number" - }, - "format": { - "type": "string" - } - }, - "required": [ - "soundName", - "soundID", - "md5", - "sampleCount", - "rate", - "format" - ] - }, - "costumes": { - "type": "array", - "properties": { - "costumeName": { - "type": "string" - }, - "baseLayerID": { - "type": "number" - }, - "baseLayerMD5": { - "type": "string" - }, - "bitmapResolution": { - "type": "number" - }, - "rotationCenterX": { - "type": "number" - }, - "rotationCenterY": { - "type": "number" - } - }, - "required": [ - "costumeName", - "baseLayerID", - "baseLayerMD5", - "bitmapResolution", - "rotationCenterX", - "rotationCenterY" - ] - }, - "currentCostumeIndex": { - "type": "number", - "minimum": 0 - }, - "penLayerMD5": { - "oneOf":[ - {"type": "string"}, - {"type": "null"} - ] - }, - "penLayerID": { - "type": "number", - "minimum": -1 - }, - "tempoBPM": { - "type": "number" - }, - "videoAlpha": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "children": { - "type": "array", - "properties": {}, - "required": [ - "objName", - "sounds", - "costumes", - "currentCostumeIndex" - ] - }, - "info": { + "allOf": [ + {"$ref": "sb2_definitions.json#/definitions/stage_object"}, + { "type": "object", "properties": { - "flashVersion": { - "type": "string" - }, - "swfVersion": { - "type": "string" - }, - "userAgent": { - "type": "string" - }, - "videoOn": { - "type": "boolean" - }, - "savedExtensions": { - "type": "array", + "info": { + "type": "object", "properties": { - "extensionName": { + "flashVersion": { "type": "string" }, - "blockSpecs": { - "type": "array" - }, - "menus": { - "type": "object" + "swfVersion": { + "type": "string" }, - "javascriptURL": { + "userAgent": { "type": "string" + }, + "videoOn": { + "type": "boolean" + }, + "savedExtensions": { + "type": "array", + "properties": { + "extensionName": { + "type": "string" + }, + "blockSpecs": { + "type": "array" + }, + "menus": { + "type": "object" + }, + "javascriptURL": { + "type": "string" + } + }, + "required": [ + "extensionName", + "blockSpecs", + "menus", + "javascriptURL" + ] } - }, - "required": [ - "extensionName", - "blockSpecs", - "menus", - "javascriptURL" - ] + } } } } - }, - "required": [ - "objName", - "costumes", - "currentCostumeIndex", - "penLayerMD5", - "tempoBPM", - "children", - "info" ] } diff --git a/lib/sb3_definitions.json b/lib/sb3_definitions.json new file mode 100644 index 00000000..008e9c5a --- /dev/null +++ b/lib/sb3_definitions.json @@ -0,0 +1,404 @@ +{ + "$id": "https://scratch.mit.edu/sb3_definitions.json", + "$schema": "http://json-schema.org/schema#", + "description": "Scratch 3.0 Project and Sprite Schema Definitions", + "definitions": { + "optionalString": { + "oneOf": [ + {"type": "string"}, + {"type": "null"} + ] + }, + "boolOrBoolString": { + "oneOf": [ + {"type": "string", + "enum": ["true", "false"]}, + {"type": "boolean"} + ] + }, + "stringOrNumber": { + "oneOf": [ + {"type": "string"}, + {"type": "number"} + ] + }, + "scalarVal": { + "oneOf": [ + {"$ref":"#/definitions/stringOrNumber"}, + {"type": "boolean"} + ] + }, + "assetId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$" + }, + "costume": { + "type": "object", + "properties": { + "assetId": { "$ref": "#/definitions/assetId"}, + "bitmapResolution": { + "type": "integer" + }, + "dataFormat": { + "type": "string", + "enum": ["png", "svg", "jpeg", "jpg", "bmp", "gif"] + }, + "md5ext": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}\\.[a-zA-Z]+$" + }, + "name": { + "type": "string" + }, + "rotationCenterX": { + "type": "number", + "description": "This property is not required, but is highly recommended." + }, + "rotationCenterY": { + "type": "number", + "description": "This property is not required, but is highly recommended." + } + }, + "required": [ + "assetId", + "dataFormat", + "name" + ] + }, + "sound": { + "type": "object", + "properties": { + "assetId": { "$ref": "#/definitions/assetId"}, + "dataFormat": { + "type": "string", + "enum": ["wav", "wave", "mp3"] + }, + "md5ext": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}\\.[a-zA-Z0-9]+$" + }, + "name": { + "type": "string" + }, + "rate": { + "type": "integer" + }, + "sampleCount": { + "type": "integer" + } + }, + "required": [ + "assetId", + "dataFormat", + "name" + ] + }, + "scalar_variable": { + "type": "array", + "items": [ + {"type": "string", "description": "name of the variable"}, + {"$ref":"#/definitions/scalarVal", "description": "value of the variable"} + ], + "additionalItems": {"type": "boolean", "enum": [true], "description": "Whether this is a cloud variable"}, + "maxItems": 3 + }, + "list": { + "type": "array", + "items": [ + {"type":"string", "description": "name of the list"}, + { + "type": "array", + "description": "contents of the list", + "items": {"$ref":"#/definitions/scalarVal"} + } + ], + "additionalItems": false + }, + "broadcast_message": { + "type": "string", + "description": "the message being broadcasted" + }, + "num_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [4,5,6,7,8] + }, + {"$ref":"#/definitions/stringOrNumber"} + ], + "additionalItems": false + }, + "color_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [9] + }, + { + "type": "string", + "pattern": "^#[a-fA-F0-9]{6}$" + } + ], + "additionalItems": false + }, + "text_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [10] + }, + {"$ref":"#/definitions/stringOrNumber"} + ], + "additionalItems": false + }, + "broadcast_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [11] + }, + {"type": "string", "description": "broadcast message"}, + {"type": "string", "description": "broadcast message id"} + ], + "additionalItems": false + }, + "variable_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [12] + }, + {"type": "string", "description": "variable name"}, + {"type": "string", "description": "variable id"} + ], + "additionalItems": { + "type": "number" + }, + "minItems": 3, + "maxItems": 5 + }, + "list_primitive": { + "type": "array", + "items": [ + { + "type": "number", + "enum": [13] + }, + {"type": "string", "description": "list name"}, + {"type": "string", "description": "list id"} + ], + "additionalItems": { + "type": "number" + }, + "minItems": 3, + "maxItems": 5 + }, + "topLevelPrimitive": { + "oneOf": [ + {"$ref":"#/definitions/variable_primitive"}, + {"$ref":"#/definitions/list_primitive"} + ] + }, + "inputPrimitive": { + "oneOf": [ + {"$ref":"#/definitions/num_primitive"}, + {"$ref":"#/definitions/color_primitive"}, + {"$ref":"#/definitions/text_primitive"}, + {"$ref":"#/definitions/broadcast_primitive"}, + {"$ref":"#/definitions/variable_primitive"}, + {"$ref":"#/definitions/list_primitive"} + ] + }, + "block": { + "type": "object", + "properties": { + "opcode": { + "type": "string" + }, + "inputs": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "type":"number", + "enum":[1,2,3], + "description": "1 = unobscured shadow, 2 = no shadow, 3 = obscured shadow" + } + ], + "additionalItems": { + "oneOf": [ + {"$ref":"#/definitions/optionalString"}, + {"$ref":"#/definitions/inputPrimitive"} + ] + } + } + }, + "fields": { + "type": "object" + }, + "next": {"$ref":"#/definitions/optionalString"}, + "topLevel": { + "type": "boolean" + }, + "parent": {"$ref":"#/definitions/optionalString"}, + "shadow": { + "type": "boolean" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "mutation": { + "type": "object", + "properties": { + "tagName": { + "type": "string", + "enum": ["mutation"] + }, + "children": { + "type": "array" + }, + "proccode": { + "type": "string" + }, + "argumentids": { + "type": "string" + }, + "warp": {"$ref":"#/definitions/boolOrBoolString"}, + "hasnext": {"$ref":"#/definitions/boolOrBoolString"} + } + } + }, + "required": [ + "opcode" + ] + }, + "stage": { + "type": "object", + "description": "Description of property (and/or property/value pairs) that are unique to the stage.", + "properties": { + "name": { + "type": "string", + "enum": ["Stage"] + }, + "isStage": { + "type": "boolean", + "enum": [true] + }, + "tempo": { + "type": "number" + }, + "videoTransparency": { + "type": "number" + }, + "videoState": { + "type": "string", + "enum": ["on", "off", "on-flipped"] + } + }, + "required": [ + "name", + "isStage" + ] + }, + "sprite": { + "type": "object", + "description": "Description of property (and/or property/value pairs) for sprites.", + "properties": { + "name": { + "type": "string", + "not": {"enum": ["Stage", "stage"]} + }, + "isStage": { + "type": "boolean", + "enum": [false] + }, + "visible": { + "type": "boolean" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "size": { + "type": "number" + }, + "direction": { + "type": "number" + }, + "draggable": { + "type": "boolean" + }, + "rotationStyle": { + "type": "string", + "enum": ["all around", "don't rotate", "left-right"] + } + }, + "required": [ + "name", + "isStage" + ] + }, + "target": { + "type": "object", + "description" : "Properties common to both Scratch 3.0 Stage and Sprite", + "properties": { + "currentCostume": { + "type": "integer", + "minimum": 0 + }, + "blocks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + {"$ref":"#/definitions/block"}, + {"$ref":"#/definitions/topLevelPrimitive"} + ] + } + }, + "variables": { + "type": "object", + "additionalProperties": {"$ref":"#/definitions/scalar_variable"} + }, + "lists": { + "type": "object", + "additionalProperties": {"$ref":"#/definitions/list"} + }, + "broadcasts": { + "type": "object", + "additionalProperties": {"$ref":"#/definitions/broadcast_message"} + }, + "costumes": { + "type": "array", + "items": {"$ref":"#/definitions/costume"}, + "minItems": 1, + "uniqueItems": true + }, + "sounds": { + "type": "array", + "items": {"$ref":"#/definitions/sound"}, + "uniqueItems": true + }, + "volume": { + "type": "number" + } + }, + "required": [ + "variables", + "costumes", + "sounds", + "blocks" + ] + } + } +} diff --git a/lib/sb3_schema.json b/lib/sb3_schema.json index eba4a408..19dc552f 100644 --- a/lib/sb3_schema.json +++ b/lib/sb3_schema.json @@ -2,410 +2,6 @@ "$id": "https://scratch.mit.edu/sb3_schema.json", "$schema": "http://json-schema.org/schema#", "description": "Scratch 3.0 Project Schema", - "definitions": { - "optionalString": { - "oneOf": [ - {"type": "string"}, - {"type": "null"} - ] - }, - "boolOrBoolString": { - "oneOf": [ - {"type": "string", - "enum": ["true", "false"]}, - {"type": "boolean"} - ] - }, - "stringOrNumber": { - "oneOf": [ - {"type": "string"}, - {"type": "number"} - ] - }, - "scalarVal": { - "oneOf": [ - {"$ref":"#/definitions/stringOrNumber"}, - {"type": "boolean"} - ] - }, - "assetId": { - "type": "string", - "pattern": "^[a-fA-F0-9]{32}$" - }, - "costume": { - "type": "object", - "properties": { - "assetId": { "$ref": "#/definitions/assetId"}, - "bitmapResolution": { - "type": "integer" - }, - "dataFormat": { - "type": "string", - "enum": ["png", "svg", "jpeg", "jpg", "bmp", "gif"] - }, - "md5ext": { - "type": "string", - "pattern": "^[a-fA-F0-9]{32}\\.[a-zA-Z]+$" - }, - "name": { - "type": "string" - }, - "rotationCenterX": { - "type": "number" - }, - "rotationCenterY": { - "type": "number" - } - }, - "required": [ - "assetId", - "dataFormat", - "name", - "rotationCenterX", - "rotationCenterY" - ] - }, - "sound": { - "type": "object", - "properties": { - "assetId": { "$ref": "#/definitions/assetId"}, - "dataFormat": { - "type": "string", - "enum": ["wav", "wave", "mp3"] - }, - "format": { - "type": "string", - "enum": ["", "adpcm"] - }, - "md5ext": { - "type": "string", - "pattern": "^[a-fA-F0-9]{32}\\.[a-zA-Z0-9]+$" - }, - "name": { - "type": "string" - }, - "rate": { - "type": "integer" - }, - "sampleCount": { - "type": "integer" - } - }, - "required": [ - "assetId", - "dataFormat", - "format", - "name" - ] - }, - "scalar_variable": { - "type": "array", - "items": [ - {"type": "string", "description": "name of the variable"}, - {"$ref":"#/definitions/scalarVal", "description": "value of the variable"} - ], - "additionalItems": {"type": "boolean", "enum": [true], "description": "Whether this is a cloud variable"}, - "maxItems": 3 - }, - "list": { - "type": "array", - "items": [ - {"type":"string", "description": "name of the list"}, - { - "type": "array", - "description": "contents of the list", - "items": {"$ref":"#/definitions/scalarVal"} - } - ], - "additionalItems": false - }, - "broadcast_message": { - "type": "string", - "description": "the message being broadcasted" - }, - "num_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [4,5,6,7,8] - }, - {"$ref":"#/definitions/stringOrNumber"} - ], - "additionalItems": false - }, - "color_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [9] - }, - { - "type": "string", - "pattern": "^#[a-fA-F0-9]{6}$" - } - ], - "additionalItems": false - }, - "text_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [10] - }, - {"$ref":"#/definitions/stringOrNumber"} - ], - "additionalItems": false - }, - "broadcast_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [11] - }, - {"type": "string", "description": "broadcast message"}, - {"type": "string", "description": "broadcast message id"} - ], - "additionalItems": false - }, - "variable_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [12] - }, - {"type": "string", "description": "variable name"}, - {"type": "string", "description": "variable id"} - ], - "additionalItems": { - "type": "number" - }, - "minItems": 3, - "maxItems": 5 - }, - "list_primitive": { - "type": "array", - "items": [ - { - "type": "number", - "enum": [13] - }, - {"type": "string", "description": "list name"}, - {"type": "string", "description": "list id"} - ], - "additionalItems": { - "type": "number" - }, - "minItems": 3, - "maxItems": 5 - }, - "topLevelPrimitive": { - "oneOf": [ - {"$ref":"#/definitions/variable_primitive"}, - {"$ref":"#/definitions/list_primitive"} - ] - }, - "inputPrimitive": { - "oneOf": [ - {"$ref":"#/definitions/num_primitive"}, - {"$ref":"#/definitions/color_primitive"}, - {"$ref":"#/definitions/text_primitive"}, - {"$ref":"#/definitions/broadcast_primitive"}, - {"$ref":"#/definitions/variable_primitive"}, - {"$ref":"#/definitions/list_primitive"} - ] - }, - "block": { - "type": "object", - "properties": { - "opcode": { - "type": "string" - }, - "inputs": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": [ - { - "type":"number", - "enum":[1,2,3], - "description": "1 = unobscured shadow, 2 = no shadow, 3 = obscured shadow" - } - ], - "additionalItems": { - "oneOf": [ - {"$ref":"#/definitions/optionalString"}, - {"$ref":"#/definitions/inputPrimitive"} - ] - } - } - }, - "fields": { - "type": "object" - }, - "next": {"$ref":"#/definitions/optionalString"}, - "topLevel": { - "type": "boolean" - }, - "parent": {"$ref":"#/definitions/optionalString"}, - "shadow": { - "type": "boolean" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "mutation": { - "type": "object", - "properties": { - "tagName": { - "type": "string", - "enum": ["mutation"] - }, - "children": { - "type": "array" - }, - "proccode": { - "type": "string" - }, - "argumentids": { - "type": "string" - }, - "warp": {"$ref":"#/definitions/boolOrBoolString"}, - "hasnext": {"$ref":"#/definitions/boolOrBoolString"} - } - } - }, - "required": [ - "opcode" - ] - }, - "stage": { - "type": "object", - "description": "Description of property (and/or property/value pairs) that are unique to the stage.", - "properties": { - "name": { - "type": "string", - "enum": ["Stage"] - }, - "isStage": { - "type": "boolean", - "enum": [true] - }, - "tempo": { - "type": "number" - }, - "videoTransparency": { - "type": "number" - }, - "videoState": { - "type": "string", - "enum": ["on", "off", "on-flipped"] - } - }, - "required": [ - "name", - "isStage" - ] - }, - "sprite": { - "type": "object", - "description": "Description of property (and/or property/value pairs) for sprites.", - "properties": { - "name": { - "type": "string", - "not": {"enum": ["Stage", "stage"]} - }, - "isStage": { - "type": "boolean", - "enum": [false] - }, - "visible": { - "type": "boolean" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "size": { - "type": "number" - }, - "direction": { - "type": "number" - }, - "draggable": { - "type": "boolean" - }, - "rotationStyle": { - "type": "string", - "enum": ["all around", "don't rotate", "left-right"] - } - }, - "required": [ - "name", - "isStage" - ] - }, - "target": { - "type": "object", - "description" : "Properties common to both Scratch 3.0 Stage and Sprite", - "properties": { - "currentCostume": { - "type": "integer", - "minimum": 0 - }, - "blocks": { - "type": "object", - "additionalProperties": { - "oneOf": [ - {"$ref":"#/definitions/block"}, - {"$ref":"#/definitions/topLevelPrimitive"} - ] - } - }, - "variables": { - "type": "object", - "additionalProperties": {"$ref":"#/definitions/scalar_variable"} - }, - "lists": { - "type": "object", - "additionalProperties": {"$ref":"#/definitions/list"} - }, - "broadcasts": { - "type": "object", - "additionalProperties": {"$ref":"#/definitions/broadcast_message"} - }, - "costumes": { - "type": "array", - "items": {"$ref":"#/definitions/costume"}, - "minItems": 1, - "uniqueItems": true - }, - "sounds": { - "type": "array", - "items": {"$ref":"#/definitions/sound"}, - "uniqueItems": true - }, - "volume": { - "type": "number" - } - }, - "required": [ - "variables", - "costumes", - "sounds", - "blocks" - ] - } - }, "type": "object", "properties": { "meta": { @@ -427,21 +23,20 @@ "semver" ] }, - "targets": { "type": "array", "items": [ { "allOf": [ - {"$ref": "#/definitions/stage" }, - {"$ref": "#/definitions/target"} + {"$ref": "sb3_definitions.json#/definitions/stage" }, + {"$ref": "sb3_definitions.json#/definitions/target"} ] } ], "additionalItems": { "allOf": [ - {"$ref": "#/definitions/sprite"}, - {"$ref": "#/definitions/target"} + {"$ref": "sb3_definitions.json#/definitions/sprite"}, + {"$ref": "sb3_definitions.json#/definitions/target"} ] } } diff --git a/lib/sprite2_schema.json b/lib/sprite2_schema.json new file mode 100644 index 00000000..8b1ee2ef --- /dev/null +++ b/lib/sprite2_schema.json @@ -0,0 +1,22 @@ +{ + "$id": "https://scratch.mit.edu/sprite2_schema.json", + "description": "Scratch 2.0 project schema", + "type": "object", + "allOf": [ + {"$ref": "sb2_definitions.json#/definitions/sprite_object"}, + { + "type": "object", + "properties": { + "scratchX": {"type": "number"}, + "scratchY": {"type": "number"}, + "scale": {"type": "number"}, + "direction": {"type": "number"}, + "rotationStyle": {"type": "string"}, + "isDraggable": {"type": "boolean"}, + "indexInLibrary": {"type": "number"}, + "visible": {"type": "boolean"}, + "spriteInfo": {"type": "object"} + } + } + ] +} diff --git a/lib/sprite3_schema.json b/lib/sprite3_schema.json new file mode 100644 index 00000000..eb750422 --- /dev/null +++ b/lib/sprite3_schema.json @@ -0,0 +1,42 @@ +{ + "$id": "https://scratch.mit.edu/sprite3_schema.json", + "$schema": "http://json-schema.org/schema#", + "description": "Scratch 3.0 Sprite Schema", + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "semver": { + "type": "string", + "pattern": "^(3.[0-9]+.[0-9]+)$" + }, + "vm": { + "type": "string", + "pattern": "^([0-9]+.[0-9]+.[0-9]+)($|-)" + }, + "agent": { + "type": "string" + } + }, + "required": [ + "semver" + ] + }, + "targets": { + "type": "array", + "items": [ + { + "allOf": [ + {"$ref": "sb3_definitions.json#/definitions/sprite"}, + {"$ref": "sb3_definitions.json#/definitions/target"} + ] + } + ], + "additionalItems": false + } + }, + "required": [ + "targets" + ] +} diff --git a/lib/unpack.js b/lib/unpack.js index 400377f3..0918c57f 100644 --- a/lib/unpack.js +++ b/lib/unpack.js @@ -5,10 +5,12 @@ var unzip = require('./unzip'); * If input is encoded in zip or gzip format, the input will be extracted and decoded. * If input is a string, passes that string along to the given callback. * @param {Buffer | string} input Project data + * @param {boolean} isSprite Whether the input should be treated as + * a sprite (true) or a whole project (false) * @param {Function} callback Error or stringified project data * @return {void} */ -module.exports = function (input, callback) { +module.exports = function (input, isSprite, callback) { if (typeof input === 'string') { // Pass string to callback return callback(null, [input, null]); @@ -46,5 +48,5 @@ module.exports = function (input, callback) { // Return error if legacy encoding detected if (isLegacy) return callback('Parser only supports Scratch 2.X and above'); - unzip(input, isGZip, callback); + unzip(input, isGZip, isSprite, callback); }; diff --git a/lib/unzip.js b/lib/unzip.js index 033053c0..0d643111 100644 --- a/lib/unzip.js +++ b/lib/unzip.js @@ -5,10 +5,12 @@ var GZip = require('gzip-js'); * Unpacks a zip or gzip file. * @param {string} input Zip file provided as a string * @param {boolean} isGZip Whether the input is a GZip file, otherwise treat as zip + * @param {boolean} isSprite Whether the input should be treated as + * a sprite (true) or whole project (false) * @param {array} callback Array including both the project and zip archive * @return {void} */ -module.exports = function (input, isGZip, callback) { +module.exports = function (input, isGZip, isSprite, callback) { var msg = 'Failed to unzip and extract project.json, with error: '; if (isGZip) { var unpackedProject = null; @@ -21,7 +23,7 @@ module.exports = function (input, isGZip, callback) { } return JSZip.loadAsync(input) .then(function (zip) { - return zip.file('project.json').async('string') + return zip.file(isSprite ? 'sprite.json' : 'project.json').async('string') .then(function (project) { return callback(null, [project, zip]); }); diff --git a/lib/validate.js b/lib/validate.js index 3ea3bcbc..5af3a80c 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,10 +1,15 @@ var ajv = require('ajv')(); -var sb2schema = require('./sb2_schema.json'); -var sb3schema = require('./sb3_schema.json'); +var sb2Defs = require('./sb2_definitions.json'); +var sb3Defs = require('./sb3_definitions.json'); +var sb2Schema = require('./sb2_schema.json'); +var sb3Schema = require('./sb3_schema.json'); +var sprite2Schema = require('./sprite2_schema.json'); +var sprite3Schema = require('./sprite3_schema.json'); +ajv.addSchema(sb2Defs).addSchema(sb3Defs); -module.exports = function (input, callback) { - var validateSb2 = ajv.compile(sb2schema); - var validateSb3 = ajv.compile(sb3schema); +module.exports = function (isSprite, input, callback) { + var validateSb2 = ajv.compile(isSprite ? sprite2Schema : sb2Schema); + var validateSb3 = ajv.compile(isSprite ? sprite3Schema : sb3Schema); var isValidSb2 = validateSb2(input); diff --git a/test/fixtures/data.js b/test/fixtures/data.js index 09e0094d..91b6bf53 100644 --- a/test/fixtures/data.js +++ b/test/fixtures/data.js @@ -5,14 +5,19 @@ var path = require('path'); // Build file listings var sb = glob.sync(path.resolve(__dirname, './data/*.sb')); var sb2 = glob.sync(path.resolve(__dirname, './data/*.sb2')); +var sprite2 = glob.sync(path.resolve(__dirname, './data/*.sprite2')); +// Sprite2 and Sprite3 jsons have modified file extensions +// so that they don't get caught here but can still be used by the +// validate unit tests var json = glob.sync(path.resolve(__dirname, './data/*.json')); var gzipJson = glob.sync(path.resolve(__dirname, './data/*.json.gz')); // Read files and convert to buffers -for (var w in sb) sb[w] = fs.readFileSync(sb[w]); -for (var x in sb2) sb2[x] = fs.readFileSync(sb2[x]); -for (var y in json) json[y] = fs.readFileSync(json[y]); -for (var z in gzipJson) gzipJson[z] = fs.readFileSync(gzipJson[z]); +for (var a in sb) sb[a] = fs.readFileSync(sb[a]); +for (var b in sb2) sb2[b] = fs.readFileSync(sb2[b]); +for (var c in json) json[c] = fs.readFileSync(json[c]); +for (var d in gzipJson) gzipJson[d] = fs.readFileSync(gzipJson[d]); +for (var e in sprite2) sprite2[e] = fs.readFileSync(sprite2[e]); // Return listings module.exports = { @@ -28,8 +33,15 @@ module.exports = { json: fs.readFileSync(path.resolve(__dirname, './data/_example.json')), gzipJson: fs.readFileSync(path.resolve(__dirname, './data/_example.json.gz')) }, + sprites: { + default_cat_sprite2: fs.readFileSync(path.resolve(__dirname, './data/_default_cat.sprite2')), + default_cat_sprite2_json: fs.readFileSync(path.resolve(__dirname, './data/_default_cat.sprite2json')), + example_sprite2: fs.readFileSync(path.resolve(__dirname, './data/_example_sprite.sprite2')), + example_sprite2_json: fs.readFileSync(path.resolve(__dirname, './data/_example_sprite.sprite2json')) + }, sb: sb, sb2: sb2, json: json, - gzipJson: gzipJson + gzipJson: gzipJson, + sprite2: sprite2 }; diff --git a/test/fixtures/data/_default_cat.sprite2 b/test/fixtures/data/_default_cat.sprite2 new file mode 100644 index 00000000..9c6248f8 Binary files /dev/null and b/test/fixtures/data/_default_cat.sprite2 differ diff --git a/test/fixtures/data/_default_cat.sprite2json b/test/fixtures/data/_default_cat.sprite2json new file mode 100755 index 00000000..b661a348 --- /dev/null +++ b/test/fixtures/data/_default_cat.sprite2json @@ -0,0 +1,38 @@ +{ + "objName": "Sprite1", + "sounds": [{ + "soundName": "meow", + "soundID": 0, + "md5": "83c36d806dc92327b9e7049a565c6bff.wav", + "sampleCount": 18688, + "rate": 22050, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 0, + "baseLayerMD5": "f9a1c175dbe2e5dee472858dd30d16bb.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }, + { + "costumeName": "costume2", + "baseLayerID": 1, + "baseLayerMD5": "6e8bd9ae68fdb02b7e1e3df656a75635.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 100000, + "visible": true, + "spriteInfo": { + } +} diff --git a/test/fixtures/data/_example_sprite.sprite2 b/test/fixtures/data/_example_sprite.sprite2 new file mode 100644 index 00000000..2cd8818b Binary files /dev/null and b/test/fixtures/data/_example_sprite.sprite2 differ diff --git a/test/fixtures/data/_example_sprite.sprite2json b/test/fixtures/data/_example_sprite.sprite2json new file mode 100755 index 00000000..532378e2 --- /dev/null +++ b/test/fixtures/data/_example_sprite.sprite2json @@ -0,0 +1,131 @@ +{ + "objName": "1080 Hip-Hop", + "scripts": [[27, + 26, + [["whenKeyPressed", "space"], + ["doRepeat", 10, [["playSound:", "dance celebrate"]]], + ["doForever", [["changeGraphicEffect:by:", "color", 25]]]]]], + "sounds": [{ + "soundName": "dance celebrate", + "soundID": 0, + "md5": "0edb8fb88af19e6e17d0f8cf64c1d136.wav", + "sampleCount": 176401, + "rate": 22050, + "format": "adpcm" + }], + "costumes": [{ + "costumeName": "1080 stance", + "baseLayerID": 0, + "baseLayerMD5": "b50767d57a9979120db126ab0e78c967.png", + "bitmapResolution": 2, + "rotationCenterX": 70, + "rotationCenterY": 278 + }, + { + "costumeName": "1080 top stand", + "baseLayerID": 1, + "baseLayerMD5": "2beeccef2956cf00f3fe04a70941a304.png", + "bitmapResolution": 2, + "rotationCenterX": 74, + "rotationCenterY": 274 + }, + { + "costumeName": "1080 top R step", + "baseLayerID": 2, + "baseLayerMD5": "877253c7955364d01d3cdf6c00ad8f26.png", + "bitmapResolution": 2, + "rotationCenterX": 200, + "rotationCenterY": 270 + }, + { + "costumeName": "1080 top L step", + "baseLayerID": 3, + "baseLayerMD5": "0182ebabc5f0e2ba2f8c5a88543c841a.png", + "bitmapResolution": 2, + "rotationCenterX": 144, + "rotationCenterY": 266 + }, + { + "costumeName": "1080 top freeze", + "baseLayerID": 4, + "baseLayerMD5": "eeb7ec45ab14f11c59faa86d68aa6711.png", + "bitmapResolution": 2, + "rotationCenterX": 54, + "rotationCenterY": 258 + }, + { + "costumeName": "1080 top R cross", + "baseLayerID": 5, + "baseLayerMD5": "69eae69a7bb4951d2bf44031d709e9b7.png", + "bitmapResolution": 2, + "rotationCenterX": 206, + "rotationCenterY": 252 + }, + { + "costumeName": "1080 pop front", + "baseLayerID": 6, + "baseLayerMD5": "dd96cd3037b3e48814529bbf1d615bea.png", + "bitmapResolution": 2, + "rotationCenterX": 72, + "rotationCenterY": 266 + }, + { + "costumeName": "1080 pop down", + "baseLayerID": 7, + "baseLayerMD5": "0a4fe68e96f9194f3b5a039af8598c05.png", + "bitmapResolution": 2, + "rotationCenterX": 74, + "rotationCenterY": 188 + }, + { + "costumeName": "1080 pop left", + "baseLayerID": 8, + "baseLayerMD5": "097b4b61b323e4041315992f41196ec9.png", + "bitmapResolution": 2, + "rotationCenterX": 184, + "rotationCenterY": 266 + }, + { + "costumeName": "1080 pop right", + "baseLayerID": 9, + "baseLayerMD5": "9ab30ea9351f88b0bfcc7c1eff219ec4.png", + "bitmapResolution": 2, + "rotationCenterX": 78, + "rotationCenterY": 276 + }, + { + "costumeName": "1080 pop L arm", + "baseLayerID": 10, + "baseLayerMD5": "532ca24749bd0968b329c0970209805b.png", + "bitmapResolution": 2, + "rotationCenterX": 100, + "rotationCenterY": 280 + }, + { + "costumeName": "1080 pop stand", + "baseLayerID": 11, + "baseLayerMD5": "b140b94daf02503e0abfc1ec284c6ccd.png", + "bitmapResolution": 2, + "rotationCenterX": 92, + "rotationCenterY": 280 + }, + { + "costumeName": "1080 pop R arm", + "baseLayerID": 12, + "baseLayerMD5": "c69c7b764530b8a464f2ca4e5e85c303.png", + "bitmapResolution": 2, + "rotationCenterX": 74, + "rotationCenterY": 278 + }], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 100000, + "visible": true, + "spriteInfo": { + } +} diff --git a/test/integration/empty.js b/test/integration/empty.js index 6aa55561..cc9494f3 100644 --- a/test/integration/empty.js +++ b/test/integration/empty.js @@ -4,7 +4,7 @@ var data = require('../fixtures/data'); var parser = require('../../index'); test('sb', function (t) { - parser(data.empty.sb, function (err, res) { + parser(data.empty.sb, false, function (err, res) { t.type(err, 'string'); t.type(res, 'undefined'); t.end(); @@ -12,7 +12,7 @@ test('sb', function (t) { }); test('sb2', function (t) { - parser(data.empty.sb2, function (err, result) { + parser(data.empty.sb2, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -25,7 +25,7 @@ test('sb2', function (t) { }); test('json', function (t) { - parser(data.empty.json, function (err, result) { + parser(data.empty.json, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -38,7 +38,7 @@ test('json', function (t) { }); test('json string', function (t) { - parser(data.empty.json.toString('utf-8'), function (err, result) { + parser(data.empty.json.toString('utf-8'), false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -51,7 +51,7 @@ test('json string', function (t) { }); test('gzipped json', function (t) { - parser(data.empty.gzipJson, function (err, result) { + parser(data.empty.gzipJson, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; diff --git a/test/integration/example.js b/test/integration/example.js index b1e266a4..7dac9ad4 100644 --- a/test/integration/example.js +++ b/test/integration/example.js @@ -4,7 +4,7 @@ var data = require('../fixtures/data'); var parser = require('../../index'); test('sb', function (t) { - parser(data.example.sb, function (err, res) { + parser(data.example.sb, false, function (err, res) { t.type(err, 'string'); t.type(res, 'undefined'); t.end(); @@ -12,7 +12,7 @@ test('sb', function (t) { }); test('sb2', function (t) { - parser(data.example.sb2, function (err, result) { + parser(data.example.sb2, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -25,7 +25,7 @@ test('sb2', function (t) { }); test('json', function (t) { - parser(data.example.json, function (err, result) { + parser(data.example.json, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -38,7 +38,7 @@ test('json', function (t) { }); test('json string', function (t) { - parser(data.example.json.toString('utf-8'), function (err, result) { + parser(data.example.json.toString('utf-8'), false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -51,7 +51,7 @@ test('json string', function (t) { }); test('gzipped json', function (t) { - parser(data.example.gzipJson, function (err, result) { + parser(data.example.gzipJson, false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; diff --git a/test/integration/sprites.js b/test/integration/sprites.js new file mode 100644 index 00000000..a12b1d1d --- /dev/null +++ b/test/integration/sprites.js @@ -0,0 +1,56 @@ +var test = require('tap').test; +var JSZip = require('jszip'); +var data = require('../fixtures/data'); +var parser = require('../../index'); + +test('default cat sprite2', function (t) { + parser(data.sprites.default_cat_sprite2, true, function (err, result) { + t.equal(err, null); + t.equal(Array.isArray(result), true); + var res = result[0]; + var possibleZip = result[1]; + t.type(res, 'object'); + t.equal(res.projectVersion, 2); + t.equal(possibleZip instanceof JSZip, true); + t.end(); + }); +}); + +test('example sprite2', function (t) { + parser(data.sprites.example_sprite2, true, function (err, result) { + t.equal(err, null); + t.equal(Array.isArray(result), true); + var res = result[0]; + var possibleZip = result[1]; + t.type(res, 'object'); + t.equal(res.projectVersion, 2); + t.equal(possibleZip instanceof JSZip, true); + t.end(); + }); +}); + +test('default cat sprite2 json', function (t) { + parser(data.sprites.default_cat_sprite2_json, true, function (err, result) { + t.equal(err, null); + t.equal(Array.isArray(result), true); + var res = result[0]; + var possibleZip = result[1]; + t.type(res, 'object'); + t.equal(res.projectVersion, 2); + t.equal(possibleZip, null); + t.end(); + }); +}); + +test('default cat sprite2 json', function (t) { + parser(data.sprites.example_sprite2_json, true, function (err, result) { + t.equal(err, null); + t.equal(Array.isArray(result), true); + var res = result[0]; + var possibleZip = result[1]; + t.type(res, 'object'); + t.equal(res.projectVersion, 2); + t.equal(possibleZip, null); + t.end(); + }); +}); diff --git a/test/integration/stress.js b/test/integration/stress.js index 0fd60d20..eee5f4ae 100644 --- a/test/integration/stress.js +++ b/test/integration/stress.js @@ -7,7 +7,7 @@ test('sb', function (t) { var set = data.sb; t.plan(set.length * 2); for (var i in data.sb) { - parser(data.sb[i], function (err, res) { + parser(data.sb[i], false, function (err, res) { t.type(err, 'string'); t.type(res, 'undefined'); }); @@ -18,7 +18,7 @@ test('sb2', function (t) { var set = data.sb2; t.plan(set.length * 5); for (var i in data.sb2) { - parser(data.sb2[i], function (err, result) { + parser(data.sb2[i], false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -34,7 +34,7 @@ test('json', function (t) { var set = data.json; t.plan(set.length * 5); for (var i in data.json) { - parser(data.json[i], function (err, result) { + parser(data.json[i], false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; @@ -50,7 +50,7 @@ test('json string', function (t) { var set = data.json; t.plan(set.length * 5); for (var i in data.json) { - parser(data.json[i].toString('utf-8'), function (err, result) { + parser(data.json[i].toString('utf-8'), false, function (err, result) { t.equal(err, null); t.equal(Array.isArray(result), true); var res = result[0]; diff --git a/test/unit/unpack.js b/test/unit/unpack.js index 70cf115f..856c9256 100644 --- a/test/unit/unpack.js +++ b/test/unit/unpack.js @@ -21,7 +21,7 @@ test('spec', function (t) { test('sb', function (t) { var buffer = new Buffer(fixtures.sb); - unpack(buffer, function (err, res) { + unpack(buffer, false, function (err, res) { t.type(err, 'string'); t.type(res, 'undefined'); t.end(); @@ -30,7 +30,7 @@ test('sb', function (t) { test('sb2', function (t) { var buffer = new Buffer(fixtures.sb2); - unpack(buffer, function (err, res) { + unpack(buffer, false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -42,9 +42,18 @@ test('sb2', function (t) { }); }); +test('sb2 does not validate as sprite', function (t) { + var buffer = new Buffer(fixtures.sb2); + unpack(buffer, true, function (err, res) { + t.type(err, 'string'); + t.type(res, 'undefined'); + t.end(); + }); +}); + test('json', function (t) { var buffer = new Buffer(fixtures.json); - unpack(buffer, function (err, res) { + unpack(buffer, false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -58,7 +67,7 @@ test('json', function (t) { test('json utf-8 string', function (t) { var buffer = new Buffer(fixtures.json); - unpack(buffer.toString('utf-8'), function (err, res) { + unpack(buffer.toString('utf-8'), false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -71,7 +80,16 @@ test('json utf-8 string', function (t) { }); test('invalid string', function (t) { - unpack('this is not json', function (err, res) { + unpack('this is not json', false, function (err, res) { + t.equal(err, null); + t.equal(Array.isArray(res), true); + t.type(res[0], 'string'); + t.throws(function () { + JSON.parse(res[0]); + }); + t.equal(res[1], null); + }); + unpack('this is not json', true, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -85,7 +103,7 @@ test('invalid string', function (t) { test('undefined', function (t) { var foo; - unpack(foo, function (err, res) { + unpack(false, foo, function (err, res) { t.type(err, 'string'); t.type(res, 'undefined'); t.end(); @@ -93,7 +111,7 @@ test('undefined', function (t) { }); test('null', function (t) { - unpack(null, function (err, obj) { + unpack(false, null, function (err, obj) { t.type(err, 'string'); t.type(obj, 'undefined'); t.end(); @@ -101,7 +119,7 @@ test('null', function (t) { }); test('object', function (t) { - unpack({}, function (err, obj) { + unpack(false, {}, function (err, obj) { t.type(err, 'string'); t.type(obj, 'undefined'); t.end(); diff --git a/test/unit/unzip.js b/test/unit/unzip.js index e81c25fc..4a0178c5 100644 --- a/test/unit/unzip.js +++ b/test/unit/unzip.js @@ -18,6 +18,8 @@ for (var i in fixtures) { fixtures[i] = fs.readFileSync(fixtures[i]); } +var errorMessage = 'Failed to unzip and extract project.json'; + test('spec', function (t) { t.type(unzip, 'function'); t.end(); @@ -25,9 +27,8 @@ test('spec', function (t) { test('sb', function (t) { var buffer = new Buffer(fixtures.sb); - unzip(buffer, false, function (err, res) { + unzip(buffer, false, false, function (err, res) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(res, 'undefined'); t.end(); @@ -36,7 +37,7 @@ test('sb', function (t) { test('sb2', function (t) { var buffer = new Buffer(fixtures.sb2); - unzip(buffer, false, function (err, res) { + unzip(buffer, false, false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -50,7 +51,7 @@ test('sb2', function (t) { test('gzipped JSON', function (t) { var buffer = new Buffer(fixtures.gzipJSON); - unzip(buffer, true, function (err, res) { + unzip(buffer, true, false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -64,9 +65,8 @@ test('gzipped JSON', function (t) { test('zip without project json', function (t) { var buffer = new Buffer(fixtures.zipNoProjectJSON); - unzip(buffer, false, function (err, res) { + unzip(buffer, false, false, function (err, res) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(res, 'undefined'); t.end(); @@ -75,7 +75,7 @@ test('zip without project json', function (t) { test('zip with fake project json', function (t) { var buffer = new Buffer(fixtures.zipFakeProjectJSON); - unzip(buffer, false, function (err, res) { + unzip(buffer, false, false, function (err, res) { t.equal(err, null); t.equal(Array.isArray(res), true); t.type(res[0], 'string'); @@ -88,20 +88,38 @@ test('zip with fake project json', function (t) { }); }); -test('random string instead of zip #1', function (t) { - unzip('this is not a zip', false, function (err, res) { +var randomString = 'this is not a zip'; + +test('random string instead of zip, whole project', function (t) { + unzip(randomString, false, false, function (err, res) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(res, 'undefined'); + t.end(); + }); +}); + +test('random string instead of zip, sprite', function (t) { + unzip(randomString, false, true, function (err, res) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(res, 'undefined'); + t.end(); + }); +}); + +test('random string instead of gzip, whole project ', function (t) { + unzip(randomString, true, false, function (err, res) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(res, 'undefined'); t.end(); }); }); -test('random string instead of zip #2', function (t) { - unzip('this is not a zip', true, function (err, res) { +test('random string instead of gzip, sprite', function (t) { + unzip(randomString, true, true, function (err, res) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(res, 'undefined'); t.end(); @@ -110,9 +128,8 @@ test('random string instead of zip #2', function (t) { test('undefined', function (t) { var foo; - unzip(foo, false, function (err, obj) { + unzip(foo, false, false, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); @@ -121,49 +138,80 @@ test('undefined', function (t) { test('undefined isGZip', function (t) { var foo; - unzip(foo, true, function (err, obj) { + unzip(foo, true, false, function (err, obj) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(obj, 'undefined'); + t.end(); + }); +}); + +test('null instead of zip, whole project', function (t) { + unzip(null, false, false, function (err, obj) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(obj, 'undefined'); + t.end(); + }); +}); + +test('null instead of zip, sprite', function (t) { + unzip(null, false, true, function (err, obj) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(obj, 'undefined'); + t.end(); + }); +}); + +test('null instead of gzip, whole project', function (t) { + unzip(null, true, false, function (err, obj) { + t.type(err, 'string'); + t.equal(err.startsWith(errorMessage), true); + t.type(obj, 'undefined'); + t.end(); + }); +}); + +test('null instead of gzip, sprite', function (t) { + unzip(null, true, true, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); }); }); -test('null', function (t) { - unzip(null, false, function (err, obj) { +test('object instead of zip, whole project', function (t) { + unzip({}, false, false, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); }); }); -test('null isGZip', function (t) { - unzip(null, true, function (err, obj) { +test('object instead of zip, sprite', function (t) { + unzip({}, false, true, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); }); }); -test('object', function (t) { - unzip({}, false, function (err, obj) { +test('object instead of gzip, whole project', function (t) { + unzip({}, true, false, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); }); }); -test('object isGZip', function (t) { - unzip({}, true, function (err, obj) { +test('object instead of gzip, sprite', function (t) { + unzip({}, true, false, function (err, obj) { t.type(err, 'string'); - var errorMessage = 'Failed to unzip and extract project.json'; t.equal(err.startsWith(errorMessage), true); t.type(obj, 'undefined'); t.end(); diff --git a/test/unit/validate.js b/test/unit/validate.js index ab1f986e..916e1582 100644 --- a/test/unit/validate.js +++ b/test/unit/validate.js @@ -7,16 +7,56 @@ test('spec', function (t) { t.end(); }); -test('valid', function (t) { - validate(JSON.parse(data.example.json), function (err, res) { +test('valid sb2 project', function (t) { + validate(false, JSON.parse(data.example.json), function (err, res) { t.equal(err, null); t.type(res, 'object'); t.end(); }); }); -test('invalid', function (t) { - validate({foo: 1}, function (err, res) { +test('valid sprite2', function (t) { + validate(true, JSON.parse(data.sprites.default_cat_sprite2_json), function (err, res) { + t.equal(err, null); + t.type(res, 'object'); + t.end(); + }); +}); + +// Note, the way the sb2/sprite2 validation is written, a valid sb2 project can actually +// validate as a sprite2 file. The opposite should not be true. + +test('valid sprite2 is not a valid project', function (t) { + validate(false, JSON.parse(data.sprites.default_cat_sprite2_json), function (err, res) { + t.type(err, 'object'); + t.type(err.validationError, 'string'); + var sb2Errs = err.sb2Errors; + t.equal(Array.isArray(sb2Errs), true); + t.type(res, 'undefined'); + t.end(); + }); +}); + +test('invalid, whole project', function (t) { + validate(false, {foo: 1}, function (err, res) { + t.type(err, 'object'); + t.type(err.validationError, 'string'); + var sb2Errs = err.sb2Errors; + t.equal(Array.isArray(sb2Errs), true); + t.type(res, 'undefined'); + t.type(sb2Errs[0], 'object'); + t.type(sb2Errs[0].keyword, 'string'); + t.type(sb2Errs[0].dataPath, 'string'); + t.type(sb2Errs[0].schemaPath, 'string'); + t.type(sb2Errs[0].message, 'string'); + t.type(sb2Errs[0].params, 'object'); + t.type(sb2Errs[0].params.missingProperty, 'string'); + t.end(); + }); +}); + +test('invalid, sprite', function (t) { + validate(true, {foo: 1}, function (err, res) { t.type(err, 'object'); t.type(err.validationError, 'string'); var sb2Errs = err.sb2Errors;