• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1exports.parse = exports.decode = decode
2
3exports.stringify = exports.encode = encode
4
5exports.safe = safe
6exports.unsafe = unsafe
7
8var eol = typeof process !== 'undefined' &&
9  process.platform === 'win32' ? '\r\n' : '\n'
10
11function encode (obj, opt) {
12  var children = []
13  var out = ''
14
15  if (typeof opt === 'string') {
16    opt = {
17      section: opt,
18      whitespace: false,
19    }
20  } else {
21    opt = opt || {}
22    opt.whitespace = opt.whitespace === true
23  }
24
25  var separator = opt.whitespace ? ' = ' : '='
26
27  Object.keys(obj).forEach(function (k, _, __) {
28    var val = obj[k]
29    if (val && Array.isArray(val)) {
30      val.forEach(function (item) {
31        out += safe(k + '[]') + separator + safe(item) + '\n'
32      })
33    } else if (val && typeof val === 'object')
34      children.push(k)
35    else
36      out += safe(k) + separator + safe(val) + eol
37  })
38
39  if (opt.section && out.length)
40    out = '[' + safe(opt.section) + ']' + eol + out
41
42  children.forEach(function (k, _, __) {
43    var nk = dotSplit(k).join('\\.')
44    var section = (opt.section ? opt.section + '.' : '') + nk
45    var child = encode(obj[k], {
46      section: section,
47      whitespace: opt.whitespace,
48    })
49    if (out.length && child.length)
50      out += eol
51
52    out += child
53  })
54
55  return out
56}
57
58function dotSplit (str) {
59  return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002')
60    .replace(/\\\./g, '\u0001')
61    .split(/\./).map(function (part) {
62      return part.replace(/\1/g, '\\.')
63        .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001')
64    })
65}
66
67function decode (str) {
68  var out = {}
69  var p = out
70  var section = null
71  //          section     |key      = value
72  var re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i
73  var lines = str.split(/[\r\n]+/g)
74
75  lines.forEach(function (line, _, __) {
76    if (!line || line.match(/^\s*[;#]/))
77      return
78    var match = line.match(re)
79    if (!match)
80      return
81    if (match[1] !== undefined) {
82      section = unsafe(match[1])
83      if (section === '__proto__') {
84        // not allowed
85        // keep parsing the section, but don't attach it.
86        p = {}
87        return
88      }
89      p = out[section] = out[section] || {}
90      return
91    }
92    var key = unsafe(match[2])
93    if (key === '__proto__')
94      return
95    var value = match[3] ? unsafe(match[4]) : true
96    switch (value) {
97      case 'true':
98      case 'false':
99      case 'null': value = JSON.parse(value)
100    }
101
102    // Convert keys with '[]' suffix to an array
103    if (key.length > 2 && key.slice(-2) === '[]') {
104      key = key.substring(0, key.length - 2)
105      if (key === '__proto__')
106        return
107      if (!p[key])
108        p[key] = []
109      else if (!Array.isArray(p[key]))
110        p[key] = [p[key]]
111    }
112
113    // safeguard against resetting a previously defined
114    // array by accidentally forgetting the brackets
115    if (Array.isArray(p[key]))
116      p[key].push(value)
117    else
118      p[key] = value
119  })
120
121  // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}}
122  // use a filter to return the keys that have to be deleted.
123  Object.keys(out).filter(function (k, _, __) {
124    if (!out[k] ||
125      typeof out[k] !== 'object' ||
126      Array.isArray(out[k]))
127      return false
128
129    // see if the parent section is also an object.
130    // if so, add it to that, and mark this one for deletion
131    var parts = dotSplit(k)
132    var p = out
133    var l = parts.pop()
134    var nl = l.replace(/\\\./g, '.')
135    parts.forEach(function (part, _, __) {
136      if (part === '__proto__')
137        return
138      if (!p[part] || typeof p[part] !== 'object')
139        p[part] = {}
140      p = p[part]
141    })
142    if (p === out && nl === l)
143      return false
144
145    p[nl] = out[k]
146    return true
147  }).forEach(function (del, _, __) {
148    delete out[del]
149  })
150
151  return out
152}
153
154function isQuoted (val) {
155  return (val.charAt(0) === '"' && val.slice(-1) === '"') ||
156    (val.charAt(0) === "'" && val.slice(-1) === "'")
157}
158
159function safe (val) {
160  return (typeof val !== 'string' ||
161    val.match(/[=\r\n]/) ||
162    val.match(/^\[/) ||
163    (val.length > 1 &&
164     isQuoted(val)) ||
165    val !== val.trim())
166    ? JSON.stringify(val)
167    : val.replace(/;/g, '\\;').replace(/#/g, '\\#')
168}
169
170function unsafe (val, doUnesc) {
171  val = (val || '').trim()
172  if (isQuoted(val)) {
173    // remove the single quotes before calling JSON.parse
174    if (val.charAt(0) === "'")
175      val = val.substr(1, val.length - 2)
176
177    try {
178      val = JSON.parse(val)
179    } catch (_) {}
180  } else {
181    // walk the val to find the first not-escaped ; character
182    var esc = false
183    var unesc = ''
184    for (var i = 0, l = val.length; i < l; i++) {
185      var c = val.charAt(i)
186      if (esc) {
187        if ('\\;#'.indexOf(c) !== -1)
188          unesc += c
189        else
190          unesc += '\\' + c
191
192        esc = false
193      } else if (';#'.indexOf(c) !== -1)
194        break
195      else if (c === '\\')
196        esc = true
197      else
198        unesc += c
199    }
200    if (esc)
201      unesc += '\\'
202
203    return unesc.trim()
204  }
205  return val
206}
207