diff options
Diffstat (limited to 'js/baba-yaga/src/core/config.js')
-rw-r--r-- | js/baba-yaga/src/core/config.js | 444 |
1 files changed, 444 insertions, 0 deletions
diff --git a/js/baba-yaga/src/core/config.js b/js/baba-yaga/src/core/config.js new file mode 100644 index 0000000..51d9c3f --- /dev/null +++ b/js/baba-yaga/src/core/config.js @@ -0,0 +1,444 @@ +// config.js - Configuration system for Baba Yaga engine + +import fs from 'fs'; +import path from 'path'; +import { ValidationError } from './error.js'; + +/** + * Configuration class for Baba Yaga engine with validation and defaults + */ +export class BabaYagaConfig { + constructor(options = {}) { + // Performance settings + this.maxRecursionDepth = this.validateNumber(options.maxRecursionDepth, 1000, 1, 100000, 'maxRecursionDepth'); + this.maxExecutionTime = this.validateNumber(options.maxExecutionTime, 30000, 100, 300000, 'maxExecutionTime'); + this.maxMemoryUsage = this.validateNumber(options.maxMemoryUsage, 100_000_000, 1000000, 1_000_000_000, 'maxMemoryUsage'); + + // Input validation limits + this.maxSourceLength = this.validateNumber(options.maxSourceLength, 10_000_000, 1000, 100_000_000, 'maxSourceLength'); + this.maxASTDepth = this.validateNumber(options.maxASTDepth, 1000, 10, 10000, 'maxASTDepth'); + this.maxIdentifierLength = this.validateNumber(options.maxIdentifierLength, 255, 1, 1000, 'maxIdentifierLength'); + this.maxStringLength = this.validateNumber(options.maxStringLength, 1_000_000, 1000, 10_000_000, 'maxStringLength'); + this.maxListLength = this.validateNumber(options.maxListLength, 100_000, 100, 10_000_000, 'maxListLength'); + this.maxTableSize = this.validateNumber(options.maxTableSize, 10_000, 10, 1_000_000, 'maxTableSize'); + + // Feature flags + this.enableOptimizations = this.validateBoolean(options.enableOptimizations, false, 'enableOptimizations'); // Disabled by default due to lexer bug + this.enableTypeChecking = this.validateBoolean(options.enableTypeChecking, true, 'enableTypeChecking'); + this.enableDebugMode = this.validateBoolean(options.enableDebugMode, false, 'enableDebugMode'); + this.enableProfiling = this.validateBoolean(options.enableProfiling, false, 'enableProfiling'); + this.strictMode = this.validateBoolean(options.strictMode, false, 'strictMode'); + + // Security settings + this.allowUnsafeOperations = this.validateBoolean(options.allowUnsafeOperations, true, 'allowUnsafeOperations'); + this.sandboxMode = this.validateBoolean(options.sandboxMode, false, 'sandboxMode'); + this.allowedBuiltins = this.validateArray(options.allowedBuiltins, this.getDefaultBuiltins(), 'allowedBuiltins'); + this.allowedCharacters = options.allowedCharacters instanceof RegExp + ? options.allowedCharacters + : /^[\x20-\x7E\s\n\r\t]*$/; // Printable ASCII + whitespace + + // Error handling + this.verboseErrors = this.validateBoolean(options.verboseErrors, true, 'verboseErrors'); + this.maxErrorSuggestions = this.validateNumber(options.maxErrorSuggestions, 3, 0, 10, 'maxErrorSuggestions'); + this.includeStackTrace = this.validateBoolean(options.includeStackTrace, true, 'includeStackTrace'); + + // Output settings + this.outputFormat = this.validateString(options.outputFormat, 'pretty', ['pretty', 'json', 'minimal'], 'outputFormat'); + this.colorOutput = this.validateBoolean(options.colorOutput, true, 'colorOutput'); + this.showTimings = this.validateBoolean(options.showTimings, false, 'showTimings'); + + // Custom extensions + this.customBuiltins = this.validateMap(options.customBuiltins, new Map(), 'customBuiltins'); + this.plugins = this.validateArray(options.plugins, [], 'plugins'); + + // Host integration + this.hostInterface = options.hostInterface || null; + this.ioHandlers = this.validateObject(options.ioHandlers, {}, 'ioHandlers'); + + // Cache settings + this.enableCaching = this.validateBoolean(options.enableCaching, false, 'enableCaching'); + this.cacheDirectory = this.validateString(options.cacheDirectory, '.baba-cache', null, 'cacheDirectory'); + + // Development settings + this.repl = this.validateObject(options.repl, { + historySize: 1000, + multilineMode: true, + autoComplete: true, + showTypes: false + }, 'repl'); + + // Freeze configuration to prevent accidental modification + if (options.freeze !== false) { + Object.freeze(this); + } + } + + /** + * Validate and normalize a number option + */ + validateNumber(value, defaultValue, min, max, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (typeof value !== 'number' || isNaN(value)) { + throw new ValidationError(`Configuration option '${name}' must be a number, got: ${typeof value}`); + } + + if (value < min || value > max) { + throw new ValidationError(`Configuration option '${name}' must be between ${min} and ${max}, got: ${value}`); + } + + return value; + } + + /** + * Validate and normalize a boolean option + */ + validateBoolean(value, defaultValue, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (typeof value !== 'boolean') { + throw new ValidationError(`Configuration option '${name}' must be a boolean, got: ${typeof value}`); + } + + return value; + } + + /** + * Validate and normalize a string option + */ + validateString(value, defaultValue, allowedValues, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (typeof value !== 'string') { + throw new ValidationError(`Configuration option '${name}' must be a string, got: ${typeof value}`); + } + + if (allowedValues && !allowedValues.includes(value)) { + throw new ValidationError(`Configuration option '${name}' must be one of: ${allowedValues.join(', ')}, got: ${value}`); + } + + return value; + } + + /** + * Validate and normalize an array option + */ + validateArray(value, defaultValue, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (!Array.isArray(value)) { + throw new ValidationError(`Configuration option '${name}' must be an array, got: ${typeof value}`); + } + + return [...value]; // Create a copy + } + + /** + * Validate and normalize an object option + */ + validateObject(value, defaultValue, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (typeof value !== 'object' || Array.isArray(value)) { + throw new ValidationError(`Configuration option '${name}' must be an object, got: ${typeof value}`); + } + + return { ...defaultValue, ...value }; // Merge with defaults + } + + /** + * Validate and normalize a Map option + */ + validateMap(value, defaultValue, name) { + if (value === undefined || value === null) { + return defaultValue; + } + + if (!(value instanceof Map)) { + if (typeof value === 'object' && !Array.isArray(value)) { + // Convert object to Map + return new Map([...defaultValue.entries(), ...Object.entries(value)]); + } else { + throw new ValidationError(`Configuration option '${name}' must be a Map or object, got: ${typeof value}`); + } + } + + return new Map([...defaultValue.entries(), ...value.entries()]); + } + + /** + * Get default built-in functions + */ + getDefaultBuiltins() { + return [ + // Higher-order functions + 'map', 'filter', 'reduce', 'length', + + // List operations + 'append', 'prepend', 'concat', 'update', 'removeAt', 'slice', + + // Table operations + 'set', 'remove', 'merge', 'keys', 'values', + + // String operations + 'str.concat', 'str.split', 'str.join', 'str.length', 'str.substring', + 'str.replace', 'str.trim', 'str.upper', 'str.lower', + + // Math operations + 'math.abs', 'math.sign', 'math.floor', 'math.ceil', 'math.round', 'math.trunc', + 'math.min', 'math.max', 'math.clamp', 'math.pow', 'math.sqrt', 'math.exp', 'math.log', + 'math.sin', 'math.cos', 'math.tan', 'math.asin', 'math.acos', 'math.atan', 'math.atan2', + 'math.deg', 'math.rad', 'math.random', 'math.randomInt', + + // IO operations + 'io.out', 'io.in', 'io.emit', 'io.listen', + + // Introspection + 'shape' + ]; + } + + /** + * Create configuration from JSON file + */ + static fromFile(configPath) { + try { + const fullPath = path.resolve(configPath); + const configData = fs.readFileSync(fullPath, 'utf8'); + const parsed = JSON.parse(configData); + + return new BabaYagaConfig(parsed); + } catch (error) { + if (error.code === 'ENOENT') { + throw new ValidationError(`Configuration file not found: ${configPath}`); + } else if (error instanceof SyntaxError) { + throw new ValidationError(`Invalid JSON in configuration file: ${error.message}`); + } else { + throw new ValidationError(`Failed to load configuration: ${error.message}`); + } + } + } + + /** + * Create configuration from environment variables + */ + static fromEnvironment(prefix = 'BABA_') { + const options = {}; + + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith(prefix)) { + const configKey = key.slice(prefix.length).toLowerCase() + .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); + + // Try to parse as JSON, fall back to string + try { + options[configKey] = JSON.parse(value); + } catch { + options[configKey] = value; + } + } + } + + return new BabaYagaConfig(options); + } + + /** + * Create development configuration + */ + static development() { + return new BabaYagaConfig({ + enableDebugMode: true, + enableProfiling: true, + verboseErrors: true, + showTimings: true, + strictMode: false, + maxRecursionDepth: 500, // Lower for development + maxExecutionTime: 10000, // 10 seconds + repl: { + historySize: 10000, + multilineMode: true, + autoComplete: true, + showTypes: true + } + }); + } + + /** + * Create production configuration + */ + static production() { + return new BabaYagaConfig({ + enableDebugMode: false, + enableProfiling: false, + verboseErrors: false, + showTimings: false, + strictMode: true, + enableOptimizations: true, + sandboxMode: true, + allowUnsafeOperations: false, + maxRecursionDepth: 2000, + maxExecutionTime: 5000, // 5 seconds + enableCaching: true + }); + } + + /** + * Create testing configuration + */ + static testing() { + return new BabaYagaConfig({ + enableDebugMode: true, + verboseErrors: true, + strictMode: true, + maxRecursionDepth: 100, // Low for testing + maxExecutionTime: 1000, // 1 second + maxSourceLength: 100000, // Smaller for tests + includeStackTrace: true + }); + } + + /** + * Create sandbox configuration for untrusted code + */ + static sandbox() { + return new BabaYagaConfig({ + sandboxMode: true, + allowUnsafeOperations: false, + strictMode: true, + maxRecursionDepth: 100, + maxExecutionTime: 1000, + maxSourceLength: 10000, + maxASTDepth: 50, + maxListLength: 1000, + maxTableSize: 100, + allowedBuiltins: [ + 'map', 'filter', 'reduce', + 'str.length', 'str.substring', + 'math.abs', 'math.min', 'math.max', 'math.floor', 'math.ceil' + ] + }); + } + + /** + * Merge with another configuration + */ + merge(otherConfig) { + if (!(otherConfig instanceof BabaYagaConfig)) { + otherConfig = new BabaYagaConfig(otherConfig); + } + + const merged = {}; + + // Copy all properties from both configs + for (const key of Object.keys(this)) { + merged[key] = otherConfig.hasOwnProperty(key) ? otherConfig[key] : this[key]; + } + + return new BabaYagaConfig({ ...merged, freeze: false }); + } + + /** + * Export configuration to JSON + */ + toJSON() { + const config = {}; + + for (const [key, value] of Object.entries(this)) { + if (value instanceof Map) { + config[key] = Object.fromEntries(value.entries()); + } else if (value instanceof RegExp) { + config[key] = value.source; + } else { + config[key] = value; + } + } + + return config; + } + + /** + * Save configuration to file + */ + saveToFile(filePath) { + const config = this.toJSON(); + const json = JSON.stringify(config, null, 2); + + try { + fs.writeFileSync(filePath, json, 'utf8'); + } catch (error) { + throw new ValidationError(`Failed to save configuration: ${error.message}`); + } + } + + /** + * Validate configuration consistency + */ + validate() { + const errors = []; + + // Check for logical inconsistencies + if (this.maxASTDepth > this.maxRecursionDepth * 2) { + errors.push('maxASTDepth should not be more than twice maxRecursionDepth'); + } + + if (this.maxExecutionTime < 100) { + errors.push('maxExecutionTime should be at least 100ms'); + } + + if (this.sandboxMode && this.allowUnsafeOperations) { + errors.push('sandboxMode and allowUnsafeOperations cannot both be true'); + } + + if (this.enableCaching && !this.cacheDirectory) { + errors.push('cacheDirectory must be specified when enableCaching is true'); + } + + if (errors.length > 0) { + throw new ValidationError(`Configuration validation failed:\n - ${errors.join('\n - ')}`); + } + + return true; + } + + /** + * Get a summary of the current configuration + */ + summary() { + return { + performance: { + maxRecursionDepth: this.maxRecursionDepth, + maxExecutionTime: this.maxExecutionTime, + maxMemoryUsage: this.maxMemoryUsage, + enableOptimizations: this.enableOptimizations + }, + security: { + sandboxMode: this.sandboxMode, + allowUnsafeOperations: this.allowUnsafeOperations, + strictMode: this.strictMode, + allowedBuiltinsCount: this.allowedBuiltins.length + }, + limits: { + maxSourceLength: this.maxSourceLength, + maxASTDepth: this.maxASTDepth, + maxListLength: this.maxListLength, + maxTableSize: this.maxTableSize + }, + features: { + enableDebugMode: this.enableDebugMode, + enableProfiling: this.enableProfiling, + enableTypeChecking: this.enableTypeChecking, + enableCaching: this.enableCaching + } + }; + } +} |