@willwade/aac-processors 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -11
- package/dist/cli/index.js +87 -0
- package/dist/core/analyze.js +1 -0
- package/dist/core/baseProcessor.d.ts +3 -0
- package/dist/core/fileProcessor.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/optional/symbolTools.js +4 -2
- package/dist/processors/gridset/helpers.d.ts +3 -1
- package/dist/processors/gridset/helpers.js +24 -5
- package/dist/processors/gridset/password.d.ts +11 -0
- package/dist/processors/gridset/password.js +37 -0
- package/dist/processors/gridset/resolver.d.ts +1 -1
- package/dist/processors/gridset/resolver.js +8 -4
- package/dist/processors/gridset/wordlistHelpers.d.ts +2 -2
- package/dist/processors/gridset/wordlistHelpers.js +7 -4
- package/dist/processors/gridsetProcessor.d.ts +15 -1
- package/dist/processors/gridsetProcessor.js +64 -22
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +5 -2
- package/dist/processors/obfProcessor.d.ts +7 -0
- package/dist/processors/obfProcessor.js +9 -0
- package/dist/processors/snap/helpers.d.ts +2 -0
- package/dist/processors/snap/helpers.js +2 -0
- package/dist/processors/snapProcessor.d.ts +7 -0
- package/dist/processors/snapProcessor.js +9 -0
- package/dist/processors/touchchatProcessor.d.ts +7 -0
- package/dist/processors/touchchatProcessor.js +9 -0
- package/dist/utilities/screenshotConverter.d.ts +69 -0
- package/dist/utilities/screenshotConverter.js +453 -0
- package/dist/validation/baseValidator.d.ts +80 -0
- package/dist/validation/baseValidator.js +160 -0
- package/dist/validation/gridsetValidator.d.ts +36 -0
- package/dist/validation/gridsetValidator.js +288 -0
- package/dist/validation/index.d.ts +13 -0
- package/dist/validation/index.js +69 -0
- package/dist/validation/obfValidator.d.ts +44 -0
- package/dist/validation/obfValidator.js +530 -0
- package/dist/validation/snapValidator.d.ts +33 -0
- package/dist/validation/snapValidator.js +237 -0
- package/dist/validation/touchChatValidator.d.ts +33 -0
- package/dist/validation/touchChatValidator.js +229 -0
- package/dist/validation/validationTypes.d.ts +64 -0
- package/dist/validation/validationTypes.js +15 -0
- package/examples/README.md +7 -0
- package/examples/demo.js +143 -0
- package/examples/obf/aboutme.json +376 -0
- package/examples/obf/array.json +6 -0
- package/examples/obf/hash.json +4 -0
- package/examples/obf/links.obz +0 -0
- package/examples/obf/simple.obf +53 -0
- package/examples/package-lock.json +1326 -0
- package/examples/package.json +10 -0
- package/examples/styling-example.ts +316 -0
- package/examples/translate.js +39 -0
- package/examples/translate_demo.js +254 -0
- package/examples/typescript-demo.ts +251 -0
- package/package.json +3 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.ObfValidator = void 0;
|
|
30
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
31
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
32
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
33
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
34
|
+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
35
|
+
const jszip_1 = __importDefault(require("jszip"));
|
|
36
|
+
const baseValidator_1 = require("./baseValidator");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const OBF_FORMAT = 'open-board-0.1';
|
|
40
|
+
const OBF_FORMAT_CURRENT_VERSION = 0.1;
|
|
41
|
+
/**
|
|
42
|
+
* Validator for Open Board Format (OBF/OBZ) files
|
|
43
|
+
*/
|
|
44
|
+
class ObfValidator extends baseValidator_1.BaseValidator {
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate an OBF file from disk
|
|
50
|
+
*/
|
|
51
|
+
static async validateFile(filePath) {
|
|
52
|
+
const validator = new ObfValidator();
|
|
53
|
+
const content = fs.readFileSync(filePath);
|
|
54
|
+
const stats = fs.statSync(filePath);
|
|
55
|
+
return validator.validate(content, path.basename(filePath), stats.size);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if content is OBF format
|
|
59
|
+
*/
|
|
60
|
+
static async identifyFormat(content, filename) {
|
|
61
|
+
const name = filename.toLowerCase();
|
|
62
|
+
if (name.endsWith('.obf') || name.endsWith('.obz')) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Try to parse as JSON and check format
|
|
66
|
+
try {
|
|
67
|
+
const contentStr = Buffer.isBuffer(content) ? content.toString() : content;
|
|
68
|
+
const json = JSON.parse(contentStr);
|
|
69
|
+
return json && json.format && json.format.startsWith('open-board-');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Main validation method
|
|
77
|
+
*/
|
|
78
|
+
async validate(content, filename, filesize) {
|
|
79
|
+
this.reset();
|
|
80
|
+
// Determine if it's OBF or OBZ
|
|
81
|
+
const isObz = filename.toLowerCase().endsWith('.obz');
|
|
82
|
+
if (isObz) {
|
|
83
|
+
return await this.validateObz(content, filename, filesize);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return await this.validateObf(content, filename, filesize);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate OBF content
|
|
91
|
+
*/
|
|
92
|
+
async validateObf(content, filename, filesize) {
|
|
93
|
+
await this.add_check('filename', 'file name', async () => {
|
|
94
|
+
if (!filename.match(/\.obf$/)) {
|
|
95
|
+
this.warn('filename should end with .obf');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
let json = null;
|
|
99
|
+
await this.add_check('valid_json', 'JSON file', async () => {
|
|
100
|
+
try {
|
|
101
|
+
json = JSON.parse(content.toString());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.err("Couldn't parse as JSON", true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
if (!json) {
|
|
108
|
+
return this.buildResult(filename, filesize, 'obf');
|
|
109
|
+
}
|
|
110
|
+
await this.validateBoardStructure(json);
|
|
111
|
+
return this.buildResult(filename, filesize, 'obf');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate OBZ (zip) content
|
|
115
|
+
*/
|
|
116
|
+
async validateObz(content, filename, filesize) {
|
|
117
|
+
await this.add_check('filename', 'file name', async () => {
|
|
118
|
+
if (!filename.match(/\.obz$/)) {
|
|
119
|
+
this.warn('filename should end with .obz');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
let zip = null;
|
|
123
|
+
let validZip = false;
|
|
124
|
+
await this.add_check('zip', 'valid zip', async () => {
|
|
125
|
+
try {
|
|
126
|
+
zip = await jszip_1.default.loadAsync(content);
|
|
127
|
+
validZip = true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
this.err('file is not a valid zip package');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (validZip && zip) {
|
|
134
|
+
await this.validateObzStructure(zip);
|
|
135
|
+
}
|
|
136
|
+
return this.buildResult(filename, filesize, 'obz');
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validate OBF board structure
|
|
140
|
+
*/
|
|
141
|
+
async validateBoardStructure(board) {
|
|
142
|
+
await this.add_check('format_version', 'format version', async () => {
|
|
143
|
+
if (!board.format) {
|
|
144
|
+
this.err(`format attribute is required, set to ${OBF_FORMAT}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const version = parseFloat(board.format.split('-').pop() || '0');
|
|
148
|
+
if (version > OBF_FORMAT_CURRENT_VERSION) {
|
|
149
|
+
this.err(`format version (${version}) is invalid, current version is ${OBF_FORMAT_CURRENT_VERSION}`);
|
|
150
|
+
}
|
|
151
|
+
else if (version < OBF_FORMAT_CURRENT_VERSION) {
|
|
152
|
+
this.warn(`format version (${version}) is old, consider updating to ${OBF_FORMAT_CURRENT_VERSION}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
await this.add_check('id', 'board ID', async () => {
|
|
156
|
+
if (!board.id) {
|
|
157
|
+
this.err('id attribute is required');
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
await this.add_check('locale', 'locale', async () => {
|
|
161
|
+
if (!board.locale) {
|
|
162
|
+
this.err('locale attribute is required, please set to "en" for English');
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
await this.add_check('extras', 'extra attributes', async () => {
|
|
166
|
+
const attrs = [
|
|
167
|
+
'format',
|
|
168
|
+
'id',
|
|
169
|
+
'locale',
|
|
170
|
+
'url',
|
|
171
|
+
'data_url',
|
|
172
|
+
'name',
|
|
173
|
+
'description_html',
|
|
174
|
+
'default_layout',
|
|
175
|
+
'buttons',
|
|
176
|
+
'images',
|
|
177
|
+
'sounds',
|
|
178
|
+
'grid',
|
|
179
|
+
'license',
|
|
180
|
+
];
|
|
181
|
+
Object.keys(board).forEach((key) => {
|
|
182
|
+
if (!attrs.includes(key) && !key.startsWith('ext_')) {
|
|
183
|
+
this.warn(`${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
await this.add_check('description', 'descriptive attributes', async () => {
|
|
188
|
+
if (!board.name) {
|
|
189
|
+
this.warn('name attribute is strongly recommended');
|
|
190
|
+
}
|
|
191
|
+
if (!board.description_html) {
|
|
192
|
+
this.warn('description_html attribute is recommended');
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
await this.add_check('background', 'background attribute', async () => {
|
|
196
|
+
if (board.background && typeof board.background !== 'object') {
|
|
197
|
+
this.err('background attribute must be a hash');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
await this.add_check('buttons', 'buttons attribute', async () => {
|
|
201
|
+
if (!board.buttons) {
|
|
202
|
+
this.err('buttons attribute is required');
|
|
203
|
+
}
|
|
204
|
+
else if (!Array.isArray(board.buttons)) {
|
|
205
|
+
this.err('buttons attribute must be an array');
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
await this.add_check('grid', 'grid attribute', async () => {
|
|
209
|
+
if (!board.grid) {
|
|
210
|
+
this.err('grid attribute is required');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (typeof board.grid !== 'object') {
|
|
214
|
+
this.err('grid attribute must be a hash');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (typeof board.grid.rows !== 'number' || board.grid.rows < 1) {
|
|
218
|
+
this.err('grid.rows must be a positive number');
|
|
219
|
+
}
|
|
220
|
+
if (typeof board.grid.columns !== 'number' || board.grid.columns < 1) {
|
|
221
|
+
this.err('grid.columns must be a positive number');
|
|
222
|
+
}
|
|
223
|
+
if (!board.grid.order || !Array.isArray(board.grid.order)) {
|
|
224
|
+
this.err('grid.order must be an array of arrays');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (board.grid.order.length !== board.grid.rows) {
|
|
228
|
+
this.err(`grid.order length (${board.grid.order.length}) must match grid.rows (${board.grid.rows})`);
|
|
229
|
+
}
|
|
230
|
+
if (!board.grid.order.every((r) => Array.isArray(r) && r.length === board.grid.columns)) {
|
|
231
|
+
this.err(`grid.order must contain ${board.grid.rows} arrays each of size ${board.grid.columns}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
await this.add_check('grid_ids', 'button IDs in grid.order attribute', async () => {
|
|
235
|
+
const buttonIds = (board.buttons || []).map((b) => b.id);
|
|
236
|
+
const usedButtonIds = [];
|
|
237
|
+
if (board.grid && board.grid.order) {
|
|
238
|
+
board.grid.order.forEach((row) => {
|
|
239
|
+
if (Array.isArray(row)) {
|
|
240
|
+
row.forEach((id) => {
|
|
241
|
+
if (id !== null && id !== undefined) {
|
|
242
|
+
usedButtonIds.push(id);
|
|
243
|
+
if (!buttonIds.includes(id)) {
|
|
244
|
+
this.err(`grid.order references button with id ${id} but no button with that id found in buttons attribute`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (usedButtonIds.length === 0) {
|
|
252
|
+
this.warn('board has no buttons defined in the grid');
|
|
253
|
+
}
|
|
254
|
+
const unusedIds = buttonIds.filter((id) => !usedButtonIds.includes(id));
|
|
255
|
+
if (unusedIds.length > 0) {
|
|
256
|
+
this.warn(`not all defined buttons were included in the grid order (${unusedIds.join(',')})`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
await this.add_check('images', 'images attribute', async () => {
|
|
260
|
+
if (!board.images) {
|
|
261
|
+
this.err('images attribute is required');
|
|
262
|
+
}
|
|
263
|
+
else if (!Array.isArray(board.images)) {
|
|
264
|
+
this.err('images attribute must be an array');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (Array.isArray(board.images)) {
|
|
268
|
+
for (let i = 0; i < board.images.length; i++) {
|
|
269
|
+
const image = board.images[i];
|
|
270
|
+
await this.add_check(`image[${i}]`, `image at images[${i}]`, async () => {
|
|
271
|
+
if (typeof image !== 'object') {
|
|
272
|
+
this.err('image must be a hash');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!image.id) {
|
|
276
|
+
this.err('image.id is required');
|
|
277
|
+
}
|
|
278
|
+
if (!image.width || typeof image.width !== 'number' || image.width < 1) {
|
|
279
|
+
this.warn('image.width should be a valid positive number');
|
|
280
|
+
}
|
|
281
|
+
if (!image.height || typeof image.height !== 'number' || image.height < 1) {
|
|
282
|
+
this.warn('image.height should be a valid positive number');
|
|
283
|
+
}
|
|
284
|
+
if (!image.content_type || !image.content_type.match(/^image\/.+$/)) {
|
|
285
|
+
this.err('image.content_type must be a valid image mime type');
|
|
286
|
+
}
|
|
287
|
+
if (!image.url && !image.data && !image.symbol && !image.path) {
|
|
288
|
+
this.err('image must have data, url, path or symbol attribute defined');
|
|
289
|
+
}
|
|
290
|
+
const imageAttrs = [
|
|
291
|
+
'id',
|
|
292
|
+
'width',
|
|
293
|
+
'height',
|
|
294
|
+
'content_type',
|
|
295
|
+
'data',
|
|
296
|
+
'url',
|
|
297
|
+
'symbol',
|
|
298
|
+
'path',
|
|
299
|
+
'data_url',
|
|
300
|
+
'license',
|
|
301
|
+
];
|
|
302
|
+
Object.keys(image).forEach((key) => {
|
|
303
|
+
if (!imageAttrs.includes(key) && !key.startsWith('ext_')) {
|
|
304
|
+
this.warn(`image.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
await this.add_check('sounds', 'sounds attribute', async () => {
|
|
311
|
+
if (!board.sounds) {
|
|
312
|
+
this.err('sounds attribute is required');
|
|
313
|
+
}
|
|
314
|
+
else if (!Array.isArray(board.sounds)) {
|
|
315
|
+
this.err('sounds attribute must be an array');
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
if (Array.isArray(board.sounds)) {
|
|
319
|
+
for (let i = 0; i < board.sounds.length; i++) {
|
|
320
|
+
const sound = board.sounds[i];
|
|
321
|
+
await this.add_check(`sounds[${i}]`, `sound at sounds[${i}]`, async () => {
|
|
322
|
+
if (typeof sound !== 'object') {
|
|
323
|
+
this.err('sound must be a hash');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!sound.id) {
|
|
327
|
+
this.err('sound.id is required');
|
|
328
|
+
}
|
|
329
|
+
if (sound.duration !== undefined &&
|
|
330
|
+
(typeof sound.duration !== 'number' || sound.duration < 0)) {
|
|
331
|
+
this.err('sound.duration must be a valid positive number');
|
|
332
|
+
}
|
|
333
|
+
if (!sound.content_type || !sound.content_type.match(/^audio\/.+$/)) {
|
|
334
|
+
this.err('sound.content_type must be a valid audio mime type');
|
|
335
|
+
}
|
|
336
|
+
if (!sound.url && !sound.data && !sound.path) {
|
|
337
|
+
this.err('sound must have data, url, or path attribute defined');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(board.buttons)) {
|
|
343
|
+
for (let i = 0; i < board.buttons.length; i++) {
|
|
344
|
+
const button = board.buttons[i];
|
|
345
|
+
await this.add_check(`buttons[${i}]`, `button at buttons[${i}]`, async () => {
|
|
346
|
+
await this.validateButton(button);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Validate a single button
|
|
353
|
+
*/
|
|
354
|
+
async validateButton(button) {
|
|
355
|
+
if (typeof button !== 'object') {
|
|
356
|
+
this.err('button must be a hash');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!button.id) {
|
|
360
|
+
this.err('button.id is required');
|
|
361
|
+
}
|
|
362
|
+
if (!button.label) {
|
|
363
|
+
this.err('button.label is required');
|
|
364
|
+
}
|
|
365
|
+
['top', 'left', 'width', 'height'].forEach((attr) => {
|
|
366
|
+
if (button[attr] !== undefined && (typeof button[attr] !== 'number' || button[attr] < 0)) {
|
|
367
|
+
this.warn(`button.${attr} should be a positive number`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
['background_color', 'border_color'].forEach((color) => {
|
|
371
|
+
if (button[color]) {
|
|
372
|
+
if (!button[color].match(/^\s*rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[01]?\.?\d*)?\)\s*/)) {
|
|
373
|
+
this.err(`button.${color} must be a valid rgb or rgba value if defined ("${button[color]}" is invalid)`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
if (button.hidden !== undefined && typeof button.hidden !== 'boolean') {
|
|
378
|
+
this.err('button.hidden must be a boolean if defined');
|
|
379
|
+
}
|
|
380
|
+
if (!button.image_id) {
|
|
381
|
+
this.warn('button.image_id is recommended');
|
|
382
|
+
}
|
|
383
|
+
if (button.action && typeof button.action === 'string' && !button.action.match(/^(:|\+)/)) {
|
|
384
|
+
this.err('button.action must start with either : or + if defined');
|
|
385
|
+
}
|
|
386
|
+
if (button.actions && !Array.isArray(button.actions)) {
|
|
387
|
+
this.err('button.actions must be an array of strings');
|
|
388
|
+
}
|
|
389
|
+
const buttonAttrs = [
|
|
390
|
+
'id',
|
|
391
|
+
'label',
|
|
392
|
+
'vocalization',
|
|
393
|
+
'image_id',
|
|
394
|
+
'sound_id',
|
|
395
|
+
'hidden',
|
|
396
|
+
'background_color',
|
|
397
|
+
'border_color',
|
|
398
|
+
'action',
|
|
399
|
+
'actions',
|
|
400
|
+
'load_board',
|
|
401
|
+
'top',
|
|
402
|
+
'left',
|
|
403
|
+
'width',
|
|
404
|
+
'height',
|
|
405
|
+
];
|
|
406
|
+
Object.keys(button).forEach((key) => {
|
|
407
|
+
if (!buttonAttrs.includes(key) && !key.startsWith('ext_')) {
|
|
408
|
+
this.warn(`button.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Validate OBZ structure
|
|
414
|
+
*/
|
|
415
|
+
async validateObzStructure(zip) {
|
|
416
|
+
let json = null;
|
|
417
|
+
await this.add_check('manifest', 'manifest.json', async () => {
|
|
418
|
+
const manifestFile = zip.file('manifest.json');
|
|
419
|
+
if (!manifestFile) {
|
|
420
|
+
this.err('manifest.json is required in the zip package');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const manifestStr = await manifestFile.async('string');
|
|
425
|
+
json = JSON.parse(manifestStr);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
json = null;
|
|
429
|
+
}
|
|
430
|
+
if (!json) {
|
|
431
|
+
this.err('manifest.json must contain a valid JSON structure');
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (json) {
|
|
435
|
+
await this.validateManifest(json, zip);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Validate manifest structure
|
|
440
|
+
*/
|
|
441
|
+
async validateManifest(manifest, zip) {
|
|
442
|
+
await this.add_check('manifest_format', 'manifest.json format version', async () => {
|
|
443
|
+
if (!manifest.format) {
|
|
444
|
+
this.err(`format attribute is required, set to ${OBF_FORMAT}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const version = parseFloat(manifest.format.split('-').pop());
|
|
448
|
+
if (version > OBF_FORMAT_CURRENT_VERSION) {
|
|
449
|
+
this.err(`format version (${version}) is invalid, current version is ${OBF_FORMAT_CURRENT_VERSION}`);
|
|
450
|
+
}
|
|
451
|
+
else if (version < OBF_FORMAT_CURRENT_VERSION) {
|
|
452
|
+
this.warn(`format version (${version}) is old, consider updating to ${OBF_FORMAT_CURRENT_VERSION}`);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
await this.add_check('manifest_root', 'manifest.json root attribute', async () => {
|
|
456
|
+
if (!manifest.root) {
|
|
457
|
+
this.err('root attribute is required');
|
|
458
|
+
}
|
|
459
|
+
if (!zip.file(manifest.root)) {
|
|
460
|
+
this.err('root attribute must reference a file in the package');
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
await this.add_check('manifest_paths', 'manifest.json paths attribute', async () => {
|
|
464
|
+
if (!manifest.paths || typeof manifest.paths !== 'object') {
|
|
465
|
+
this.err('paths attribute must be a valid hash');
|
|
466
|
+
}
|
|
467
|
+
if (!manifest.paths.boards || typeof manifest.paths.boards !== 'object') {
|
|
468
|
+
this.err('paths.boards must be a valid hash');
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
await this.add_check('manifest_extras', 'manifest.json extra attributes', async () => {
|
|
472
|
+
const attrs = ['format', 'root', 'paths'];
|
|
473
|
+
Object.keys(manifest).forEach((key) => {
|
|
474
|
+
if (!attrs.includes(key) && !key.startsWith('ext_')) {
|
|
475
|
+
this.warn(`${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
const pathAttrs = ['boards', 'images', 'sounds'];
|
|
479
|
+
Object.keys(manifest.paths || {}).forEach((key) => {
|
|
480
|
+
if (!pathAttrs.includes(key) && !key.startsWith('ext_')) {
|
|
481
|
+
this.warn(`paths.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
// Validate boards referenced in manifest
|
|
486
|
+
if (manifest.paths && manifest.paths.boards) {
|
|
487
|
+
for (const [id, boardPath] of Object.entries(manifest.paths.boards)) {
|
|
488
|
+
await this.add_check(`manifest_boards[${id}]`, `manifest.json path.boards.${id}`, async () => {
|
|
489
|
+
const bFile = zip.file(boardPath);
|
|
490
|
+
if (!bFile) {
|
|
491
|
+
this.err(`board path (${boardPath}) not found in the zip package`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const boardStr = await bFile.async('string');
|
|
496
|
+
const boardJson = JSON.parse(boardStr);
|
|
497
|
+
if (!boardJson || boardJson.id !== id) {
|
|
498
|
+
const boardId = (boardJson && boardJson.id) || 'null';
|
|
499
|
+
this.err(`board at path (${boardPath}) defined in manifest with id "${id}" but actually has id "${boardId}"`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
this.err(`could not parse board at path (${boardPath})`);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Validate images referenced in manifest
|
|
509
|
+
if (manifest.paths && manifest.paths.images) {
|
|
510
|
+
for (const [id, imgPath] of Object.entries(manifest.paths.images)) {
|
|
511
|
+
await this.add_check(`manifest_images[${id}]`, `manifest.json path.images.${id}`, async () => {
|
|
512
|
+
if (!zip.file(imgPath)) {
|
|
513
|
+
this.err(`image path (${imgPath}) not found in the zip package`);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Validate sounds referenced in manifest
|
|
519
|
+
if (manifest.paths && manifest.paths.sounds) {
|
|
520
|
+
for (const [id, soundPath] of Object.entries(manifest.paths.sounds)) {
|
|
521
|
+
await this.add_check(`manifest_sounds[${id}]`, `manifest.json path.sounds.${id}`, async () => {
|
|
522
|
+
if (!zip.file(soundPath)) {
|
|
523
|
+
this.err(`sound path (${soundPath}) not found in the zip package`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
exports.ObfValidator = ObfValidator;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BaseValidator } from './baseValidator';
|
|
2
|
+
import { ValidationResult } from './validationTypes';
|
|
3
|
+
/**
|
|
4
|
+
* Validator for Snap files (.spb, .sps)
|
|
5
|
+
* Snap files are zipped packages containing XML configuration
|
|
6
|
+
*/
|
|
7
|
+
export declare class SnapValidator extends BaseValidator {
|
|
8
|
+
constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Validate a Snap file from disk
|
|
11
|
+
*/
|
|
12
|
+
static validateFile(filePath: string): Promise<ValidationResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Check if content is Snap format
|
|
15
|
+
*/
|
|
16
|
+
static identifyFormat(content: any, filename: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Main validation method
|
|
19
|
+
*/
|
|
20
|
+
validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Validate Snap package structure
|
|
23
|
+
*/
|
|
24
|
+
private validateSnapStructure;
|
|
25
|
+
/**
|
|
26
|
+
* Validate the main settings file
|
|
27
|
+
*/
|
|
28
|
+
private validateSettingsFile;
|
|
29
|
+
/**
|
|
30
|
+
* Validate a page file
|
|
31
|
+
*/
|
|
32
|
+
private validatePageFile;
|
|
33
|
+
}
|