From 18dcf0c42fff69399b209afda8df7a165457aefe Mon Sep 17 00:00:00 2001 From: Matt Artist Date: Tue, 26 Nov 2024 14:45:31 -0500 Subject: [PATCH 1/4] feat: allow exact values This just disables the inline comment escaping --- ini.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ini.js b/ini.js index 70296f1..b343dee 100644 --- a/ini.js +++ b/ini.js @@ -17,6 +17,7 @@ const encode = (obj, options) => { whitespace: false, inlineArrays: false, allowEmptySection: false, + exactValue: false, }; }else{ options = options || Object.create(null); @@ -100,7 +101,12 @@ const decode = (str, options = {}) => { } let key = unsafe(match[2]); if(isConstructorOrProto(ref, key)){ continue; } - let value = match[3] ? unsafe(match[3]) : defaultValue; + let value = null; + if(options.exactValue){ + value = match[3]; + }else{ + value = match[3] ? unsafe(match[3]) : defaultValue; + } switch(value){ case 'true': case 'True': @@ -180,6 +186,10 @@ const safe = (val, key, options = {}) => { if(key && options.forceStringifyKeys && options.forceStringifyKeys.includes(key)){ return JSON.stringify(val); } + if(options.exactValue){ + // Don't try to escape a comment in a value + return val; + } // comments return val.replace(/;/g, '\\;').replace(/#/g, '\\#'); }; From b4230e310c042f3ce95a6fb87974cc74511e40f2 Mon Sep 17 00:00:00 2001 From: Matt Artist Date: Tue, 26 Nov 2024 15:05:09 -0500 Subject: [PATCH 2/4] fix: test --- ini.js | 20 ++++++- test/fixtures/fooExactValues.ini | 68 ++++++++++++++++++++++++ test/foo.js | 91 ++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/fooExactValues.ini diff --git a/ini.js b/ini.js index b343dee..5e0161b 100644 --- a/ini.js +++ b/ini.js @@ -58,6 +58,7 @@ const encode = (obj, options) => { inlineArrays: options.inlineArrays, forceStringifyKeys: options.forceStringifyKeys, allowEmptySection: options.allowEmptySection, + exactValue: options.exactValue, }); if(out.length > 0 && child.length > 0){ out += eol; @@ -103,7 +104,7 @@ const decode = (str, options = {}) => { if(isConstructorOrProto(ref, key)){ continue; } let value = null; if(options.exactValue){ - value = match[3]; + value = match[3] ? unsafeExact(match[3]) : defaultValue; }else{ value = match[3] ? unsafe(match[3]) : defaultValue; } @@ -240,6 +241,23 @@ const unsafe = (val) => { return escapedVal.trim(); }; +const unsafeExact = (val) => { + val = (val || '').trim(); + if(isQuoted(val)){ + // remove the single quotes before calling JSON.parse + if(val.charAt(0) === "'"){ + val = val.substr(1, val.length - 2); // eslint-disable-line unicorn/prefer-string-slice + } + try{ + val = JSON.parse(val); + }catch{ + // we tried :( + } + return val; + } + return val.trim(); +}; + module.exports = { parse: decode, decode, diff --git a/test/fixtures/fooExactValues.ini b/test/fixtures/fooExactValues.ini new file mode 100644 index 0000000..5666460 --- /dev/null +++ b/test/fixtures/fooExactValues.ini @@ -0,0 +1,68 @@ +o = p + + a with spaces = b c + +; wrap in quotes to JSON-decode and preserve spaces +" xa n p " = "\"\r\nyoyoyo\r\r\n" + +; wrap in quotes to get a key with a bracket, not a section. +"[disturbing]" = hey you never know + +; Test single quotes +s = 'something' + +; Test mixing quotes + +s1 = "something' + +; Test double quotes +s2 = "something else" + +; Test arrays +zr[] = deedee +ar[] = one +ar[] = three +; This should be included in the array +ar = this is included + +; Test resetting of a value (and not turn it into an array) +br = cold +br = warm + +eq = "eq=eq" + +; Test no value +nv = + +; a section +[a] +av = a val +e = { o: p, a: { av: a val, b: { c: { e: "this [value]" } } } } +j = "{ o: "p", a: { av: "a val", b: { c: { e: "this [value]" } } } }" +"[]" = a square? + +; Nested array +cr[] = four +cr[] = eight + +; nested child without middle parent +; should create otherwise-empty a.b +[a.b.c] +e = 1 +j = 2 + +; dots in the section name should be literally interpreted +[x\.y\.z] +x.y.z = xyz + +[x\.y\.z.a\.b\.c] +a.b.c = abc + +; this next one is not a comment! it's escaped! +nocomment = this; this is not a comment + +# Support the use of the number sign (#) as an alternative to the semicolon for indicating comments. +# http://en.wikipedia.org/wiki/INI_file#Comments + +# this next one is not a comment! it's escaped! +noHashComment = this# this is not a comment diff --git a/test/foo.js b/test/foo.js index d33e1d9..e40ec94 100644 --- a/test/foo.js +++ b/test/foo.js @@ -8,8 +8,10 @@ const test = tap.test; const fixture = path.resolve(__dirname, "./fixtures/foo.ini"); const fixtureInlineArrays = path.resolve(__dirname, "./fixtures/fooInlineArrays.ini"); +const fixtureExactValues = path.resolve(__dirname, "./fixtures/fooExactValues.ini"); const data = fs.readFileSync(fixture, "utf8"); const dataInlineArrays = fs.readFileSync(fixtureInlineArrays, "utf8"); +const dataExactValues = fs.readFileSync(fixtureExactValues, "utf8"); const eol = require('os').EOL; @@ -115,6 +117,40 @@ const expectforceStringifyKeys = 'o=p' + eol + 'a.b.c=abc' + eol + 'nocomment=this\\; this is not a comment' + eol + 'noHashComment=this\\# this is not a comment' + eol; +const expectExactValues = 'o=p' + eol + + 'a with spaces=b c' + eol + + '" xa n p "="\\"\\r\\nyoyoyo\\r\\r\\n"' + eol + + '"[disturbing]"=hey you never know' + eol + + 's=something' + eol + + 's1="something\'' + eol + + 's2=something else' + eol + + 'zr[]=deedee' + eol + + 'ar[]=one' + eol + + 'ar[]=three' + eol + + 'ar[]=this is included' + eol + + 'br=warm' + eol + + 'eq="eq=eq"' + eol + + 'nv=' + eol + eol + + '[a]' + eol + + 'av=a val' + eol + + 'e={ o: p, a: ' + + '{ av: a val, b: { c: { e: "this [value]" ' + + '} } } }' + eol + + 'j="\\"{ o: \\"p\\", a: { av:' + + ' \\"a val\\", b: { c: { e: \\"this [value]' + + '\\" } } } }\\""' + eol + + '"[]"=a square?' + eol + + 'cr[]=four' + eol + + 'cr[]=eight' + eol + eol + + '[a.b.c]' + eol + + 'e=1' + eol + + 'j=2' + eol + eol + + '[x\\.y\\.z]' + eol + + 'x.y.z=xyz' + eol + eol + + '[x\\.y\\.z.a\\.b\\.c]' + eol + + 'a.b.c=abc' + eol + + 'nocomment=this; this is not a comment' + eol + + 'noHashComment=this# this is not a comment' + eol; const expectD = { o: 'p', 'a with spaces': 'b c', @@ -189,6 +225,43 @@ const expectDInlineArrays = { }, }, }; +const expectDExactValues = { + o: 'p', + 'a with spaces': 'b c', + " xa n p ": '"\r\nyoyoyo\r\r\n', + '[disturbing]': 'hey you never know', + 's': 'something', + 's1': '"something\'', + 's2': 'something else', + 'zr': ['deedee'], + 'ar': ['one', 'three', 'this is included'], + 'br': 'warm', + 'eq': 'eq=eq', + 'nv': '', + a: { + av: 'a val', + e: '{ o: p, a: { av: a val, b: { c: { e: "this [value]" } } } }', + j: '"{ o: "p", a: { av: "a val", b: { c: { e: "this [value]" } } } }"', + "[]": "a square?", + cr: [ + 'four', 'eight', + ], + b: { + c: { + e: '1', + j: '2', + }, + }, + }, + 'x.y.z': { + 'x.y.z': 'xyz', + 'a.b.c': { + 'a.b.c': 'abc', + 'nocomment': 'this; this is not a comment', + noHashComment: 'this# this is not a comment', + }, + }, +}; const expectF = '[prefix.log]' + eol + 'type=file' + eol + eol + '[prefix.log.level]' + eol @@ -213,6 +286,12 @@ test("decode from file inlineArrays=true", function(t){ t.end(); }); +test("decode from file exactValue=true", function(t){ + const d = ini.decode(dataExactValues, {exactValue: true}); + t.same(d, expectDExactValues); + t.end(); +}); + test("encode from data, inlineArrays=false", function(t){ let e = ini.encode(expectD, {inlineArrays: false}); t.same(e, expectE); @@ -237,6 +316,18 @@ test("encode from data, inlineArrays=true", function(t){ t.end(); }); +test("encode from data exactValue=true", function(t){ + let e = ini.encode(expectDExactValues, {exactValue: true}); + t.same(e, expectExactValues); + + const obj = {log: {type: 'file', level: {label: 'debug', value: 10}}}; + e = ini.encode(obj); + t.not(e.slice(0, 1), eol, 'Never a blank first line'); + t.not(e.slice(-2), eol + eol, 'Never a blank final line'); + + t.end(); +}); + test("encode with option", function(t){ const obj = {log: {type: 'file', level: {label: 'debug', value: 10}}}; const e = ini.encode(obj, {section: 'prefix'}); From e8b91f5e24e4f348c2b775f56a6fbbdc0875188a Mon Sep 17 00:00:00 2001 From: Matt Artist Date: Wed, 27 Nov 2024 10:40:59 -0500 Subject: [PATCH 3/4] feat: update readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index fd88cb5..4b88f76 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,13 @@ If you want to allow empty sections, you can set this option to `true`. ``` Previously, this would omit the section entirely on encode. Now, it will be included in the output. +## New `exactValue` option +If you want to preserve all characters in a value, you can set this option to `true`. +```ini + key=some string ; comment +``` +Previously, this would be parsed as `some string` and not `some string ; comment`. + ## Usage Consider an ini-file `config.ini` that looks like this: From c1a1ce23927cb160fedac358f5181fe01088ee03 Mon Sep 17 00:00:00 2001 From: Matt Artist Date: Wed, 27 Nov 2024 10:41:10 -0500 Subject: [PATCH 4/4] feat: handle common comment case --- ini.js | 6 +++++- test/foo.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ini.js b/ini.js index 5e0161b..b2200f2 100644 --- a/ini.js +++ b/ini.js @@ -227,7 +227,11 @@ const unsafe = (val) => { } isEscaping = false; }else if(commentChars.includes(char)){ - break; + // Check if there's spaces around this comment character + // If there is, then we're done parsing at the character before this one + if(val.charAt(i - 1) === ' ' && val.charAt(i + 1) === ' '){ + break; + } }else if(char === '\\'){ isEscaping = true; }else{ diff --git a/test/foo.js b/test/foo.js index e40ec94..7e562b7 100644 --- a/test/foo.js +++ b/test/foo.js @@ -400,7 +400,7 @@ test('ignores invalid line (=)', function(t){ test("unsafe escape values", function(t){ t.equal(ini.unsafe(''), ''); - t.equal(ini.unsafe('x;y'), 'x'); + t.equal(ini.unsafe('x;y'), 'xy'); t.equal(ini.unsafe('x # y'), 'x'); t.equal(ini.unsafe('x "\\'), 'x "\\'); t.end();