1
0
rotorflight-presets/indexer/PresetsFile.js
Chris Kim fd9f215f9d
Add initial presets (#1)
Signed-off-by: Chris Kim <oats87g@gmail.com>
2025-04-01 12:47:05 +10:00

474 lines
16 KiB
JavaScript

'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;