snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
211 lines • 10.8 kB
JavaScript
;
/**
* Input Validation Module
*
* Provides comprehensive validation for all user inputs
* to prevent runtime errors and improve user experience.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputValidator = void 0;
const result_types_1 = require("../types/result-types");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class InputValidator {
/**
* Validate ROM file path and accessibility
*/
static async validateROMFile(romPath) {
if (!romPath || typeof romPath !== 'string') {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.ROM_NOT_FOUND, 'ROM file path is required'));
}
try {
await fs.promises.access(romPath, fs.constants.R_OK);
const stats = await fs.promises.stat(romPath);
if (!stats.isFile()) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ROM_FORMAT, 'ROM path must point to a file, not a directory'));
}
const ext = path.extname(romPath).toLowerCase();
const validExtensions = ['.smc', '.sfc', '.fig', '.bin'];
if (!validExtensions.includes(ext)) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ROM_FORMAT, `Invalid ROM file extension. Expected: ${validExtensions.join(', ')}, got: ${ext}`));
}
// Check file size (SNES ROMs are typically 512KB to 8MB)
const maxSize = 8 * 1024 * 1024; // 8MB
const minSize = 32 * 1024; // 32KB
if (stats.size > maxSize) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ROM_FORMAT, `ROM file too large: ${stats.size} bytes (max: ${maxSize})`));
}
if (stats.size < minSize) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ROM_FORMAT, `ROM file too small: ${stats.size} bytes (min: ${minSize})`));
}
return (0, result_types_1.Ok)(romPath);
}
catch (error) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.ROM_NOT_FOUND, `Cannot access ROM file: ${error instanceof Error ? error.message : String(error)}`));
}
}
/**
* Validate address range
*/
static validateAddressRange(start, end) {
const result = {};
if (start) {
const startAddr = this.parseHexAddress(start);
if (startAddr === null) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ADDRESS_RANGE, `Invalid start address format: ${start}. Expected hex format (e.g., '8000', '0x8000', '$8000')`));
}
result.start = startAddr;
}
if (end) {
const endAddr = this.parseHexAddress(end);
if (endAddr === null) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ADDRESS_RANGE, `Invalid end address format: ${end}. Expected hex format (e.g., 'FFFF', '0xFFFF', '$FFFF')`));
}
result.end = endAddr;
}
// Validate range logic
if (result.start && result.end && result.start >= result.end) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ADDRESS_RANGE, `Start address (${result.start.toString(16)}) must be less than end address (${result.end.toString(16)})`));
}
// Validate SNES address ranges
if (result.start && (result.start < 0 || result.start > 0xFFFFFF)) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ADDRESS_RANGE, `Start address out of range: $${result.start.toString(16)} (valid range: $000000-$FFFFFF)`));
}
if (result.end && (result.end < 0 || result.end > 0xFFFFFF)) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.INVALID_ADDRESS_RANGE, `End address out of range: $${result.end.toString(16)} (valid range: $000000-$FFFFFF)`));
}
return (0, result_types_1.Ok)(result);
}
/**
* Validate output directory
*/
static async validateOutputDirectory(outputDir) {
try {
const resolvedPath = path.resolve(outputDir);
// Check if directory exists
try {
const stats = await fs.promises.stat(resolvedPath);
if (!stats.isDirectory()) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `Output path exists but is not a directory: ${resolvedPath}`));
}
}
catch (error) {
// Directory doesn't exist, try to create it
try {
await fs.promises.mkdir(resolvedPath, { recursive: true });
}
catch (mkdirError) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `Cannot create output directory: ${mkdirError instanceof Error ? mkdirError.message : String(mkdirError)}`));
}
}
// Test write permissions
try {
await fs.promises.access(resolvedPath, fs.constants.W_OK);
}
catch (error) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `No write permission for output directory: ${resolvedPath}`));
}
return (0, result_types_1.Ok)(resolvedPath);
}
catch (error) {
return (0, result_types_1.Err)(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `Invalid output directory: ${error instanceof Error ? error.message : String(error)}`));
}
}
/**
* Validate CLI options
*/
static validateCLIOptions(options) {
const errors = [];
// Validate output format
if (options.format) {
const validFormats = ['ca65', 'wla-dx', 'bass', 'html', 'json', 'xml', 'csv', 'markdown'];
if (!validFormats.includes(options.format)) {
errors.push(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `Invalid output format: ${options.format}. Valid formats: ${validFormats.join(', ')}`));
}
}
// Validate asset types
if (options.assetTypes) {
const validAssetTypes = ['graphics', 'audio', 'text'];
const requestedTypes = options.assetTypes.split(',').map(t => t.trim());
const invalidTypes = requestedTypes.filter(t => !validAssetTypes.includes(t));
if (invalidTypes.length > 0) {
errors.push(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.OUTPUT_ERROR, `Invalid asset types: ${invalidTypes.join(', ')}. Valid types: ${validAssetTypes.join(', ')}`));
}
}
// Validate BRR sample rate
if (options.brrSampleRate) {
const sampleRate = parseInt(options.brrSampleRate);
if (isNaN(sampleRate) || sampleRate < 1000 || sampleRate > 96000) {
errors.push(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.BRR_DECODE_ERROR, `Invalid BRR sample rate: ${options.brrSampleRate}. Valid range: 1000-96000 Hz`));
}
}
// Validate BRR max samples
if (options.brrMaxSamples) {
const maxSamples = parseInt(options.brrMaxSamples);
if (isNaN(maxSamples) || maxSamples < 1 || maxSamples > 10000000) {
errors.push(new result_types_1.DisassemblerError(result_types_1.DisassemblerErrorType.BRR_DECODE_ERROR, `Invalid BRR max samples: ${options.brrMaxSamples}. Valid range: 1-10000000`));
}
}
if (errors.length > 0) {
return (0, result_types_1.Err)(errors);
}
return (0, result_types_1.Ok)(options);
}
/**
* Parse hex address from various formats
*/
static parseHexAddress(address) {
if (!address || typeof address !== 'string') {
return null;
}
// Remove common prefixes
let cleanAddress = address.trim().toLowerCase();
if (cleanAddress.startsWith('0x')) {
cleanAddress = cleanAddress.substring(2);
}
else if (cleanAddress.startsWith('$')) {
cleanAddress = cleanAddress.substring(1);
}
// Validate hex characters
if (!/^[0-9a-f]+$/.test(cleanAddress)) {
return null;
}
const parsed = parseInt(cleanAddress, 16);
return isNaN(parsed) ? null : parsed;
}
}
exports.InputValidator = InputValidator;
//# sourceMappingURL=input-validator.js.map