diff --git a/examples/vitedemo/index.html b/examples/vitedemo/index.html
index 7b2dfd0..b360e10 100644
--- a/examples/vitedemo/index.html
+++ b/examples/vitedemo/index.html
@@ -191,6 +191,48 @@
font-size: 13px;
}
+ .validation-panel {
+ margin-top: 15px;
+ }
+
+ .validation-summary {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 10px 12px;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 10px;
+ }
+
+ .validation-summary.success {
+ border-left: 4px solid #2ecc71;
+ color: #2d7a4f;
+ }
+
+ .validation-summary.error {
+ border-left: 4px solid #e74c3c;
+ color: #b03a2e;
+ }
+
+ .validation-list {
+ max-height: 180px;
+ overflow-y: auto;
+ font-size: 12px;
+ }
+
+ .validation-item {
+ padding: 6px 0;
+ border-bottom: 1px solid #ececec;
+ }
+
+ .validation-item.warn {
+ color: #c18401;
+ }
+
+ .validation-item.error {
+ color: #b03a2e;
+ }
+
.processor-name {
font-weight: 600;
color: #333;
@@ -437,6 +479,7 @@
๐ฏ AAC Processors Browser Demo
+
@@ -467,6 +510,12 @@ ๐ฏ AAC Processors Browser Demo
Test Results
+
+
diff --git a/examples/vitedemo/src/main.ts b/examples/vitedemo/src/main.ts
index 187d616..4a6656a 100644
--- a/examples/vitedemo/src/main.ts
+++ b/examples/vitedemo/src/main.ts
@@ -81,6 +81,7 @@ import {
AACPage,
AACButton
} from 'aac-processors';
+import { validateFileOrBuffer, type ValidationResult } from 'aac-processors/validation';
import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
@@ -92,6 +93,7 @@ configureSqlJs({
const dropArea = document.getElementById('dropArea') as HTMLElement;
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const processBtn = document.getElementById('processBtn') as HTMLButtonElement;
+const validateBtn = document.getElementById('validateBtn') as HTMLButtonElement;
const runTestsBtn = document.getElementById('runTestsBtn') as HTMLButtonElement;
const clearBtn = document.getElementById('clearBtn') as HTMLButtonElement;
const fileInfo = document.getElementById('fileInfo') as HTMLElement;
@@ -102,6 +104,9 @@ const results = document.getElementById('results') as HTMLElement;
const logPanel = document.getElementById('logPanel') as HTMLElement;
const testResults = document.getElementById('testResults') as HTMLElement;
const testList = document.getElementById('testList') as HTMLElement;
+const validationPanel = document.getElementById('validationPanel') as HTMLElement;
+const validationSummary = document.getElementById('validationSummary') as HTMLElement;
+const validationList = document.getElementById('validationList') as HTMLElement;
const tabButtons = document.querySelectorAll('.tab-btn') as NodeListOf;
const inspectTab = document.getElementById('inspectTab') as HTMLElement;
const pagesetTab = document.getElementById('pagesetTab') as HTMLElement;
@@ -218,6 +223,7 @@ function handleFile(file: File) {
fileDetails.textContent = extension;
fileInfo.style.display = 'block';
processBtn.disabled = true;
+ validateBtn.disabled = true;
return;
}
@@ -228,12 +234,14 @@ function handleFile(file: File) {
fileDetails.textContent = `${file.name} โข ${formatFileSize(file.size)}`;
fileInfo.style.display = 'block';
processBtn.disabled = false;
+ validateBtn.disabled = false;
currentSourceLabel = file.name;
log(`Using processor: ${currentProcessor.constructor.name}`, 'success');
} catch (error) {
log(`Error getting processor: ${(error as Error).message}`, 'error');
processBtn.disabled = true;
+ validateBtn.disabled = true;
}
}
@@ -314,6 +322,79 @@ processBtn.addEventListener('click', async () => {
}
});
+function collectValidationMessages(
+ result: ValidationResult,
+ prefix = ''
+): Array<{ type: 'error' | 'warn'; message: string }> {
+ const messages: Array<{ type: 'error' | 'warn'; message: string }> = [];
+ const label = prefix ? `${prefix}: ` : '';
+ result.results.forEach((check) => {
+ if (!check.valid && check.error) {
+ messages.push({ type: 'error', message: `${label}${check.description}: ${check.error}` });
+ }
+ if (check.warnings?.length) {
+ check.warnings.forEach((warning) => {
+ messages.push({ type: 'warn', message: `${label}${check.description}: ${warning}` });
+ });
+ }
+ });
+ result.sub_results?.forEach((sub) => {
+ const nextPrefix = `${label}${sub.filename || sub.format}`;
+ messages.push(...collectValidationMessages(sub, nextPrefix));
+ });
+ return messages;
+}
+
+function renderValidationResult(result: ValidationResult) {
+ validationPanel.style.display = 'block';
+ validationSummary.classList.remove('success', 'error');
+ validationSummary.classList.add(result.valid ? 'success' : 'error');
+ validationSummary.textContent = `${result.valid ? 'โ
Valid' : 'โ Invalid'} โข ${result.format.toUpperCase()} โข ${result.errors} errors, ${result.warnings} warnings`;
+
+ validationList.innerHTML = '';
+ const messages = collectValidationMessages(result).slice(0, 30);
+ if (messages.length === 0) {
+ const empty = document.createElement('div');
+ empty.className = 'validation-item';
+ empty.textContent = 'No issues reported.';
+ validationList.appendChild(empty);
+ return;
+ }
+
+ messages.forEach((entry) => {
+ const item = document.createElement('div');
+ item.className = `validation-item ${entry.type}`;
+ item.textContent = entry.message;
+ validationList.appendChild(item);
+ });
+}
+
+validateBtn.addEventListener('click', async () => {
+ if (!currentFile) return;
+ log('Validating file...', 'info');
+
+ try {
+ validateBtn.disabled = true;
+ const arrayBuffer = await currentFile.arrayBuffer();
+ const result = await validateFileOrBuffer(new Uint8Array(arrayBuffer), currentFile.name);
+ renderValidationResult(result);
+ log(
+ `${result.valid ? 'โ
' : 'โ'} Validation complete: ${result.errors} errors, ${result.warnings} warnings`,
+ result.valid ? 'success' : 'warn'
+ );
+ } catch (error) {
+ const errorMsg = (error as Error).message;
+ validationPanel.style.display = 'block';
+ validationSummary.classList.remove('success');
+ validationSummary.classList.add('error');
+ validationSummary.textContent = `โ Validation failed: ${errorMsg}`;
+ validationList.innerHTML = '';
+ log(`โ Validation failed: ${errorMsg}`, 'error');
+ } finally {
+ validateBtn.disabled = !currentFile;
+ }
+});
+
// Display results
function displayResults(tree: AACTree) {
results.innerHTML = '';
@@ -423,6 +504,9 @@ clearBtn.addEventListener('click', () => {
stats.style.display = 'none';
results.innerHTML = 'Load a file to see its contents here
';
testResults.style.display = 'none';
+ validationPanel.style.display = 'none';
+ validationSummary.textContent = '';
+ validationList.innerHTML = '';
logPanel.innerHTML = 'Cleared. Ready to process files...
';
pagesetOutput.textContent = 'Generate or convert a pageset to preview the output JSON.';
updateConvertButtons();
diff --git a/examples/vitedemo/vite.config.ts b/examples/vitedemo/vite.config.ts
index 8e37b81..32a8eb4 100644
--- a/examples/vitedemo/vite.config.ts
+++ b/examples/vitedemo/vite.config.ts
@@ -3,13 +3,32 @@ import path from 'path';
export default defineConfig({
resolve: {
- alias: {
- 'aac-processors': path.resolve(__dirname, '../../src/index.browser.ts'),
- stream: path.resolve(__dirname, 'node_modules/stream-browserify'),
- events: path.resolve(__dirname, 'node_modules/events'),
- timers: path.resolve(__dirname, 'node_modules/timers-browserify'),
- util: path.resolve(__dirname, 'node_modules/util')
- }
+ alias: [
+ {
+ find: /^aac-processors\/validation$/,
+ replacement: path.resolve(__dirname, '../../src/validation.ts'),
+ },
+ {
+ find: /^aac-processors$/,
+ replacement: path.resolve(__dirname, '../../src/index.browser.ts'),
+ },
+ {
+ find: /^stream$/,
+ replacement: path.resolve(__dirname, 'node_modules/stream-browserify'),
+ },
+ {
+ find: /^events$/,
+ replacement: path.resolve(__dirname, 'node_modules/events'),
+ },
+ {
+ find: /^timers$/,
+ replacement: path.resolve(__dirname, 'node_modules/timers-browserify'),
+ },
+ {
+ find: /^util$/,
+ replacement: path.resolve(__dirname, 'node_modules/util'),
+ },
+ ],
},
optimizeDeps: {
exclude: ['aac-processors'],
diff --git a/src/utils/io.ts b/src/utils/io.ts
index 15b299a..3a65e74 100644
--- a/src/utils/io.ts
+++ b/src/utils/io.ts
@@ -79,8 +79,24 @@ export function isNodeRuntime(): boolean {
}
export function getBasename(filePath: string): string {
- const parts = filePath.split(/[/\\]/);
- return parts[parts.length - 1] || filePath;
+ const trimmed = filePath.replace(/[/\\]+$/, '') || filePath;
+ const parts = trimmed.split(/[/\\]/);
+ return parts[parts.length - 1] || trimmed;
+}
+
+export function toUint8Array(input: Uint8Array | ArrayBuffer | Buffer): Uint8Array {
+ if (input instanceof Uint8Array) {
+ return input;
+ }
+ return new Uint8Array(input);
+}
+
+export function toArrayBuffer(input: Uint8Array | ArrayBuffer | Buffer): ArrayBuffer {
+ if (input instanceof ArrayBuffer) {
+ return input;
+ }
+ const view = input instanceof Uint8Array ? input : new Uint8Array(input);
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
}
export function decodeText(input: Uint8Array): string {
diff --git a/src/validation/applePanelsValidator.ts b/src/validation/applePanelsValidator.ts
index 55aacbe..be8c9d3 100644
--- a/src/validation/applePanelsValidator.ts
+++ b/src/validation/applePanelsValidator.ts
@@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import plist from 'plist';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, getPath, toUint8Array } from '../utils/io';
type PanelsContainer = { panels?: any; Panels?: Record };
@@ -13,8 +12,10 @@ type PanelsContainer = { panels?: any; Panels?: Record };
export class ApplePanelsValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new ApplePanelsValidator();
+ const fs = getFs();
+ const path = getPath();
let content: Buffer;
- const filename = path.basename(filePath);
+ const filename = getBasename(filePath);
let size = 0;
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
@@ -40,7 +41,14 @@ export class ApplePanelsValidator extends BaseValidator {
}
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const parsed = plist.parse(str) as PanelsContainer;
return Boolean(parsed.panels || parsed.Panels);
} catch {
@@ -64,7 +72,7 @@ export class ApplePanelsValidator extends BaseValidator {
let parsed: PanelsContainer | null = null;
await this.add_check('plist_parse', 'valid plist/XML', async () => {
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ const str = decodeText(content);
parsed = plist.parse(str) as PanelsContainer;
} catch (e: any) {
this.err(`Failed to parse plist: ${e.message}`, true);
diff --git a/src/validation/astericsValidator.ts b/src/validation/astericsValidator.ts
index 8c2f8a6..664ec9b 100644
--- a/src/validation/astericsValidator.ts
+++ b/src/validation/astericsValidator.ts
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for Asterics Grid (.grd) JSON files
@@ -13,9 +12,9 @@ export class AstericsGridValidator extends BaseValidator {
*/
static async validateFile(filePath: string): Promise {
const validator = new AstericsGridValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
/**
@@ -28,7 +27,14 @@ export class AstericsGridValidator extends BaseValidator {
}
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const json = JSON.parse(str);
return Array.isArray(json?.grids);
} catch {
@@ -52,7 +58,7 @@ export class AstericsGridValidator extends BaseValidator {
let json: any = null;
await this.add_check('json_parse', 'valid JSON', async () => {
try {
- let str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ let str = decodeText(content);
if (str.charCodeAt(0) === 0xfeff) {
str = str.slice(1);
}
diff --git a/src/validation/dotValidator.ts b/src/validation/dotValidator.ts
index 053f9e6..36389c2 100644
--- a/src/validation/dotValidator.ts
+++ b/src/validation/dotValidator.ts
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for Graphviz DOT files
@@ -10,9 +9,9 @@ import { ValidationResult } from './validationTypes';
export class DotValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new DotValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
static async identifyFormat(content: any, filename: string): Promise {
@@ -20,7 +19,14 @@ export class DotValidator extends BaseValidator {
if (name.endsWith('.dot')) return true;
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
return str.includes('digraph') || str.includes('->');
} catch {
return false;
@@ -42,7 +48,7 @@ export class DotValidator extends BaseValidator {
let text = '';
await this.add_check('text', 'text content', async () => {
- text = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ text = decodeText(content);
if (!text.trim()) {
this.err('DOT file is empty', true);
}
diff --git a/src/validation/excelValidator.ts b/src/validation/excelValidator.ts
index 24dd345..661214d 100644
--- a/src/validation/excelValidator.ts
+++ b/src/validation/excelValidator.ts
@@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import * as ExcelJS from 'exceljs';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { getBasename, getFs, readBinaryFromInput, toArrayBuffer } from '../utils/io';
/**
* Validator for Excel imports (.xlsx/.xls)
@@ -11,9 +10,9 @@ import { ValidationResult } from './validationTypes';
export class ExcelValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new ExcelValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
static async identifyFormat(_content: any, filename: string): Promise {
@@ -44,7 +43,8 @@ export class ExcelValidator extends BaseValidator {
return this.buildResult(filename, filesize, 'excel');
}
- const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
+ const buffer =
+ typeof Buffer !== 'undefined' && Buffer.isBuffer(content) ? content : toArrayBuffer(content);
const workbook = new ExcelJS.Workbook();
await this.add_check('open', 'open workbook', async () => {
diff --git a/src/validation/gridsetValidator.ts b/src/validation/gridsetValidator.ts
index af938fb..2317725 100644
--- a/src/validation/gridsetValidator.ts
+++ b/src/validation/gridsetValidator.ts
@@ -2,26 +2,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import JSZip from 'jszip';
+import * as xml2js from 'xml2js';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
-import { getFs, getNodeRequire, getPath } from '../utils/io';
-
-let cachedXml2js: typeof import('xml2js') | null = null;
-function getXml2js(): typeof import('xml2js') {
- if (cachedXml2js) return cachedXml2js;
- try {
- const nodeRequire = getNodeRequire();
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const module = nodeRequire('xml2js') as typeof import('xml2js') & {
- default?: typeof import('xml2js');
- };
- const resolved = module.default || module;
- cachedXml2js = resolved;
- return resolved;
- } catch {
- throw new Error('Validator requires Xml2js in this environment.');
- }
-}
+import { decodeText, getBasename, getFs, toUint8Array } from '../utils/io';
/**
* Validator for Grid3/Smartbox Gridset files (.gridset, .gridsetx)
@@ -37,10 +21,9 @@ export class GridsetValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new GridsetValidator();
const fs = getFs();
- const path = getPath();
const content = fs.readFileSync(filePath);
const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
/**
@@ -54,10 +37,9 @@ export class GridsetValidator extends BaseValidator {
// Try to parse as XML and check for gridset structure
try {
- const contentStr = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
- const xml2js = getXml2js();
+ const contentStr = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const parser = new xml2js.Parser();
- const result = await parser.parseStringPromise(contentStr as string);
+ const result = await parser.parseStringPromise(contentStr);
return result && (result.gridset || result.Gridset);
} catch {
return false;
@@ -122,9 +104,8 @@ export class GridsetValidator extends BaseValidator {
let xmlObj: any = null;
await this.add_check('xml_parse', 'valid XML', async () => {
try {
- const xml2js = getXml2js();
const parser = new xml2js.Parser();
- const contentStr = content.toString('utf-8');
+ const contentStr = decodeText(content);
xmlObj = await parser.parseStringPromise(contentStr);
} catch (e: any) {
this.err(`Failed to parse XML: ${e.message}`, true);
@@ -155,7 +136,7 @@ export class GridsetValidator extends BaseValidator {
): Promise {
let zip: JSZip;
try {
- zip = await JSZip.loadAsync(Buffer.from(content));
+ zip = await JSZip.loadAsync(toUint8Array(content));
} catch (e: any) {
this.err(`Failed to open ZIP archive: ${e.message}`, true);
return;
@@ -171,14 +152,13 @@ export class GridsetValidator extends BaseValidator {
} else {
try {
const gridsetXml = await gridsetEntry.async('string');
- const xml2js = getXml2js();
const parser = new xml2js.Parser();
const xmlObj = await parser.parseStringPromise(gridsetXml);
const gridset = xmlObj.gridset || xmlObj.Gridset;
if (!gridset) {
this.err('Invalid gridset.xml structure', true);
} else {
- await this.validateGridsetStructure(gridset, filename, Buffer.from(gridsetXml));
+ await this.validateGridsetStructure(gridset, filename, new Uint8Array());
}
} catch (e: any) {
this.err(`Failed to parse gridset.xml: ${e.message}`, true);
@@ -194,7 +174,6 @@ export class GridsetValidator extends BaseValidator {
} else {
try {
const settingsXml = await settingsEntry.async('string');
- const xml2js = getXml2js();
const parser = new xml2js.Parser();
const xmlObj = await parser.parseStringPromise(settingsXml);
const settings =
diff --git a/src/validation/index.ts b/src/validation/index.ts
index a685185..615161f 100644
--- a/src/validation/index.ts
+++ b/src/validation/index.ts
@@ -43,8 +43,14 @@ import { OpmlValidator } from './opmlValidator';
import { DotValidator } from './dotValidator';
import { ApplePanelsValidator } from './applePanelsValidator';
import { ObfsetValidator } from './obfsetValidator';
-import * as fs from 'fs';
-import * as path from 'path';
+import {
+ getBasename,
+ getFs,
+ isNodeRuntime,
+ readBinaryFromInput,
+ toUint8Array,
+ type ProcessorInput,
+} from '../utils/io';
export function getValidatorForFormat(format: string): BaseValidator | null {
switch (format.toLowerCase()) {
@@ -124,11 +130,11 @@ export function getValidatorForFile(filename: string): BaseValidator | null {
* will be used if available to access nested resources.
*/
export async function validateFileOrBuffer(
- filePathOrBuffer: string | Buffer,
+ filePathOrBuffer: ProcessorInput,
filenameHint?: string
): Promise {
const isPath = typeof filePathOrBuffer === 'string';
- const name = filenameHint || (isPath ? path.basename(filePathOrBuffer) : 'upload');
+ const name = filenameHint || (isPath ? getBasename(filePathOrBuffer) : 'upload');
const validator = getValidatorForFile(name) || getValidatorForFormat(name);
if (!validator) {
@@ -136,6 +142,9 @@ export async function validateFileOrBuffer(
}
if (isPath) {
+ if (!isNodeRuntime()) {
+ throw new Error('File path validation is only supported in Node.js environments.');
+ }
const ctor = validator.constructor as typeof BaseValidator & {
validateFile?: (filePath: string) => Promise;
};
@@ -144,13 +153,11 @@ export async function validateFileOrBuffer(
return ctor.validateFile(filePathOrBuffer);
}
- const buf = fs.readFileSync(filePathOrBuffer);
- const stats = fs.statSync(filePathOrBuffer);
- return validator.validate(buf, path.basename(filePathOrBuffer), stats.size);
+ const buf = readBinaryFromInput(filePathOrBuffer);
+ const stats = getFs().statSync(filePathOrBuffer);
+ return validator.validate(buf, getBasename(filePathOrBuffer), stats.size);
}
- const buffer = Buffer.isBuffer(filePathOrBuffer)
- ? filePathOrBuffer
- : Buffer.from(filePathOrBuffer);
+ const buffer = toUint8Array(filePathOrBuffer);
return validator.validate(buffer, name, buffer.byteLength);
}
diff --git a/src/validation/obfValidator.ts b/src/validation/obfValidator.ts
index d28a009..044b892 100644
--- a/src/validation/obfValidator.ts
+++ b/src/validation/obfValidator.ts
@@ -6,7 +6,7 @@
import JSZip from 'jszip';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
-import { getFs, getPath, readBinaryFromInput } from '../utils/io';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
const OBF_FORMAT = 'open-board-0.1';
const OBF_FORMAT_CURRENT_VERSION = 0.1;
@@ -26,7 +26,7 @@ export class ObfValidator extends BaseValidator {
const validator = new ObfValidator();
const content = readBinaryFromInput(filePath);
const stats = getFs().statSync(filePath);
- return validator.validate(content, getPath().basename(filePath), stats.size);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
/**
@@ -40,7 +40,14 @@ export class ObfValidator extends BaseValidator {
// Try to parse as JSON and check format
try {
- const contentStr = Buffer.isBuffer(content) ? content.toString() : content;
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const contentStr = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const json = JSON.parse(contentStr);
return json && json.format && json.format.startsWith('open-board-');
} catch {
@@ -85,7 +92,7 @@ export class ObfValidator extends BaseValidator {
let json: any = null;
await this.add_check('valid_json', 'JSON file', async () => {
try {
- json = JSON.parse(content.toString());
+ json = JSON.parse(decodeText(content));
} catch {
this.err("Couldn't parse as JSON", true);
}
diff --git a/src/validation/obfsetValidator.ts b/src/validation/obfsetValidator.ts
index b4970ff..650a83d 100644
--- a/src/validation/obfsetValidator.ts
+++ b/src/validation/obfsetValidator.ts
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for OBF set bundles (.obfset) - JSON arrays of boards
@@ -10,9 +9,9 @@ import { ValidationResult } from './validationTypes';
export class ObfsetValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new ObfsetValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
static async identifyFormat(content: any, filename: string): Promise {
@@ -20,7 +19,14 @@ export class ObfsetValidator extends BaseValidator {
if (name.endsWith('.obfset')) return true;
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const parsed = JSON.parse(str);
return Array.isArray(parsed);
} catch {
@@ -44,7 +50,7 @@ export class ObfsetValidator extends BaseValidator {
let boards: any[] | null = null;
await this.add_check('json_parse', 'valid JSON array', async () => {
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ const str = decodeText(content);
const parsed = JSON.parse(str);
if (!Array.isArray(parsed)) {
this.err('root must be a JSON array of boards', true);
diff --git a/src/validation/opmlValidator.ts b/src/validation/opmlValidator.ts
index 2b4b41c..3ef78f5 100644
--- a/src/validation/opmlValidator.ts
+++ b/src/validation/opmlValidator.ts
@@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/require-await */
-import * as fs from 'fs';
-import * as path from 'path';
import { XMLParser, XMLValidator } from 'fast-xml-parser';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for OPML files
@@ -11,9 +10,9 @@ import { ValidationResult } from './validationTypes';
export class OpmlValidator extends BaseValidator {
static async validateFile(filePath: string): Promise {
const validator = new OpmlValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
static async identifyFormat(content: any, filename: string): Promise {
@@ -23,7 +22,14 @@ export class OpmlValidator extends BaseValidator {
}
try {
- const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ if (
+ typeof content !== 'string' &&
+ !(content instanceof ArrayBuffer) &&
+ !(content instanceof Uint8Array)
+ ) {
+ return false;
+ }
+ const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const validation = XMLValidator.validate(str);
if (validation !== true) {
return false;
@@ -52,7 +58,7 @@ export class OpmlValidator extends BaseValidator {
let text = '';
await this.add_check('content', 'non-empty content', async () => {
- text = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
+ text = decodeText(content);
if (!text.trim()) {
this.err('OPML file is empty', true);
}
diff --git a/src/validation/snapValidator.ts b/src/validation/snapValidator.ts
index 2af4797..4d424ca 100644
--- a/src/validation/snapValidator.ts
+++ b/src/validation/snapValidator.ts
@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
-import * as fs from 'fs';
-import * as path from 'path';
import * as xml2js from 'xml2js';
import JSZip from 'jszip';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for Snap files (.spb, .sps)
@@ -21,9 +20,9 @@ export class SnapValidator extends BaseValidator {
*/
static async validateFile(filePath: string): Promise {
const validator = new SnapValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
/**
@@ -38,8 +37,7 @@ export class SnapValidator extends BaseValidator {
// Try to parse as ZIP and check for Snap structure
try {
- const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
- const zip = await JSZip.loadAsync(buffer);
+ const zip = await JSZip.loadAsync(toUint8Array(content));
const entries = Object.values(zip.files).filter((entry) => !entry.dir);
return entries.some(
(entry) => entry.name.includes('settings') || entry.name.includes('.xml')
@@ -70,8 +68,7 @@ export class SnapValidator extends BaseValidator {
await this.add_check('zip', 'valid zip package', async () => {
try {
- const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
- zip = await JSZip.loadAsync(buffer);
+ zip = await JSZip.loadAsync(toUint8Array(content));
const entries = Object.values(zip.files);
validZip = entries.length > 0;
} catch (e: any) {
diff --git a/src/validation/touchChatValidator.ts b/src/validation/touchChatValidator.ts
index fddb9c1..0e50b83 100644
--- a/src/validation/touchChatValidator.ts
+++ b/src/validation/touchChatValidator.ts
@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
-import * as fs from 'fs';
-import * as path from 'path';
import * as xml2js from 'xml2js';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
+import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
/**
* Validator for TouchChat files (.ce)
@@ -21,9 +20,9 @@ export class TouchChatValidator extends BaseValidator {
*/
static async validateFile(filePath: string): Promise {
const validator = new TouchChatValidator();
- const content = fs.readFileSync(filePath);
- const stats = fs.statSync(filePath);
- return validator.validate(content, path.basename(filePath), stats.size);
+ const content = readBinaryFromInput(filePath);
+ const stats = getFs().statSync(filePath);
+ return validator.validate(content, getBasename(filePath), stats.size);
}
/**
@@ -37,7 +36,7 @@ export class TouchChatValidator extends BaseValidator {
// Try to parse as XML and check for TouchChat structure
try {
- const contentStr = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
+ const contentStr = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(contentStr);
// TouchChat files typically have specific structure
@@ -67,7 +66,7 @@ export class TouchChatValidator extends BaseValidator {
await this.add_check('xml_parse', 'valid XML', async () => {
try {
const parser = new xml2js.Parser();
- const contentStr = content.toString('utf-8');
+ const contentStr = decodeText(content);
xmlObj = await parser.parseStringPromise(contentStr);
} catch (e: any) {
this.err(`Failed to parse XML: ${e.message}`, true);