@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
390 lines • 14.1 kB
JavaScript
/**
* Five File Format Manager
*
* Centralized management for Five file formats (.five, .bin, .v)
* This is the SINGLE SOURCE OF TRUTH for all Five file operations.
*
* Design Principles:
* - Single responsibility for file format detection and loading
* - Consistent error handling across all file operations
* - Extensible for future file formats
* - Rich metadata preservation and validation
* - Performance optimized with caching where appropriate
*/
import { readFile, writeFile, stat } from 'fs/promises';
import { extname } from 'path';
import { Buffer } from 'buffer';
/**
* Centralized Five File Manager
* All Five file operations MUST go through this class
*/
export class FiveFileManager {
static instance;
fileCache = new Map();
supportedExtensions = ['.five', '.bin', '.v'];
/**
* Singleton pattern to ensure consistent behavior across the CLI
*/
static getInstance() {
if (!FiveFileManager.instance) {
FiveFileManager.instance = new FiveFileManager();
}
return FiveFileManager.instance;
}
/**
* Universal file loader - handles ALL Five file formats
* This is the PRIMARY method that all commands should use
*/
async loadFile(filePath, options = {}) {
// Input validation
await this.validateFilePath(filePath);
const ext = extname(filePath).toLowerCase();
const cacheKey = `${filePath}:${JSON.stringify(options)}`;
// Check cache if enabled
if (options.cacheable && this.fileCache.has(cacheKey)) {
return this.fileCache.get(cacheKey);
}
let result;
switch (ext) {
case '.five':
result = await this.loadFiveFile(filePath, options);
break;
case '.bin':
result = await this.loadBinFile(filePath, options);
break;
case '.v':
result = await this.loadSourceFile(filePath, options);
break;
default:
throw new FiveFileError(`Unsupported file format: ${ext}. Supported: ${this.supportedExtensions.join(', ')}`, 'UNSUPPORTED_FORMAT', { filePath, extension: ext });
}
// Validate result if requested
if (options.validateFormat) {
const validation = this.validateFileContent(result);
if (!validation.valid) {
throw new FiveFileError(`File validation failed: ${validation.errors.join(', ')}`, 'VALIDATION_FAILED', { filePath, errors: validation.errors });
}
}
// Require ABI if requested
if (options.requireABI && !result.abi) {
throw new FiveFileError(`ABI required but not found in ${ext} file`, 'ABI_REQUIRED', { filePath, format: result.format });
}
// Cache if enabled
if (options.cacheable) {
this.fileCache.set(cacheKey, result);
}
return result;
}
/**
* Save compiled data to .five format
* This ensures consistent .five file structure across the CLI
*/
async saveFiveFile(filePath, bytecode, abi, metadata, disassembly, options = {}) {
const fiveFile = {
bytecode: Buffer.from(bytecode).toString('base64'),
abi: {
functions: abi.functions || {},
fields: abi.fields || [],
version: abi.version || '1.0'
},
version: '1.0',
metadata: {
compilationTime: Date.now(),
compilerVersion: await this.getCompilerVersion(),
...metadata
}
};
// Add disassembly if present
if (disassembly && disassembly.length > 0) {
fiveFile.disassembly = disassembly;
}
// Add debug info if present
if (metadata?.debug) {
fiveFile.debug = metadata.debug;
}
const jsonContent = JSON.stringify(fiveFile, null, options.indent || 2);
try {
await writeFile(filePath, jsonContent, 'utf8');
}
catch (error) {
throw new FiveFileError(`Failed to save .five file: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SAVE_FAILED', { filePath, error });
}
}
/**
* Save raw bytecode to .bin format
*/
async saveBinFile(filePath, bytecode) {
try {
await writeFile(filePath, bytecode);
}
catch (error) {
throw new FiveFileError(`Failed to save .bin file: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SAVE_FAILED', { filePath, error });
}
}
/**
* Detect file format without loading content
*/
detectFormat(filePath) {
const ext = extname(filePath).toLowerCase();
switch (ext) {
case '.five': return 'five';
case '.bin': return 'bin';
case '.v': return 'v';
default: return 'unknown';
}
}
/**
* Validate file format and content
*/
validateFileContent(file) {
const errors = [];
const warnings = [];
// Basic validation
if (!file.bytecode || file.bytecode.length === 0) {
errors.push('Bytecode is empty or missing');
}
// Five-specific validation
if (file.format === 'five') {
if (!file.abi) {
errors.push('ABI is missing in .five file');
}
else {
if (!file.abi.functions) {
warnings.push('No functions defined in ABI');
}
if (!file.abi.version) {
warnings.push('ABI version not specified');
}
}
}
// Bytecode validation
if (file.bytecode.length < 4) {
errors.push('Bytecode too short to be valid');
}
// Check Five VM magic bytes (5IVE)
const expectedMagic = [0x35, 0x49, 0x56, 0x45]; // "5IVE"
const actualMagic = Array.from(file.bytecode.slice(0, 4));
if (!this.arraysEqual(expectedMagic, actualMagic)) {
warnings.push('Bytecode does not start with Five VM magic bytes');
}
return {
valid: errors.length === 0,
errors,
warnings,
format: file.format,
size: file.size
};
}
/**
* Get file information without loading full content
*/
async getFileInfo(filePath) {
try {
const stats = await stat(filePath);
return {
exists: true,
size: stats.size,
format: this.detectFormat(filePath),
lastModified: stats.mtime
};
}
catch (error) {
return {
exists: false,
size: 0,
format: 'unknown',
lastModified: new Date(0)
};
}
}
/**
* Convert between file formats
*/
async convertFormat(inputPath, outputPath, options = {}) {
const inputFile = await this.loadFile(inputPath, { validateFormat: true });
const outputFormat = this.detectFormat(outputPath);
switch (outputFormat) {
case 'five':
await this.saveFiveFile(outputPath, inputFile.bytecode, inputFile.abi || { functions: {}, fields: [], version: '1.0' }, options.preserveMetadata ? inputFile.metadata : undefined);
break;
case 'bin':
await this.saveBinFile(outputPath, inputFile.bytecode);
break;
default:
throw new FiveFileError(`Cannot convert to format: ${outputFormat}`, 'CONVERSION_UNSUPPORTED', { inputPath, outputPath, outputFormat });
}
}
/**
* Clear file cache
*/
clearCache() {
this.fileCache.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
size: this.fileCache.size,
keys: Array.from(this.fileCache.keys())
};
}
// ==================== PRIVATE METHODS ====================
async loadFiveFile(filePath, options) {
try {
const fileContent = await readFile(filePath, 'utf8');
const stats = await stat(filePath);
let fiveFile;
try {
fiveFile = JSON.parse(fileContent);
}
catch (parseError) {
throw new FiveFileError('Invalid JSON in .five file', 'JSON_PARSE_ERROR', { filePath, parseError });
}
// Validate required fields
if (!fiveFile.bytecode) {
throw new FiveFileError('Missing bytecode field in .five file', 'INVALID_FORMAT', { filePath });
}
if (!fiveFile.abi) {
throw new FiveFileError('Missing abi field in .five file', 'INVALID_FORMAT', { filePath });
}
// Decode base64 bytecode
let bytecode;
try {
bytecode = new Uint8Array(Buffer.from(fiveFile.bytecode, 'base64'));
}
catch (decodeError) {
throw new FiveFileError('Invalid base64 bytecode in .five file', 'BYTECODE_DECODE_ERROR', { filePath, decodeError });
}
return {
bytecode,
abi: fiveFile.abi,
metadata: fiveFile.metadata,
debug: fiveFile.debug,
format: 'five',
sourceFile: filePath,
size: stats.size
};
}
catch (error) {
if (error instanceof FiveFileError) {
throw error;
}
throw new FiveFileError(`Failed to load .five file: ${error instanceof Error ? error.message : 'Unknown error'}`, 'LOAD_FAILED', { filePath, error });
}
}
async loadBinFile(filePath, options) {
try {
const bytecode = await readFile(filePath);
const stats = await stat(filePath);
return {
bytecode: new Uint8Array(bytecode),
format: 'bin',
sourceFile: filePath,
size: stats.size
};
}
catch (error) {
throw new FiveFileError(`Failed to load .bin file: ${error instanceof Error ? error.message : 'Unknown error'}`, 'LOAD_FAILED', { filePath, error });
}
}
async loadSourceFile(filePath, options) {
try {
const sourceCode = await readFile(filePath, 'utf8');
const stats = await stat(filePath);
// Note: This returns the source code as "bytecode" for now
// In a real implementation, you might want to compile it first
const sourceBuffer = Buffer.from(sourceCode, 'utf8');
return {
bytecode: new Uint8Array(sourceBuffer),
format: 'v',
sourceFile: filePath,
size: stats.size,
metadata: {
isSourceCode: true,
sourceLength: sourceCode.length
}
};
}
catch (error) {
throw new FiveFileError(`Failed to load .v file: ${error instanceof Error ? error.message : 'Unknown error'}`, 'LOAD_FAILED', { filePath, error });
}
}
async validateFilePath(filePath) {
if (!filePath || typeof filePath !== 'string') {
throw new FiveFileError('File path is required and must be a string', 'INVALID_PATH', { filePath });
}
const info = await this.getFileInfo(filePath);
if (!info.exists) {
throw new FiveFileError(`File does not exist: ${filePath}`, 'FILE_NOT_FOUND', { filePath });
}
const format = this.detectFormat(filePath);
if (format === 'unknown') {
throw new FiveFileError(`Unsupported file extension. Supported: ${this.supportedExtensions.join(', ')}`, 'UNSUPPORTED_EXTENSION', { filePath, supportedExtensions: this.supportedExtensions });
}
}
async getCompilerVersion() {
// In a real implementation, this would return the actual compiler version
return '1.0.0';
}
arraysEqual(a, b) {
return a.length === b.length && a.every((val, i) => val === b[i]);
}
}
/**
* Custom error class for Five file operations
*/
export class FiveFileError extends Error {
code;
details;
constructor(message, code, details) {
super(message);
this.name = 'FiveFileError';
this.code = code;
this.details = details;
}
}
/**
* Convenience functions for common operations
* These provide a simple API while still using the centralized manager
*/
/**
* Quick load function for simple use cases
*/
export async function loadFiveFile(filePath) {
const manager = FiveFileManager.getInstance();
return manager.loadFile(filePath, { validateFormat: true });
}
/**
* Quick save function for .five files
*/
export async function saveFiveFile(filePath, bytecode, abi, metadata, disassembly) {
const manager = FiveFileManager.getInstance();
return manager.saveFiveFile(filePath, bytecode, abi, metadata, disassembly);
}
/**
* Quick bytecode extraction function
*/
export async function extractBytecode(filePath) {
const file = await loadFiveFile(filePath);
return file.bytecode;
}
/**
* Quick ABI extraction function
*/
export async function extractABI(filePath) {
const file = await loadFiveFile(filePath);
if (!file.abi) {
throw new FiveFileError(`No ABI available in ${file.format} file`, 'NO_ABI', { filePath, format: file.format });
}
return file.abi;
}
/**
* Validate any Five file format
*/
export async function validateFiveFile(filePath) {
const manager = FiveFileManager.getInstance();
const file = await manager.loadFile(filePath);
return manager.validateFileContent(file);
}
//# sourceMappingURL=FiveFileManager.js.map