'use strict'; const readline = require('readline'); const fs = require("fs"); const crypto = require('crypto'); class PresetsFile { constructor(fullPath, settings, errors) { this.fullPath = fullPath; this.hash = ""; this._presetsFileMetadata = settings.presetsFileMetadata; this._errors = errors; this._settings = settings; this.options = []; this.optionGroups = []; this._currentOption = undefined; this._currentOptionGroup = undefined; const binaryFileContent = fs.readFileSync(this.fullPath); let sum = crypto.createHash('sha256'); sum.update(binaryFileContent); this.hash = sum.digest('hex'); this._processLines(binaryFileContent, settings.presetsFileEncoding); this._checkProperties(); if (undefined === this.priority) { this.priority = this._settings.PresetCategoriesPriorities[this.category]; } this._clearProperties(); } _clearProperties() { delete this._presetsFileMetadata; delete this._errors; delete this._settings; delete this._currentOption; delete this._currentOptionGroup; delete this.options; delete this.optionGroups; delete this.description; delete this.include; delete this.discussion; delete this.warning; delete this.disclaimer; delete this.include_warning; delete this.include_disclaimer; delete this.parser; } _checkProperties() { for (const [property, value] of Object.entries(this._presetsFileMetadata)) { if (!value.optional) { if (this._isEmptyProperty(this[property])) { this._addError(`missing or empty property '${property}'`); } } } if (undefined !== this._currentOption) { this._addError(`Missing ${this._settings.OptionsDirectives.END_OPTION_DIRECTIVE} for ${this._currentOption.name}`); } if (undefined !== this._currentOptionGroup) { this._addError(`Missing ${this._settings.OptionsDirectives.END_OPTION_GROUP_DIRECTIVE} for ${this._currentOptionGroup.name}`); } } _isEmptyProperty(property) { return (property === undefined || property.length === 0); } _processLines(binaryFileContent, presetsFileEncoding) { const fileContent = binaryFileContent.toString(presetsFileEncoding); const lines = fileContent.split('\n'); this._currentLine = 1; for (let line of lines) { line = line.trim(); if (line.startsWith(this._settings.MetapropertyDirective)) { this._processMetapropertyLine(line); } this._currentLine++; } delete this._currentLine; } _processMetapropertyLine(line) { line = line.slice(this._settings.MetapropertyDirective.length).trim(); // (#$ Title: foo) -> (Title: foo) const lowCaseLine = line.toLowerCase(); let isProperty = false; let isPropertyMissingSemicolon = false; let isOptionDirective = false; for (const [property, value] of Object.entries(this._presetsFileMetadata)) { const lineBeginning = `${property.toLowerCase()}:`; // "TITLE:" const wrongLineBeginning = `${property.toLowerCase()}`; // "TITLE" if (lowCaseLine.startsWith(lineBeginning)) { line = line.slice(lineBeginning.length).trim(); // (Title: foo) -> (foo) this._processProperty(property, line); isProperty = true; } else if (lowCaseLine.startsWith(wrongLineBeginning)) { isPropertyMissingSemicolon = true; } } if (!isProperty && lowCaseLine.startsWith(this._settings.OptionsDirectives.OPTION_DIRECTIVE)) { this._processOptionDirective(line); isOptionDirective = true; } if (!isProperty && !isOptionDirective) { if (isPropertyMissingSemicolon) { this._addError(`line ${this._currentLine}, property missing ":"`); } else { this._addError(`line ${this._currentLine}, unknown preset directive: '${line}'`); } } } _processOptionDirective(line) { const lowCaseLine = line.toLowerCase(); if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE)) { this._processOptionBeginDirective(line, lowCaseLine); } else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_DIRECTIVE)) { this._processOptionEndDirective(line, lowCaseLine); } else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE)) { this._processOptionGroupBeginDirective(line, lowCaseLine); } else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_GROUP_DIRECTIVE)) { this._processOptionGroupEndDirective(line, lowCaseLine); } } _processOptionGroupBeginDirective(line, lowCaseLine) { const optionGroup = this._getOptionGroup(line); if ("" === optionGroup.name) { this._addError(`line ${this._currentLine}, empty optionGroup name`); } else if (undefined !== this._currentOptionGroup) { this._addError(`line ${this._currentLine}, nested #$ option groups are not allowed`); } else { this._currentOptionGroup = optionGroup; } } _processOptionGroupEndDirective(line, lowCaseLine) { if (undefined === this._currentOptionGroup) { this._addError(`line ${this._currentLine}, end Option Group directive found but no Option Group to close`); } else { const lowCaseOptionGroupName = this._currentOptionGroup.name.toLowerCase(); const indexOfOption = this.optionGroups.findIndex(item => lowCaseOptionGroupName === item.name.toLowerCase()); if (-1 === indexOfOption) { this.optionGroups.push(this._currentOptionGroup); } this._currentOptionGroup = undefined; } } _processOptionBeginDirective(line, lowCaseLine) { const Option = this._getOption(line); const lowCaseOptionName = Option.name.toLowerCase(); if ("" === Option.name) { this._addError(`line ${this._currentLine}, empty Option name`); } else if (undefined !== this._currentOption) { this._addError(`line ${this._currentLine}, nested #$ options are not allowed`); } else { this._currentOption = Option; } } _processOptionEndDirective(line, lowCaseLine) { if (undefined === this._currentOption) { this._addError(`line ${this._currentLine}, end Option directive found but no Option to close`); } else { const lowCaseOptionName = this._currentOption.name.toLowerCase(); const indexOfOption = this.options.findIndex(item => lowCaseOptionName === item.name.toLowerCase()); if (-1 === indexOfOption) { this.options.push(this._currentOption); } this._currentOption = undefined; } } _escapeRegex(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } _getOptionGroup(line) { const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE.length).trim(); const isExclusiveGroup = this._isExclusiveGroup(directiveRemoved.toLowerCase()); const exclusiveOptionGroupRegex = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.EXCLUSIVE_OPTION_GROUP), 'gi'); const optionGroupName = directiveRemoved.replace(exclusiveOptionGroupRegex, ""); if (0 == optionGroupName.length || optionGroupName[0] != ":") { this._addError(`line ${this._currentLine}, OPTION_GROUP BEGIN directive should be followed by ":". Example: #$ OPTION_GROUP BEGIN: My Group Name or if its exclusive: #$ OPTION_GROUP BEGIN: (EXCLUSIVE) My Exclusive Group`); } let optionGroup = { name: optionGroupName.slice(1).trim(), exclusive: isExclusiveGroup, } return optionGroup; } _isExclusiveGroup(lowercaseLine) { return lowercaseLine.includes(this._settings.OptionsDirectives.EXCLUSIVE_OPTION_GROUP) } _getOption(line) { const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim(); const directiveRemovedLowCase = directiveRemoved.toLowerCase(); const optionChecked = this._isOptionChecked(directiveRemovedLowCase); const regExpRemoveChecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_CHECKED), 'gi'); const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_UNCHECKED), 'gi'); let optionName = directiveRemoved.replace(regExpRemoveChecked, ""); optionName = optionName.replace(regExpRemoveUnchecked, "").trim(); if (0 == optionName.length || optionName[0] != ":") { this._addError(`line ${this._currentLine}, OPTION BEGIN directive should be followed by ":". Example: #$ OPTION BEGIN (UNCHECKED): My Option Name`); } let option = { name: optionName.slice(1).trim(), checked: optionChecked } return option; } _isOptionChecked(lowCaseLine) { let OptionChecked = false; let OptionUnchecked = false; if (lowCaseLine.includes(this._settings.OptionsDirectives.OPTION_CHECKED)) { OptionChecked = true; } if (lowCaseLine.includes(this._settings.OptionsDirectives.OPTION_UNCHECKED)) { OptionUnchecked = true; } if (OptionChecked && OptionUnchecked) { this._addError(`line ${this._currentLine}, Option can't be checked and unchecked at the same time`); } else if (!OptionChecked && !OptionUnchecked) { this._addError(`line ${this._currentLine}, Every option must specify whether it is ${this._settings.OptionsDirectives.OPTION_CHECKED.toUpperCase()} or ${this._settings.OptionsDirectives.OPTION_UNCHECKED.toUpperCase()}`); } else { OptionChecked = OptionChecked; } return OptionChecked; } _processProperty(property, line) { switch(this._presetsFileMetadata[property].type) { case this._settings.MetadataTypes.STRING_ARRAY: this._processArrayProperty(property, line); break; case this._settings.MetadataTypes.STRING: this._processStringProperty(property, line); break; case this._settings.MetadataTypes.PRESET_CATEGORY: this._processPresetCategoryProperty(property, line); break; case this._settings.MetadataTypes.FILE_PATH: this._processFilePathProperty(property, line); break; case this._settings.MetadataTypes.FILE_PATH_ARRAY: this._processFilePathArrayProperty(property, line); break; case this._settings.MetadataTypes.BOOLEAN: this._processBooleanProperty(property, line); break; case this._settings.MetadataTypes.WORDS_ARRAY: this._processWordsArrayProperty(property, line); break; case this._settings.MetadataTypes.PRESET_STATUS: this._processPresetStatusProperty(property, line); break; case this._settings.MetadataTypes.PRIORITY: this._processPriorityProperty(property, line); break; case this._settings.MetadataTypes.PARSER: this._processParserProperty(property, line); break; default: this._addError(`line ${this._currentLine}, unknown property type '${this._presetsFileMetadata[property].type}' for the property '${property}'`); } } _processPresetStatusProperty(property, line) { this._checkPropertyDublicated(property); if (this._settings.PresetStatusEnum.includes(line)) { this[property] = line; } else { this._addError(`line ${this._currentLine}, unknown ${property} value: '${line}'; available values: ${this._settings.PresetStatusEnum}`); } } _processParserProperty(property, line) { this._checkPropertyDublicated(property); if (this._settings.ParserEnum.includes(line)) { this[property] = line; } else { this._addError(`line ${this._currentLine}, unknown ${property} value: '${line}'; available values: ${this._settings.ParserEnum}`); } } _processWordsArrayProperty(property, line) { this._checkPropertyDublicated(property); let words = line.split(","); words = words.map(word => word.trim()); words = words.filter(word => word); this[property] = words; } _processBooleanProperty(property, line) { this._checkPropertyDublicated(property); const trueValues = ["true", "yes"]; const falseValues = ["false", "no"]; const lineLowCase = line.toLowerCase(); let result = false; if (trueValues.includes(lineLowCase)) { result = true; } else if (falseValues.includes(lineLowCase)) { result = false; } else { this._addError(`line ${this._currentLine}, boolean property '${property}'' has a wrong value: '${line}'`); } this[property] = result; } _checkPropertyDublicated(property) { if (undefined !== this[property]) { this._addError(`line ${this._currentLine}, duplicated property '${property}'`); } } _processFilePathProperty(property, line) { this._checkPropertyDublicated(property); const stat = fs.statSync(line); if (!stat || stat.isDirectory()) { this._addError(`line ${this._currentLine}, can't find file '${line}'`); } else { this[property] = line; } } _processFilePathArrayProperty(property, line) { if (!this[property]) { this[property] = []; } if (fs.existsSync(line)) { // still could be a folder, so have to filter out folders const stat = fs.statSync(line); if (!stat || stat.isDirectory()) { this._addError(`line ${this._currentLine}, a folder is specified instead of a file: '${line}'`); } else { this[property].push(line); } } else { this._addError(`line ${this._currentLine}, can't find file '${line}'`); } } _processPresetCategoryProperty(property, line) { this._checkPropertyDublicated(property); line = line.toLowerCase(); let presetTypeValid = false; for (const [key, value] of Object.entries(this._settings.PresetCategories)) { if (key.toLowerCase() === line) { presetTypeValid = true; this[property] = key; } } if (!presetTypeValid) { this._addError(`line ${this._currentLine}, unknown preset category: '${line}'`); } } _processArrayProperty(property, line) { if (!this[property]) { this[property] = []; } this[property].push(line); } _processStringProperty(property, line) { this._checkPropertyDublicated(property); this[property] = line; } _processPriorityProperty(property, line) { this._checkPropertyDublicated(property); const value = parseInt(line); const value2 = Number(line); if (NaN === value || value !== value2) { this._addError(`line ${this._currentLine}, PRIORITY value must be integer. Instead it is: '${line}'`); } this[property] = value; } _addError(error) { const fullError = `${this.fullPath}: ${error}`; this._errors.push(fullError); console.error(fullError); } } module.exports = PresetsFile;