diff --git a/lib/node/config.js b/lib/node/config.js index 4d397733..e25a3504 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -135,7 +135,8 @@ Config.prototype.set = function set(key, value) { if (value == null) return; - key = key.toLowerCase().replace(/-/g, ''); + key = key.replace(/-/g, ''); + key = key.toLowerCase(); this.options[key] = value; }; @@ -156,7 +157,8 @@ Config.prototype.has = function has(key) { assert(typeof key === 'string', 'Key must be a string.'); - key = key.toLowerCase().replace(/-/g, ''); + key = key.replace(/-/g, ''); + key = key.toLowerCase(); if (this.hash[key] != null) return true; @@ -214,7 +216,8 @@ Config.prototype.get = function get(key, fallback) { assert(typeof key === 'string', 'Key must be a string.'); - key = key.toLowerCase().replace(/-/g, ''); + key = key.replace(/-/g, ''); + key = key.toLowerCase(); if (this.hash[key] != null) return this.hash[key]; @@ -735,47 +738,69 @@ Config.prototype.location = function location(file) { Config.prototype.parseConfig = function parseConfig(text) { assert(typeof text === 'string', 'Config must be text.'); - text = text.trim(); + if (text.charCodeAt(0) === 0xfeff) + text = text.substring(1); - const parts = text.split(/\n+/); + text = text.replace(/\r\n/g, '\n'); + text = text.replace(/\r/g, '\n'); + text = text.replace(/\\\n/g, ''); - for (let line of parts) { - line = line.trim(); + let colons = true; + let seen = false; + let num = 0; + + for (const chunk of text.split('\n')) { + const line = chunk.trim(); + + num += 1; if (line.length === 0) continue; - if (/^\s*#/.test(line)) + if (line[0] === '#') continue; - let eq = line.indexOf('='); - const col = line.indexOf(':'); + const equal = line.indexOf('='); + const colon = line.indexOf(':'); - if (col !== -1 && (col < eq || eq === -1)) - eq = col; + let index = -1; - let key, value; - if (eq === -1) { - key = line.trim(); - value = ''; + if (colon !== -1 && (colon < equal || equal === -1)) { + if (seen && !colons) + throw new Error(`Expected \`=\` on line ${num}: "${line}".`); + + index = colon; + seen = true; + colons = true; + } else if (equal !== -1) { + if (seen && colons) + throw new Error(`Expected \`:\` on line ${num}: "${line}".`); + + index = equal; + seen = true; + colons = false; } else { - key = line.substring(0, eq).trim(); - value = line.substring(eq + 1).trim(); + const symbol = colons ? ':' : '='; + throw new Error(`Expected \`${symbol}\` on line ${num}: "${line}".`); } - key = key.replace(/\-/g, '').toLowerCase(); + 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 = Config.alias[key]; if (alias) key = alias; - if (key.length === 0) - continue; - - if (value.length === 0) - continue; - this.data[key] = value; } }; @@ -816,19 +841,21 @@ Config.prototype.parseArg = function parseArg(argv) { } if (arg.indexOf('--') === 0) { - // e.g. --opt - const parts = arg.split('='); + const index = arg.indexOf('='); - let key = parts[0]; + let key = null; let value = null; let empty = false; - if (parts.length > 1) { + if (index !== -1) { // e.g. --opt=val - value = parts.slice(1).join('=').trim(); + 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; @@ -836,8 +863,8 @@ Config.prototype.parseArg = function parseArg(argv) { key = key.replace(/\-/g, ''); - if (key.length === 0) - continue; + if (!isLowerKey(key)) + throw new Error(`Invalid argument: --${key}.`); if (value.length === 0) continue; @@ -860,19 +887,32 @@ Config.prototype.parseArg = function parseArg(argv) { 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 = Config.alias[key]; + if (alias) key = alias; + this.args[key] = 'true'; + last = key; } + continue; } // e.g. foo - const value = arg.trim(); + const value = arg; if (value.length === 0) { last = null; @@ -908,24 +948,24 @@ Config.prototype.parseEnv = function parseEnv(env) { assert(env && typeof env === 'object'); for (let key of Object.keys(env)) { - let value = env[key]; + const value = env[key]; assert(typeof value === 'string'); - if (key.indexOf(prefix) !== 0) + if (!util.startsWith(key, prefix)) continue; key = key.substring(prefix.length); - key = key.replace(/_/g, '').toLowerCase(); + key = key.replace(/_/g, ''); - if (key.length === 0) + if (!isUpperKey(key)) continue; - value = value.trim(); - if (value.length === 0) continue; + key = key.toLowerCase(); + // Do not allow one-letter aliases. if (key.length > 1) { const alias = Config.alias[key]; @@ -989,27 +1029,30 @@ Config.prototype.parseForm = function parseForm(query, map) { if (query.length === 0) return; - if (query[0] === '?' || query[0] === '#') + let ch = '?'; + + if (map === this.hash) + ch = '#'; + + if (query[0] === ch) query = query.substring(1); - const parts = query.split('&'); - - for (const pair of parts) { + for (const pair of query.split('&')) { const index = pair.indexOf('='); let key, value; - if (index === -1) { - key = pair; - value = ''; - } else { + 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, '').toLowerCase(); + key = key.replace(/\-/g, ''); - if (key.length === 0) + if (!isLowerKey(key)) continue; value = unescape(value); @@ -1055,6 +1098,24 @@ function isAlpha(str) { return /^[a-z0-9]+$/.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); +} + /* * Expose */