From ac1fde606352eb581e02b90c5573ea90652ccbe0 Mon Sep 17 00:00:00 2001 From: will wade Date: Mon, 19 Jan 2026 10:14:47 +0000 Subject: [PATCH 1/2] refactor validator fs access --- src/validation/applePanelsValidator.ts | 7 ++++--- src/validation/astericsValidator.ts | 9 ++++----- src/validation/dotValidator.ts | 9 ++++----- src/validation/excelValidator.ts | 9 ++++----- src/validation/index.ts | 11 +++++------ src/validation/obfsetValidator.ts | 9 ++++----- src/validation/opmlValidator.ts | 9 ++++----- src/validation/snapValidator.ts | 9 ++++----- src/validation/touchChatValidator.ts | 9 ++++----- 9 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/validation/applePanelsValidator.ts b/src/validation/applePanelsValidator.ts index 55aacbe..6425c56 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 { getBasename, getFs, getPath } 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; diff --git a/src/validation/astericsValidator.ts b/src/validation/astericsValidator.ts index 8c2f8a6..4562b62 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 { getBasename, getFs, readBinaryFromInput } 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); } /** diff --git a/src/validation/dotValidator.ts b/src/validation/dotValidator.ts index 053f9e6..015fac2 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 { getBasename, getFs, readBinaryFromInput } 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 { diff --git a/src/validation/excelValidator.ts b/src/validation/excelValidator.ts index 24dd345..92de660 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 } 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 { diff --git a/src/validation/index.ts b/src/validation/index.ts index a685185..a05e130 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -43,8 +43,7 @@ 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, readBinaryFromInput } from '../utils/io'; export function getValidatorForFormat(format: string): BaseValidator | null { switch (format.toLowerCase()) { @@ -128,7 +127,7 @@ export async function validateFileOrBuffer( 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) { @@ -144,9 +143,9 @@ 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) diff --git a/src/validation/obfsetValidator.ts b/src/validation/obfsetValidator.ts index b4970ff..ac6a133 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 { getBasename, getFs, readBinaryFromInput } 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 { diff --git a/src/validation/opmlValidator.ts b/src/validation/opmlValidator.ts index 2b4b41c..9a6db9f 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 { getBasename, getFs, readBinaryFromInput } 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 { diff --git a/src/validation/snapValidator.ts b/src/validation/snapValidator.ts index 2af4797..6eb5943 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 } 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); } /** diff --git a/src/validation/touchChatValidator.ts b/src/validation/touchChatValidator.ts index fddb9c1..ac426d1 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 { getBasename, getFs, readBinaryFromInput } 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); } /** From 4ab93f35b3fe4c00fb67812110ed7f67488e038a Mon Sep 17 00:00:00 2001 From: Will Wade Date: Mon, 19 Jan 2026 11:46:09 +0000 Subject: [PATCH 2/2] fix for browser validation - and demo demoing this --- examples/vitedemo/index.html | 49 +++++++++++++++ examples/vitedemo/src/main.ts | 84 ++++++++++++++++++++++++++ examples/vitedemo/vite.config.ts | 33 +++++++--- src/utils/io.ts | 20 +++++- src/validation/applePanelsValidator.ts | 13 +++- src/validation/astericsValidator.ts | 13 +++- src/validation/dotValidator.ts | 13 +++- src/validation/excelValidator.ts | 5 +- src/validation/gridsetValidator.ts | 37 +++--------- src/validation/index.ts | 18 ++++-- src/validation/obfValidator.ts | 15 +++-- src/validation/obfsetValidator.ts | 13 +++- src/validation/opmlValidator.ts | 13 +++- src/validation/snapValidator.ts | 8 +-- src/validation/touchChatValidator.ts | 6 +- 15 files changed, 268 insertions(+), 72 deletions(-) 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 6425c56..be8c9d3 100644 --- a/src/validation/applePanelsValidator.ts +++ b/src/validation/applePanelsValidator.ts @@ -2,7 +2,7 @@ import plist from 'plist'; import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, getPath } from '../utils/io'; +import { decodeText, getBasename, getFs, getPath, toUint8Array } from '../utils/io'; type PanelsContainer = { panels?: any; Panels?: Record }; @@ -41,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 { @@ -65,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 4562b62..664ec9b 100644 --- a/src/validation/astericsValidator.ts +++ b/src/validation/astericsValidator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/require-await */ import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for Asterics Grid (.grd) JSON files @@ -27,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 { @@ -51,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 015fac2..36389c2 100644 --- a/src/validation/dotValidator.ts +++ b/src/validation/dotValidator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/require-await */ import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for Graphviz DOT files @@ -19,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; @@ -41,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 92de660..661214d 100644 --- a/src/validation/excelValidator.ts +++ b/src/validation/excelValidator.ts @@ -2,7 +2,7 @@ import * as ExcelJS from 'exceljs'; import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { getBasename, getFs, readBinaryFromInput, toArrayBuffer } from '../utils/io'; /** * Validator for Excel imports (.xlsx/.xls) @@ -43,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 a05e130..615161f 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -43,7 +43,14 @@ import { OpmlValidator } from './opmlValidator'; import { DotValidator } from './dotValidator'; import { ApplePanelsValidator } from './applePanelsValidator'; import { ObfsetValidator } from './obfsetValidator'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { + getBasename, + getFs, + isNodeRuntime, + readBinaryFromInput, + toUint8Array, + type ProcessorInput, +} from '../utils/io'; export function getValidatorForFormat(format: string): BaseValidator | null { switch (format.toLowerCase()) { @@ -123,7 +130,7 @@ 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'; @@ -135,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; }; @@ -148,8 +158,6 @@ export async function validateFileOrBuffer( 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 ac6a133..650a83d 100644 --- a/src/validation/obfsetValidator.ts +++ b/src/validation/obfsetValidator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/require-await */ import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for OBF set bundles (.obfset) - JSON arrays of boards @@ -19,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 { @@ -43,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 9a6db9f..3ef78f5 100644 --- a/src/validation/opmlValidator.ts +++ b/src/validation/opmlValidator.ts @@ -2,7 +2,7 @@ import { XMLParser, XMLValidator } from 'fast-xml-parser'; import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for OPML files @@ -22,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; @@ -51,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 6eb5943..4d424ca 100644 --- a/src/validation/snapValidator.ts +++ b/src/validation/snapValidator.ts @@ -4,7 +4,7 @@ import * as xml2js from 'xml2js'; import JSZip from 'jszip'; import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for Snap files (.spb, .sps) @@ -37,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') @@ -69,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 ac426d1..0e50b83 100644 --- a/src/validation/touchChatValidator.ts +++ b/src/validation/touchChatValidator.ts @@ -4,7 +4,7 @@ import * as xml2js from 'xml2js'; import { BaseValidator } from './baseValidator'; import { ValidationResult } from './validationTypes'; -import { getBasename, getFs, readBinaryFromInput } from '../utils/io'; +import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io'; /** * Validator for TouchChat files (.ce) @@ -36,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 @@ -66,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);