• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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