From 82dddc75cf6522a5f9d5393de584864f046fef8c Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Wed, 21 Sep 2022 14:15:14 -0400 Subject: [PATCH] *Initial port to typescript --- LICENSE | 2 +- lib/bcfg.js | 9 - lib/config.js | 1204 --------------------------------------------- lib/fs-browser.js | 3 - lib/fs.js | 3 - package.json | 49 +- src/index.ts | 1022 ++++++++++++++++++++++++++++++++++++++ tsconfig.json | 12 + 8 files changed, 1053 insertions(+), 1251 deletions(-) delete mode 100644 lib/bcfg.js delete mode 100644 lib/config.js delete mode 100644 lib/fs-browser.js delete mode 100644 lib/fs.js create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/LICENSE b/LICENSE index 156ded8..a2ecf73 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ This software is licensed under the MIT License. -Copyright (c) 2017, Christopher Jeffrey (https://github.com/chjj) +Copyright (c) 2017, Christopher Jeffrey (https://github.com/chjj), 2022 Hammer Technologies LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/bcfg.js b/lib/bcfg.js deleted file mode 100644 index 58f9900..0000000 --- a/lib/bcfg.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * bcfg.js - configuration parsing for bcoin - * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -module.exports = require('./config'); diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index b9e15bc..0000000 --- a/lib/config.js +++ /dev/null @@ -1,1204 +0,0 @@ -/*! - * config.js - configuration parsing for bcoin - * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -const assert = require('bsert'); -const Path = require('path'); -const os = require('os'); -const fs = require('./fs'); -const HOME = os.homedir ? os.homedir() : '/'; - -/** - * Config Parser - */ - -class Config { - /** - * Create a config. - * @constructor - * @param {String} module - Module name (e.g. `bcoin`). - * @param {Object?} options - */ - - constructor(module, options) { - assert(typeof module === 'string'); - assert(module.length > 0); - - this.module = module; - this.prefix = Path.join(HOME, `.${module}`); - this.suffix = null; - this.fallback = null; - this.alias = Object.create(null); - - this.options = Object.create(null); - this.data = Object.create(null); - this.env = Object.create(null); - this.args = Object.create(null); - this.argv = []; - this.pass = []; - this.query = Object.create(null); - this.hash = Object.create(null); - - if (options) - this.init(options); - } - - /** - * Initialize options. - * @private - * @param {Object} options - */ - - init(options) { - assert(options && typeof options === 'object'); - - if (options.suffix != null) { - assert(typeof options.suffix === 'string'); - this.suffix = options.suffix; - } - - if (options.fallback != null) { - assert(typeof options.fallback === 'string'); - this.fallback = options.fallback; - } - - if (options.alias) { - assert(typeof options.alias === 'object'); - for (const key of Object.keys(options.alias)) { - const value = options.alias[key]; - assert(typeof value === 'string'); - this.alias[key] = value; - } - } - } - - /** - * Inject options. - * @param {Object} options - */ - - inject(options) { - for (const key of Object.keys(options)) { - const value = options[key]; - - switch (key) { - case 'hash': - case 'query': - case 'env': - case 'argv': - case 'config': - continue; - } - - this.set(key, value); - } - } - - /** - * Load options from hash, query, env, or args. - * @param {Object} options - */ - - load(options) { - if (options.hash) - this.parseHash(options.hash); - - if (options.query) - this.parseQuery(options.query); - - if (options.env) - this.parseEnv(options.env); - - if (options.argv) - this.parseArg(options.argv); - - this.prefix = this.getPrefix(); - } - - /** - * Open a config file. - * @param {String} file - e.g. `bcoin.conf`. - * @throws on IO error - */ - - open(file) { - if (fs.unsupported) - return; - - const path = this.getFile(file); - - let text; - try { - text = fs.readFileSync(path, 'utf8'); - } catch (e) { - if (e.code === 'ENOENT') - return; - throw e; - } - - this.parseConfig(text); - - this.prefix = this.getPrefix(); - } - - /** - * Create a child config. Filter by plugin name. - * @param {String} name - * @returns {Config} - */ - - filter(name) { - assert(typeof name === 'string'); - - const child = new Config(this.module); - - child.prefix = this.prefix; - child.suffix = this.suffix; - child.fallback = this.fallback; - child.argv = this.argv; - child.pass = this.pass; - - _filter(name, this.env, child.env); - _filter(name, this.args, child.args); - _filter(name, this.query, child.query); - _filter(name, this.hash, child.hash); - _filter(name, this.options, child.options); - - return child; - } - - /** - * Set default option. - * @param {String} key - * @param {Object} value - */ - - set(key, value) { - assert(typeof key === 'string', 'Key must be a string.'); - - if (value == null) - return; - - key = key.replace(/-/g, ''); - key = key.toLowerCase(); - - this.options[key] = value; - } - - /** - * Test whether a config option is present. - * @param {String} key - * @returns {Boolean} - */ - - has(key) { - if (typeof key === 'number') { - assert(key >= 0, 'Index must be positive.'); - if (key >= this.argv.length) - return false; - return true; - } - - assert(typeof key === 'string', 'Key must be a string.'); - - key = key.replace(/-/g, ''); - key = key.toLowerCase(); - - if (this.hash[key] != null) - return true; - - if (this.query[key] != null) - return true; - - if (this.args[key] != null) - return true; - - if (this.env[key] != null) - return true; - - if (this.data[key] != null) - return true; - - if (this.options[key] != null) - return true; - - return false; - } - - /** - * Get a config option. - * @param {String} key - * @param {Object?} fallback - * @returns {Object|null} - */ - - get(key, fallback) { - if (fallback === undefined) - fallback = null; - - if (Array.isArray(key)) { - const keys = key; - for (const key of keys) { - const value = this.get(key); - if (value !== null) - return value; - } - return fallback; - } - - if (typeof key === 'number') { - assert(key >= 0, 'Index must be positive.'); - - if (key >= this.argv.length) - return fallback; - - if (this.argv[key] != null) - return this.argv[key]; - - return fallback; - } - - assert(typeof key === 'string', 'Key must be a string.'); - - key = key.replace(/-/g, ''); - key = key.toLowerCase(); - - if (this.hash[key] != null) - return this.hash[key]; - - if (this.query[key] != null) - return this.query[key]; - - if (this.args[key] != null) - return this.args[key]; - - if (this.env[key] != null) - return this.env[key]; - - if (this.data[key] != null) - return this.data[key]; - - if (this.options[key] != null) - return this.options[key]; - - return fallback; - } - - /** - * Get a value's type. - * @param {String} key - * @returns {String} - */ - - typeOf(key) { - const value = this.get(key); - - if (value === null) - return 'null'; - - return typeof value; - } - - /** - * Get a config option (as a string). - * @param {String} key - * @param {Object?} fallback - * @returns {String|null} - */ - - str(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'string') - throw new Error(`${fmt(key)} must be a string.`); - - return value; - } - - /** - * Get a config option (as an integer). - * @param {String} key - * @param {Object?} fallback - * @returns {Number|null} - */ - - int(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'string') { - if (typeof value !== 'number') - throw new Error(`${fmt(key)} must be an int.`); - - if (!Number.isSafeInteger(value)) - throw new Error(`${fmt(key)} must be an int.`); - - return value; - } - - if (!/^\-?\d+$/.test(value)) - throw new Error(`${fmt(key)} must be an int.`); - - const num = parseInt(value, 10); - - if (!Number.isSafeInteger(num)) - throw new Error(`${fmt(key)} must be an int.`); - - return num; - } - - /** - * Get a config option (as a unsigned integer). - * @param {String} key - * @param {Object?} fallback - * @returns {Number|null} - */ - - uint(key, fallback) { - const value = this.int(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (value < 0) - throw new Error(`${fmt(key)} must be a uint.`); - - return value; - } - - /** - * Get a config option (as a float). - * @param {String} key - * @param {Object?} fallback - * @returns {Number|null} - */ - - float(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'string') { - if (typeof value !== 'number') - throw new Error(`${fmt(key)} must be a float.`); - - if (!isFinite(value)) - throw new Error(`${fmt(key)} must be a float.`); - - return value; - } - - if (!/^\-?\d*(?:\.\d*)?$/.test(value)) - throw new Error(`${fmt(key)} must be a float.`); - - if (!/\d/.test(value)) - throw new Error(`${fmt(key)} must be a float.`); - - const num = parseFloat(value); - - if (!isFinite(num)) - throw new Error(`${fmt(key)} must be a float.`); - - return num; - } - - /** - * Get a config option (as a positive float). - * @param {String} key - * @param {Object?} fallback - * @returns {Number|null} - */ - - ufloat(key, fallback) { - const value = this.float(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (value < 0) - throw new Error(`${fmt(key)} must be a positive float.`); - - return value; - } - - /** - * Get a value (as a fixed number). - * @param {String} key - * @param {Number?} exp - * @param {Object?} fallback - * @returns {Number|null} - */ - - fixed(key, exp, fallback) { - const value = this.float(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - try { - return fromFloat(value, exp || 0); - } catch (e) { - throw new Error(`${fmt(key)} must be a fixed number.`); - } - } - - /** - * Get a value (as a positive fixed number). - * @param {String} key - * @param {Number?} exp - * @param {Object?} fallback - * @returns {Number|null} - */ - - ufixed(key, exp, fallback) { - const value = this.fixed(key, exp); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (value < 0) - throw new Error(`${fmt(key)} must be a positive fixed number.`); - - return value; - } - - /** - * Get a config option (as a boolean). - * @param {String} key - * @param {Object?} fallback - * @returns {Boolean|null} - */ - - bool(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - // Bitcoin Core compat. - if (typeof value === 'number') { - if (value === 1) - return true; - - if (value === 0) - return false; - } - - if (typeof value !== 'string') { - if (typeof value !== 'boolean') - throw new Error(`${fmt(key)} must be a boolean.`); - return value; - } - - if (value === 'true' || value === '1') - return true; - - if (value === 'false' || value === '0') - return false; - - throw new Error(`${fmt(key)} must be a boolean.`); - } - - /** - * Get a config option (as a buffer). - * @param {String} key - * @param {Object?} fallback - * @returns {Buffer|null} - */ - - buf(key, fallback, enc) { - const value = this.get(key); - - if (!enc) - enc = 'hex'; - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'string') { - if (!Buffer.isBuffer(value)) - throw new Error(`${fmt(key)} must be a buffer.`); - return value; - } - - const data = Buffer.from(value, enc); - - if (data.length !== Buffer.byteLength(value, enc)) - throw new Error(`${fmt(key)} must be a ${enc} string.`); - - return data; - } - - /** - * Get a config option (as an array of strings). - * @param {String} key - * @param {Object?} fallback - * @returns {String[]|null} - */ - - array(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'string') { - if (!Array.isArray(value)) - throw new Error(`${fmt(key)} must be an array.`); - return value; - } - - const parts = value.trim().split(/\s*,\s*/); - const result = []; - - for (const part of parts) { - if (part.length === 0) - continue; - - result.push(part); - } - - return result; - } - - /** - * Get a config option (as an object). - * @param {String} key - * @param {Object?} fallback - * @returns {Object|null} - */ - - obj(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'object' || Array.isArray(value)) - throw new Error(`${fmt(key)} must be an object.`); - - return value; - } - - /** - * Get a config option (as a function). - * @param {String} key - * @param {Object?} fallback - * @returns {Function|null} - */ - - func(key, fallback) { - const value = this.get(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (typeof value !== 'function') - throw new Error(`${fmt(key)} must be a function.`); - - return value; - } - - /** - * Get a config option (as a string). - * @param {String} key - * @param {Object?} fallback - * @returns {String|null} - */ - - path(key, fallback) { - let value = this.str(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - if (value.length === 0) - return fallback; - - switch (value[0]) { - case '~': // home dir - value = Path.join(HOME, value.substring(1)); - break; - case '@': // prefix - value = Path.join(this.prefix, value.substring(1)); - break; - default: // cwd - break; - } - - return Path.normalize(value); - } - - /** - * Get a config option (in MB). - * @param {String} key - * @param {Object?} fallback - * @returns {Number|null} - */ - - mb(key, fallback) { - const value = this.uint(key); - - if (fallback === undefined) - fallback = null; - - if (value === null) - return fallback; - - return value * 1024 * 1024; - } - - /** - * Grab suffix from config data. - * @returns {String} - */ - - getSuffix() { - if (!this.suffix) - throw new Error('No suffix presented.'); - - const suffix = this.str(this.suffix, this.fallback); - - assert(isAlpha(suffix), 'Bad suffix.'); - - return suffix; - } - - /** - * Grab prefix from config data. - * @private - * @returns {String} - */ - - getPrefix() { - let prefix = this.str('prefix'); - - if (prefix) { - if (prefix[0] === '~') - prefix = Path.join(HOME, prefix.substring(1)); - } else { - prefix = Path.join(HOME, `.${this.module}`); - } - - if (this.suffix) { - const suffix = this.str(this.suffix); - - if (suffix) { - assert(isAlpha(suffix), 'Bad suffix.'); - if (this.fallback && suffix !== this.fallback) - prefix = Path.join(prefix, suffix); - } - } - - return Path.normalize(prefix); - } - - /** - * Grab config filename from config data. - * @private - * @param {String} file - * @returns {String} - */ - - getFile(file) { - const name = this.str('config'); - - if (name) - return name; - - return Path.join(this.prefix, file); - } - - /** - * Create a file path using `prefix`. - * @param {String} file - * @returns {String} - */ - - location(file) { - return Path.join(this.prefix, file); - } - - /** - * Parse config text. - * @private - * @param {String} text - */ - - parseConfig(text) { - assert(typeof text === 'string', 'Config must be text.'); - - if (text.charCodeAt(0) === 0xfeff) - text = text.substring(1); - - text = text.replace(/\r\n/g, '\n'); - text = text.replace(/\r/g, '\n'); - text = text.replace(/\\\n/g, ''); - - let num = 0; - - for (const chunk of text.split('\n')) { - const line = chunk.trim(); - - num += 1; - - if (line.length === 0) - continue; - - if (line[0] === '#') - continue; - - const index = line.indexOf(':'); - - if (index === -1) - throw new Error(`Expected ':' on line ${num}: "${line}".`); - - let key = line.substring(0, index).trim(); - - key = key.replace(/\-/g, ''); - - if (!isLowerKey(key)) - throw new Error(`Invalid option on line ${num}: ${key}.`); - - const value = line.substring(index + 1).trim(); - - if (value.length === 0) - continue; - - const alias = this.alias[key]; - - if (alias) - key = alias; - - this.data[key] = value; - } - } - - /** - * Parse arguments. - * @private - * @param {Array?} argv - */ - - parseArg(argv) { - if (!argv || typeof argv !== 'object') - argv = process.argv; - - assert(Array.isArray(argv)); - - let last = null; - let pass = false; - - for (let i = 2; i < argv.length; i++) { - const arg = argv[i]; - - assert(typeof arg === 'string'); - - if (arg === '--') { - pass = true; - continue; - } - - if (pass) { - this.pass.push(arg); - continue; - } - - if (arg.length === 0) { - last = null; - continue; - } - - if (arg.indexOf('--') === 0) { - const index = arg.indexOf('='); - - let key = null; - let value = null; - let empty = false; - - if (index !== -1) { - // e.g. --opt=val - key = arg.substring(2, index); - value = arg.substring(index + 1); - last = null; - empty = false; - } else { - // e.g. --opt - key = arg.substring(2); - value = 'true'; - last = null; - empty = true; - } - - key = key.replace(/\-/g, ''); - - if (!isLowerKey(key)) - throw new Error(`Invalid argument: --${key}.`); - - if (value.length === 0) - continue; - - // Do not allow one-letter aliases. - if (key.length > 1) { - const alias = this.alias[key]; - if (alias) - key = alias; - } - - this.args[key] = value; - - if (empty) - last = key; - - continue; - } - - if (arg[0] === '-') { - // e.g. -abc - last = null; - - for (let j = 1; j < arg.length; j++) { - let key = arg[j]; - - if ((key < 'a' || key > 'z') - && (key < 'A' || key > 'Z') - && (key < '0' || key > '9') - && key !== '?') { - throw new Error(`Invalid argument: -${key}.`); - } - - const alias = this.alias[key]; - - if (alias) - key = alias; - - this.args[key] = 'true'; - - last = key; - } - - continue; - } - - // e.g. foo - const value = arg; - - if (value.length === 0) { - last = null; - continue; - } - - if (last) { - this.args[last] = value; - last = null; - } else { - this.argv.push(value); - } - } - } - - /** - * Parse environment variables. - * @private - * @param {Object?} env - * @returns {Object} - */ - - parseEnv(env) { - let prefix = this.module; - - prefix = prefix.toUpperCase(); - prefix = prefix.replace(/-/g, '_'); - prefix += '_'; - - if (!env || typeof env !== 'object') - env = process.env; - - assert(env && typeof env === 'object'); - - for (let key of Object.keys(env)) { - const value = env[key]; - - assert(typeof value === 'string'); - - if (key.indexOf(prefix) !== 0) - continue; - - key = key.substring(prefix.length); - key = key.replace(/_/g, ''); - - if (!isUpperKey(key)) - continue; - - if (value.length === 0) - continue; - - key = key.toLowerCase(); - - // Do not allow one-letter aliases. - if (key.length > 1) { - const alias = this.alias[key]; - if (alias) - key = alias; - } - - this.env[key] = value; - } - } - - /** - * Parse uri querystring variables. - * @private - * @param {String} query - */ - - parseQuery(query) { - if (typeof query !== 'string') { - if (!global.location) - return {}; - - query = global.location.search; - - if (typeof query !== 'string') - return {}; - } - - return this.parseForm(query, '?', this.query); - } - - /** - * Parse uri hash variables. - * @private - * @param {String} hash - */ - - parseHash(hash) { - if (typeof hash !== 'string') { - if (!global.location) - return {}; - - hash = global.location.hash; - - if (typeof hash !== 'string') - return {}; - } - - return this.parseForm(hash, '#', this.hash); - } - - /** - * Parse form-urlencoded variables. - * @private - * @param {String} query - * @param {String} ch - * @param {Object} map - */ - - parseForm(query, ch, map) { - assert(typeof query === 'string'); - - if (query.length === 0) - return; - - if (query[0] === ch) - query = query.substring(1); - - for (const pair of query.split('&')) { - const index = pair.indexOf('='); - - let key, value; - if (index !== -1) { - key = pair.substring(0, index); - value = pair.substring(index + 1); - } else { - key = pair; - value = 'true'; - } - - key = unescape(key); - key = key.replace(/\-/g, ''); - - if (!isLowerKey(key)) - continue; - - value = unescape(value); - - if (value.length === 0) - continue; - - const alias = this.alias[key]; - - if (alias) - key = alias; - - map[key] = value; - } - } -} - -/* - * Helpers - */ - -function fmt(key) { - if (Array.isArray(key)) - key = key[0]; - - if (typeof key === 'number') - return `Argument #${key}`; - - return key; -} - -function unescape(str) { - try { - str = decodeURIComponent(str); - str = str.replace(/\+/g, ' '); - } catch (e) { - ; - } - str = str.replace(/\0/g, ''); - return str; -} - -function isAlpha(str) { - return /^[a-z0-9_\-]+$/i.test(str); -} - -function isKey(key) { - return /^[a-zA-Z0-9]+$/.test(key); -} - -function isLowerKey(key) { - if (!isKey(key)) - return false; - - return !/[A-Z]/.test(key); -} - -function isUpperKey(key) { - if (!isKey(key)) - return false; - - return !/[a-z]/.test(key); -} - -function _filter(name, a, b) { - for (const key of Object.keys(a)) { - if (key.length > name.length && key.indexOf(name) === 0) { - const sub = key.substring(name.length); - b[sub] = a[key]; - } - } -} - -function fromFloat(num, exp) { - assert(typeof num === 'number' && isFinite(num)); - assert(Number.isSafeInteger(exp)); - - let str = num.toFixed(exp); - let sign = 1; - - if (str.length > 0 && str[0] === '-') { - str = str.substring(1); - sign = -1; - } - - let hi = str; - let lo = '0'; - - const index = str.indexOf('.'); - - if (index !== -1) { - hi = str.substring(0, index); - lo = str.substring(index + 1); - } - - hi = hi.replace(/^0+/, ''); - lo = lo.replace(/0+$/, ''); - - assert(hi.length <= 16 - exp, - 'Fixed number string exceeds 2^53-1.'); - - assert(lo.length <= exp, - 'Too many decimal places in fixed number string.'); - - if (hi.length === 0) - hi = '0'; - - while (lo.length < exp) - lo += '0'; - - if (lo.length === 0) - lo = '0'; - - assert(/^\d+$/.test(hi) && /^\d+$/.test(lo), - 'Non-numeric characters in fixed number string.'); - - hi = parseInt(hi, 10); - lo = parseInt(lo, 10); - - const mult = Math.pow(10, exp); - const maxLo = Number.MAX_SAFE_INTEGER % mult; - const maxHi = (Number.MAX_SAFE_INTEGER - maxLo) / mult; - - assert(hi < maxHi || (hi === maxHi && lo <= maxLo), - 'Fixed number string exceeds 2^53-1.'); - - return sign * (hi * mult + lo); -} - -/* - * Expose - */ - -module.exports = Config; diff --git a/lib/fs-browser.js b/lib/fs-browser.js deleted file mode 100644 index 2122727..0000000 --- a/lib/fs-browser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -exports.unsupported = true; diff --git a/lib/fs.js b/lib/fs.js deleted file mode 100644 index 3a7e2d5..0000000 --- a/lib/fs.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('fs'); diff --git a/package.json b/package.json index 1ad5afe..3eca51c 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,20 @@ { - "name": "bcfg", - "version": "0.1.7", - "description": "Config parser for bcoin", - "keywords": [ - "conf", - "config" - ], - "license": "MIT", - "repository": "git://github.com/bcoin-org/bcfg.git", - "homepage": "https://github.com/bcoin-org/bcfg", - "bugs": { - "url": "https://github.com/bcoin-org/bcfg/issues" - }, - "author": "Christopher Jeffrey ", - "main": "./lib/bcfg.js", - "scripts": { - "lint": "eslint lib/ || exit 0", - "test": "bmocha --reporter spec test/*-test.js" - }, - "dependencies": { - "bsert": "~0.0.10" - }, - "devDependencies": { - "bmocha": "^2.1.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "browser": { - "./lib/fs": "./lib/fs-browser.js" - } + "name": "@lumeweb/cfg", + "version": "0.1.7", + "license": "MIT", + "authors": [ + "Christopher Jeffrey ", + "Hammer Technologies LLC " + ], + "main": "dist/index.js", + "scripts": { + "lint": "eslint lib/ || exit 0", + "test": "bmocha --reporter spec test/*-test.js" + }, + "dependencies": { + "bsert": "~0.0.10" + }, + "devDependencies": { + "@types/node": "^18.7.18" + } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1ad9a4f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1022 @@ +/*! + * config.js - configuration parsing for bcoin + * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +"use strict"; + +import assert from "bsert"; +import Path from "path"; +import os from "os"; +import fs from "fs"; + +const HOME = os.homedir ? os.homedir() : "/"; + +export interface Options { + suffix?: string; + fallback?: string; + alias?: object; +} + +export interface LoadOptions { + hash?: string; + query?: string; + env?: object; + argv?: string[]; + config?: string; +} + +/** + * Config Parser + */ + +class Config { + private module: string; + private prefix: string; + private suffix?: string; + private fallback?: string; + private options: Options; + private alias = {}; + private data = {}; + private env = {}; + private args = {}; + private argv: any[] = []; + private pass: any[] = []; + private query = {}; + private hash = {}; + + constructor(module: string, options?: Options) { + assert(typeof module === "string"); + assert(module.length > 0); + + this.module = module; + this.prefix = Path.join(HOME, `.${module}`); + + if (options) { + this.init(options); + } + } + + private init(options: Options) { + assert(options && typeof options === "object"); + + if (options.suffix != null) { + assert(typeof options.suffix === "string"); + this.suffix = options.suffix; + } + + if (options.fallback != null) { + assert(typeof options.fallback === "string"); + this.fallback = options.fallback; + } + + if (options.alias) { + assert(typeof options.alias === "object"); + for (const key of Object.keys(options.alias)) { + const value = options.alias[key]; + assert(typeof value === "string"); + this.alias[key] = value; + } + } + } + public inject(options: object) { + for (const key of Object.keys(options)) { + const value = options[key]; + + switch (key) { + case "hash": + case "query": + case "env": + case "argv": + case "config": + continue; + } + + this.set(key, value); + } + } + + public load(options: LoadOptions) { + if (options.hash) { + this.parseHash(options.hash); + } + + if (options.query) { + this.parseQuery(options.query); + } + + if (options.env) { + this.parseEnv(options.env); + } + + if (options.argv) { + this.parseArg(options.argv); + } + + this.prefix = this.getPrefix(); + } + public open(file: string) { + const path = this.getFile(file); + + let text; + try { + text = fs.readFileSync(path, "utf8"); + } catch (e) { + if (e.code === "ENOENT") return; + throw e; + } + + this.parseConfig(text); + + this.prefix = this.getPrefix(); + } + + public filter(name: string) { + assert(typeof name === "string"); + + const child = new Config(this.module); + + child.prefix = this.prefix; + child.suffix = this.suffix; + child.fallback = this.fallback; + child.argv = this.argv; + child.pass = this.pass; + + _filter(name, this.env, child.env); + _filter(name, this.args, child.args); + _filter(name, this.query, child.query); + _filter(name, this.hash, child.hash); + _filter(name, this.options, child.options); + + return child; + } + + public set(key: string, value: any) { + assert(typeof key === "string", "Key must be a string."); + + if (value == null) { + return; + } + + key = key.replace(/-/g, ""); + key = key.toLowerCase(); + + this.options[key] = value; + } + + public has(key: string) { + if (typeof key === "number") { + assert(key >= 0, "Index must be positive."); + return key < this.argv.length; + } + + assert(typeof key === "string", "Key must be a string."); + + key = key.replace(/-/g, ""); + key = key.toLowerCase(); + + if (this.hash[key] !== null) { + return true; + } + + if (this.query[key] !== null) { + return true; + } + + if (this.args[key] !== null) { + return true; + } + + if (this.env[key] !== null) { + return true; + } + + if (this.data[key] !== null) { + return true; + } + + return this.options[key] !== null; + } + + public get(key: string, fallback = null) { + if (Array.isArray(key)) { + const keys = key; + for (const key of keys) { + const value = this.get(key); + if (value !== null) { + return value; + } + } + return fallback; + } + + if (typeof key === "number") { + assert(key >= 0, "Index must be positive."); + + if (key >= this.argv.length) { + return fallback; + } + + if (this.argv[key] != null) { + return this.argv[key]; + } + + return fallback; + } + + assert(typeof key === "string", "Key must be a string."); + + key = key.replace(/-/g, ""); + key = key.toLowerCase(); + + if (this.hash[key] !== null) { + return this.hash[key]; + } + + if (this.query[key] !== null) { + return this.query[key]; + } + + if (this.args[key] !== null) { + return this.args[key]; + } + + if (this.env[key] !== null) { + return this.env[key]; + } + + if (this.data[key] !== null) { + return this.data[key]; + } + + if (this.options[key] !== null) { + return this.options[key]; + } + + return fallback; + } + + public typeOf(key: string) { + const value = this.get(key); + + if (value === null) { + return "null"; + } + + return typeof value; + } + + public str(key: string, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "string") { + throw new Error(`${fmt(key)} must be a string.`); + } + + return value; + } + int(key, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "string") { + if (typeof value !== "number") { + throw new Error(`${fmt(key)} must be an int.`); + } + + if (!Number.isSafeInteger(value)) { + throw new Error(`${fmt(key)} must be an int.`); + } + + return value; + } + + if (!/^\-?\d+$/.test(value)) { + throw new Error(`${fmt(key)} must be an int.`); + } + + const num = parseInt(value, 10); + + if (!Number.isSafeInteger(num)) { + throw new Error(`${fmt(key)} must be an int.`); + } + + return num; + } + + uint(key, fallback = null) { + const value = this.int(key); + + if (value === null) { + return fallback; + } + + if (value < 0) { + throw new Error(`${fmt(key)} must be a uint.`); + } + + return value; + } + + float(key, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "string") { + if (typeof value !== "number") { + throw new Error(`${fmt(key)} must be a float.`); + } + + if (!isFinite(value)) { + throw new Error(`${fmt(key)} must be a float.`); + } + + return value; + } + + if (!/^\-?\d*(?:\.\d*)?$/.test(value)) { + throw new Error(`${fmt(key)} must be a float.`); + } + + if (!/\d/.test(value)) { + throw new Error(`${fmt(key)} must be a float.`); + } + + const num = parseFloat(value); + + if (!isFinite(num)) { + throw new Error(`${fmt(key)} must be a float.`); + } + + return num; + } + ufloat(key, fallback = null) { + const value = this.float(key); + if (value === null) { + return fallback; + } + + if (value < 0) { + throw new Error(`${fmt(key)} must be a positive float.`); + } + + return value; + } + + fixed(key, exp, fallback = null) { + const value = this.float(key); + + if (value === null) { + return fallback; + } + + try { + return fromFloat(value, exp || 0); + } catch (e) { + throw new Error(`${fmt(key)} must be a fixed number.`); + } + } + ufixed(key, exp, fallback = null) { + const value = this.fixed(key, exp); + + if (value === null) { + return fallback; + } + + if (value < 0) { + throw new Error(`${fmt(key)} must be a positive fixed number.`); + } + + return value; + } + + bool(key, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + // Bitcoin Core compat. + if (typeof value === "number") { + if (value === 1) { + return true; + } + + if (value === 0) { + return false; + } + } + + if (typeof value !== "string") { + if (typeof value !== "boolean") { + throw new Error(`${fmt(key)} must be a boolean.`); + } + return value; + } + + if (value === "true" || value === "1") { + return true; + } + + if (value === "false" || value === "0") { + return false; + } + + throw new Error(`${fmt(key)} must be a boolean.`); + } + buf(key: string, fallback = null, enc: BufferEncoding = "hex") { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "string") { + if (!Buffer.isBuffer(value)) { + throw new Error(`${fmt(key)} must be a buffer.`); + } + return value; + } + + const data = Buffer.from(value, enc); + + if (data.length !== Buffer.byteLength(value, enc)) { + throw new Error(`${fmt(key)} must be a ${enc} string.`); + } + + return data; + } + + public array(key: string, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "string") { + if (!Array.isArray(value)) { + throw new Error(`${fmt(key)} must be an array.`); + } + return value; + } + + const parts = value.trim().split(/\s*,\s*/); + const result = []; + ``; + for (const part of parts) { + if (part.length === 0) { + continue; + } + + result.push(part); + } + + return result; + } + + public obj(key: string, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "object" || Array.isArray(value)) { + throw new Error(`${fmt(key)} must be an object.`); + } + + return value; + } + + public func(key: string, fallback = null) { + const value = this.get(key); + + if (value === null) { + return fallback; + } + + if (typeof value !== "function") { + throw new Error(`${fmt(key)} must be a function.`); + } + + return value; + } + + public path(key: string, fallback = null) { + let value = this.str(key); + if (value === null) { + return fallback; + } + + if (value.length === 0) { + return fallback; + } + + switch (value[0]) { + case "~": // home dir + value = Path.join(HOME, value.substring(1)); + break; + case "@": // prefix + value = Path.join(this.prefix, value.substring(1)); + break; + default: // cwd + break; + } + + return Path.normalize(value); + } + + public mb(key: string, fallback = null) { + const value = this.uint(key); + + if (value === null) { + return fallback; + } + + return value * 1024 * 1024; + } + + public getSuffix() { + if (!this.suffix) { + throw new Error("No suffix presented."); + } + + const suffix = this.str(this.suffix, this.fallback); + + assert(isAlpha(suffix), "Bad suffix."); + + return suffix; + } + + public getPrefix() { + let prefix = this.str("prefix"); + + if (prefix) { + if (prefix[0] === "~") { + prefix = Path.join(HOME, prefix.substring(1)); + } + } else { + prefix = Path.join(HOME, `.${this.module}`); + } + + if (this.suffix) { + const suffix = this.str(this.suffix); + + if (suffix) { + assert(isAlpha(suffix), "Bad suffix."); + if (this.fallback && suffix !== this.fallback) { + prefix = Path.join(prefix, suffix); + } + } + } + + return Path.normalize(prefix); + } + + public getFile(file: string) { + const name = this.str("config"); + + if (name) { + return name; + } + + return Path.join(this.prefix, file); + } + + public location(file: string) { + return Path.join(this.prefix, file); + } + + public parseConfig(text: string) { + assert(typeof text === "string", "Config must be text."); + + if (text.charCodeAt(0) === 0xfeff) { + text = text.substring(1); + } + + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + text = text.replace(/\\\n/g, ""); + + let num = 0; + + for (const chunk of text.split("\n")) { + const line = chunk.trim(); + + num += 1; + + if (line.length === 0) { + continue; + } + + if (line[0] === "#") { + continue; + } + + const index = line.indexOf(":"); + + if (index === -1) { + throw new Error(`Expected ':' on line ${num}: "${line}".`); + } + + let key = line.substring(0, index).trim(); + + key = key.replace(/\-/g, ""); + + if (!isLowerKey(key)) { + throw new Error(`Invalid option on line ${num}: ${key}.`); + } + + const value = line.substring(index + 1).trim(); + + if (value.length === 0) { + continue; + } + + const alias = this.alias[key]; + + if (alias) { + key = alias; + } + + this.data[key] = value; + } + } + + public parseArg(argv?: string[]) { + if (!argv || typeof argv !== "object") argv = process.argv; + + assert(Array.isArray(argv)); + + let last = null; + let pass = false; + + for (let i = 2; i < argv.length; i++) { + const arg = argv[i]; + + assert(typeof arg === "string"); + + if (arg === "--") { + pass = true; + continue; + } + + if (pass) { + this.pass.push(arg); + continue; + } + + if (arg.length === 0) { + last = null; + continue; + } + + if (arg.indexOf("--") === 0) { + const index = arg.indexOf("="); + + let key = null; + let value = null; + let empty = false; + + if (index !== -1) { + // e.g. --opt=val + key = arg.substring(2, index); + value = arg.substring(index + 1); + last = null; + empty = false; + } else { + // e.g. --opt + key = arg.substring(2); + value = "true"; + last = null; + empty = true; + } + + key = key.replace(/\-/g, ""); + + if (!isLowerKey(key)) { + throw new Error(`Invalid argument: --${key}.`); + } + + if (value.length === 0) { + continue; + } + + // Do not allow one-letter aliases. + if (key.length > 1) { + const alias = this.alias[key]; + if (alias) { + key = alias; + } + } + + this.args[key] = value; + + if (empty) { + last = key; + } + + continue; + } + + if (arg[0] === "-") { + // e.g. -abc + last = null; + + for (let j = 1; j < arg.length; j++) { + let key = arg[j]; + + if ( + (key < "a" || key > "z") && + (key < "A" || key > "Z") && + (key < "0" || key > "9") && + key !== "?" + ) { + throw new Error(`Invalid argument: -${key}.`); + } + + const alias = this.alias[key]; + + if (alias) { + key = alias; + } + + this.args[key] = "true"; + + last = key; + } + + continue; + } + + // e.g. foo + const value = arg; + + if (value.length === 0) { + last = null; + continue; + } + + if (last) { + this.args[last] = value; + last = null; + } else { + this.argv.push(value); + } + } + } + + public parseEnv(env?: object) { + let prefix = this.module; + + prefix = prefix.toUpperCase(); + prefix = prefix.replace(/-/g, "_"); + prefix += "_"; + + if (!env || typeof env !== "object") { + env = process.env; + } + + assert(env && typeof env === "object"); + + for (let key of Object.keys(env)) { + const value = env[key]; + + assert(typeof value === "string"); + + if (key.indexOf(prefix) !== 0) continue; + + key = key.substring(prefix.length); + key = key.replace(/_/g, ""); + + if (!isUpperKey(key)) { + continue; + } + + if (value.length === 0) { + continue; + } + + key = key.toLowerCase(); + + // Do not allow one-letter aliases. + if (key.length > 1) { + const alias = this.alias[key]; + if (alias) { + key = alias; + } + } + + this.env[key] = value; + } + } + + public parseQuery(query: string) { + if (typeof query !== "string") { + if (!global.location) { + return {}; + } + + query = global.location.search; + + if (typeof query !== "string") { + return {}; + } + } + + return this.parseForm(query, "?", this.query); + } + public parseHash(hash: string) { + if (typeof hash !== "string") { + if (!global.location) { + return {}; + } + + hash = global.location.hash; + + if (typeof hash !== "string") { + return {}; + } + } + + return this.parseForm(hash, "#", this.hash); + } + + public parseForm(query: string, ch: string, map: object) { + assert(typeof query === "string"); + + if (query.length === 0) { + return; + } + + if (query[0] === ch) { + query = query.substring(1); + } + + for (const pair of query.split("&")) { + const index = pair.indexOf("="); + + let key, value; + if (index !== -1) { + key = pair.substring(0, index); + value = pair.substring(index + 1); + } else { + key = pair; + value = "true"; + } + + key = unescape(key); + key = key.replace(/\-/g, ""); + + if (!isLowerKey(key)) { + continue; + } + + value = unescape(value); + + if (value.length === 0) { + continue; + } + + const alias = this.alias[key]; + + if (alias) { + key = alias; + } + + map[key] = value; + } + } +} + +/* + * Helpers + */ + +function fmt(key: string[] | string | number) { + if (Array.isArray(key)) { + key = key[0]; + } + + if (typeof key === "number") { + return `Argument #${key}`; + } + + return key; +} + +function unescape(str: string) { + try { + str = decodeURIComponent(str); + str = str.replace(/\+/g, " "); + } catch (e) {} + str = str.replace(/\0/g, ""); + return str; +} + +function isAlpha(str: string) { + return /^[a-z0-9_\-]+$/i.test(str); +} + +function isKey(key: string) { + return /^[a-zA-Z0-9]+$/.test(key); +} + +function isLowerKey(key: string) { + if (!isKey(key)) { + return false; + } + + return !/[A-Z]/.test(key); +} + +function isUpperKey(key: string) { + if (!isKey(key)) { + return false; + } + + return !/[a-z]/.test(key); +} + +function _filter(name: string, a: object | any[], b: object | any[]) { + for (const key of Object.keys(a)) { + if (key.length > name.length && key.indexOf(name) === 0) { + const sub = key.substring(name.length); + b[sub] = a[key]; + } + } +} + +function fromFloat(num: number, exp: number) { + assert(typeof num === "number" && isFinite(num)); + assert(Number.isSafeInteger(exp)); + + let str = num.toFixed(exp); + let sign = 1; + + if (str.length > 0 && str[0] === "-") { + str = str.substring(1); + sign = -1; + } + + let hi: number | string = str; + let lo: number | string = "0"; + + const index = str.indexOf("."); + + if (index !== -1) { + hi = str.substring(0, index); + lo = str.substring(index + 1); + } + + hi = hi.replace(/^0+/, ""); + lo = lo.replace(/0+$/, ""); + + assert(hi.length <= 16 - exp, "Fixed number string exceeds 2^53-1."); + + assert(lo.length <= exp, "Too many decimal places in fixed number string."); + + if (hi.length === 0) { + hi = "0"; + } + + while (lo.length < exp) { + lo += "0"; + } + + if (lo.length === 0) { + lo = "0"; + } + + assert( + /^\d+$/.test(hi) && /^\d+$/.test(lo), + "Non-numeric characters in fixed number string." + ); + + hi = parseInt(hi, 10); + lo = parseInt(lo, 10); + + const mult = Math.pow(10, exp); + const maxLo = Number.MAX_SAFE_INTEGER % mult; + const maxHi = (Number.MAX_SAFE_INTEGER - maxLo) / mult; + + assert( + hi < maxHi || (hi === maxHi && lo <= maxLo), + "Fixed number string exceeds 2^53-1." + ); + + return sign * (hi * mult + lo); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2e80ca7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "sourceMap": true, + "esModuleInterop": true + }, + "include": ["src"], + "exclude": [ + "node_modules" + ] +}