From 9a3a0c72cd5828342cf4ec29a3d120db3147ef2f Mon Sep 17 00:00:00 2001 From: greymoth <246701683+greymoth-jp@users.noreply.github.com> Date: Tue, 30 Jun 2026 08:25:56 +0900 Subject: [PATCH] fix: escape backslashes in values and keys so they round-trip `unsafe` treats `\` as an escape character and unescapes `\\`, `\;` and `\#`, but `safe` only escaped `;` and `#`. A value or key containing a backslash therefore did not survive stringify -> parse: a value of two backslashes was written as `k=\\` and read back as a single backslash. `safe` now escapes the backslash as well. Section names are left unchanged because they rely on `\.` to escape literal dots in key paths. --- lib/ini.js | 10 +++++++--- test/bar.js | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/ini.js b/lib/ini.js index beb390d..100a04f 100644 --- a/lib/ini.js +++ b/lib/ini.js @@ -57,7 +57,7 @@ const encode = (obj, opt = {}) => { } if (opt.section && out.length) { - out = '[' + safe(opt.section) + ']' + (opt.newline ? eol + eol : eol) + out + out = '[' + safe(opt.section, false) + ']' + (opt.newline ? eol + eol : eol) + out } for (const k of children) { @@ -214,7 +214,7 @@ const isQuoted = val => { (val.startsWith("'") && val.endsWith("'")) } -const safe = val => { +const safe = (val, escapeBackslash = true) => { if ( typeof val !== 'string' || val.match(/[=\r\n]/) || @@ -224,7 +224,11 @@ const safe = val => { ) { return JSON.stringify(val) } - return val.split(';').join('\\;').split('#').join('\\#') + // `unsafe` treats `\` as an escape character and unescapes `\\`, `\;` and + // `\#`, so values and keys must escape backslashes to round-trip. Section + // names opt out because they use `\.` to escape literal dots in key paths. + const escaped = escapeBackslash ? val.split('\\').join('\\\\') : val + return escaped.split(';').join('\\;').split('#').join('\\#') } const unsafe = val => { diff --git a/test/bar.js b/test/bar.js index 8f7eb23..9669407 100644 --- a/test/bar.js +++ b/test/bar.js @@ -19,3 +19,25 @@ test('parse(stringify(x)) is same as x', function (t) { t.end() }) + +// `unsafe` unescapes `\\`, `\;` and `\#`, so `safe` has to escape the +// backslash for those values to survive a round trip. +test('parse(stringify(x)) round-trips values containing backslashes', function (t) { + var values = { + 'two backslashes': '\\\\', // \ \ + 'windows path': 'C:\\\\tmp\\\\x', // C : \ \ t m p \ \ x + 'backslash before semicolon': 'a\\;b', // a \ ; b + 'backslash before hash': 'a\\#b', // a \ # b + } + for (var label in values) { + var obj = { k: values[label] } + t.same(ini.parse(ini.stringify(obj)), obj, label) + } + + // a backslash in a key name has to round-trip as well + var keyed = {} + keyed['a\\\\b'] = 'v' // a \ \ b + t.same(ini.parse(ini.stringify(keyed)), keyed, 'backslash in key') + + t.end() +})