1const { hasOwnProperty } = Object.prototype 2 3const encode = (obj, opt = {}) => { 4 if (typeof opt === 'string') { 5 opt = { section: opt } 6 } 7 opt.align = opt.align === true 8 opt.newline = opt.newline === true 9 opt.sort = opt.sort === true 10 opt.whitespace = opt.whitespace === true || opt.align === true 11 // The `typeof` check is required because accessing the `process` directly fails on browsers. 12 /* istanbul ignore next */ 13 opt.platform = opt.platform || (typeof process !== 'undefined' && process.platform) 14 opt.bracketedArray = opt.bracketedArray !== false 15 16 /* istanbul ignore next */ 17 const eol = opt.platform === 'win32' ? '\r\n' : '\n' 18 const separator = opt.whitespace ? ' = ' : '=' 19 const children = [] 20 21 const keys = opt.sort ? Object.keys(obj).sort() : Object.keys(obj) 22 23 let padToChars = 0 24 // If aligning on the separator, then padToChars is determined as follows: 25 // 1. Get the keys 26 // 2. Exclude keys pointing to objects unless the value is null or an array 27 // 3. Add `[]` to array keys 28 // 4. Ensure non empty set of keys 29 // 5. Reduce the set to the longest `safe` key 30 // 6. Get the `safe` length 31 if (opt.align) { 32 padToChars = safe( 33 ( 34 keys 35 .filter(k => obj[k] === null || Array.isArray(obj[k]) || typeof obj[k] !== 'object') 36 .map(k => Array.isArray(obj[k]) ? `${k}[]` : k) 37 ) 38 .concat(['']) 39 .reduce((a, b) => safe(a).length >= safe(b).length ? a : b) 40 ).length 41 } 42 43 let out = '' 44 const arraySuffix = opt.bracketedArray ? '[]' : '' 45 46 for (const k of keys) { 47 const val = obj[k] 48 if (val && Array.isArray(val)) { 49 for (const item of val) { 50 out += safe(`${k}${arraySuffix}`).padEnd(padToChars, ' ') + separator + safe(item) + eol 51 } 52 } else if (val && typeof val === 'object') { 53 children.push(k) 54 } else { 55 out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol 56 } 57 } 58 59 if (opt.section && out.length) { 60 out = '[' + safe(opt.section) + ']' + (opt.newline ? eol + eol : eol) + out 61 } 62 63 for (const k of children) { 64 const nk = splitSections(k, '.').join('\\.') 65 const section = (opt.section ? opt.section + '.' : '') + nk 66 const child = encode(obj[k], { 67 ...opt, 68 section, 69 }) 70 if (out.length && child.length) { 71 out += eol 72 } 73 74 out += child 75 } 76 77 return out 78} 79 80function splitSections (str, separator) { 81 var lastMatchIndex = 0 82 var lastSeparatorIndex = 0 83 var nextIndex = 0 84 var sections = [] 85 86 do { 87 nextIndex = str.indexOf(separator, lastMatchIndex) 88 89 if (nextIndex !== -1) { 90 lastMatchIndex = nextIndex + separator.length 91 92 if (nextIndex > 0 && str[nextIndex - 1] === '\\') { 93 continue 94 } 95 96 sections.push(str.slice(lastSeparatorIndex, nextIndex)) 97 lastSeparatorIndex = nextIndex + separator.length 98 } 99 } while (nextIndex !== -1) 100 101 sections.push(str.slice(lastSeparatorIndex)) 102 103 return sections 104} 105 106const decode = (str, opt = {}) => { 107 opt.bracketedArray = opt.bracketedArray !== false 108 const out = Object.create(null) 109 let p = out 110 let section = null 111 // section |key = value 112 const re = /^\[([^\]]*)\]\s*$|^([^=]+)(=(.*))?$/i 113 const lines = str.split(/[\r\n]+/g) 114 const duplicates = {} 115 116 for (const line of lines) { 117 if (!line || line.match(/^\s*[;#]/) || line.match(/^\s*$/)) { 118 continue 119 } 120 const match = line.match(re) 121 if (!match) { 122 continue 123 } 124 if (match[1] !== undefined) { 125 section = unsafe(match[1]) 126 if (section === '__proto__') { 127 // not allowed 128 // keep parsing the section, but don't attach it. 129 p = Object.create(null) 130 continue 131 } 132 p = out[section] = out[section] || Object.create(null) 133 continue 134 } 135 const keyRaw = unsafe(match[2]) 136 let isArray 137 if (opt.bracketedArray) { 138 isArray = keyRaw.length > 2 && keyRaw.slice(-2) === '[]' 139 } else { 140 duplicates[keyRaw] = (duplicates?.[keyRaw] || 0) + 1 141 isArray = duplicates[keyRaw] > 1 142 } 143 const key = isArray ? keyRaw.slice(0, -2) : keyRaw 144 if (key === '__proto__') { 145 continue 146 } 147 const valueRaw = match[3] ? unsafe(match[4]) : true 148 const value = valueRaw === 'true' || 149 valueRaw === 'false' || 150 valueRaw === 'null' ? JSON.parse(valueRaw) 151 : valueRaw 152 153 // Convert keys with '[]' suffix to an array 154 if (isArray) { 155 if (!hasOwnProperty.call(p, key)) { 156 p[key] = [] 157 } else if (!Array.isArray(p[key])) { 158 p[key] = [p[key]] 159 } 160 } 161 162 // safeguard against resetting a previously defined 163 // array by accidentally forgetting the brackets 164 if (Array.isArray(p[key])) { 165 p[key].push(value) 166 } else { 167 p[key] = value 168 } 169 } 170 171 // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} 172 // use a filter to return the keys that have to be deleted. 173 const remove = [] 174 for (const k of Object.keys(out)) { 175 if (!hasOwnProperty.call(out, k) || 176 typeof out[k] !== 'object' || 177 Array.isArray(out[k])) { 178 continue 179 } 180 181 // see if the parent section is also an object. 182 // if so, add it to that, and mark this one for deletion 183 const parts = splitSections(k, '.') 184 p = out 185 const l = parts.pop() 186 const nl = l.replace(/\\\./g, '.') 187 for (const part of parts) { 188 if (part === '__proto__') { 189 continue 190 } 191 if (!hasOwnProperty.call(p, part) || typeof p[part] !== 'object') { 192 p[part] = Object.create(null) 193 } 194 p = p[part] 195 } 196 if (p === out && nl === l) { 197 continue 198 } 199 200 p[nl] = out[k] 201 remove.push(k) 202 } 203 for (const del of remove) { 204 delete out[del] 205 } 206 207 return out 208} 209 210const isQuoted = val => { 211 return (val.startsWith('"') && val.endsWith('"')) || 212 (val.startsWith("'") && val.endsWith("'")) 213} 214 215const safe = val => { 216 if ( 217 typeof val !== 'string' || 218 val.match(/[=\r\n]/) || 219 val.match(/^\[/) || 220 (val.length > 1 && isQuoted(val)) || 221 val !== val.trim() 222 ) { 223 return JSON.stringify(val) 224 } 225 return val.split(';').join('\\;').split('#').join('\\#') 226} 227 228const unsafe = (val, doUnesc) => { 229 val = (val || '').trim() 230 if (isQuoted(val)) { 231 // remove the single quotes before calling JSON.parse 232 if (val.charAt(0) === "'") { 233 val = val.slice(1, -1) 234 } 235 try { 236 val = JSON.parse(val) 237 } catch { 238 // ignore errors 239 } 240 } else { 241 // walk the val to find the first not-escaped ; character 242 let esc = false 243 let unesc = '' 244 for (let i = 0, l = val.length; i < l; i++) { 245 const c = val.charAt(i) 246 if (esc) { 247 if ('\\;#'.indexOf(c) !== -1) { 248 unesc += c 249 } else { 250 unesc += '\\' + c 251 } 252 253 esc = false 254 } else if (';#'.indexOf(c) !== -1) { 255 break 256 } else if (c === '\\') { 257 esc = true 258 } else { 259 unesc += c 260 } 261 } 262 if (esc) { 263 unesc += '\\' 264 } 265 266 return unesc.trim() 267 } 268 return val 269} 270 271module.exports = { 272 parse: decode, 273 decode, 274 stringify: encode, 275 encode, 276 safe, 277 unsafe, 278} 279