codekingpro/portable-devtools
115k
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.endsWith('[]')
144 ? keyRaw.slice(0, -2) : keyRaw
145
146 if (key === '__proto__') {
147 continue
148 }
149 const valueRaw = match[3] ? unsafe(match[4]) : true
150 const value = valueRaw === 'true' ||
151 valueRaw === 'false' ||
152 valueRaw === 'null' ? JSON.parse(valueRaw)
153 : valueRaw
154
155 // Convert keys with '[]' suffix to an array
156 if (isArray) {
157 if (!hasOwnProperty.call(p, key)) {
158 p[key] = []
159 } else if (!Array.isArray(p[key])) {
160 p[key] = [p[key]]
161 }
162 }
163
164 // safeguard against resetting a previously defined
165 // array by accidentally forgetting the brackets
166 if (Array.isArray(p[key])) {
167 p[key].push(value)
168 } else {
169 p[key] = value
170 }
171 }
172
173 // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}}
174 // use a filter to return the keys that have to be deleted.
175 const remove = []
176 for (const k of Object.keys(out)) {
177 if (!hasOwnProperty.call(out, k) ||
178 typeof out[k] !== 'object' ||
179 Array.isArray(out[k])) {
180 continue
181 }
182
183 // see if the parent section is also an object.
184 // if so, add it to that, and mark this one for deletion
185 const parts = splitSections(k, '.')
186 p = out
187 const l = parts.pop()
188 const nl = l.replace(/\\\./g, '.')
189 for (const part of parts) {
190 if (part === '__proto__') {
191 continue
192 }
193 if (!hasOwnProperty.call(p, part) || typeof p[part] !== 'object') {
194 p[part] = Object.create(null)
195 }
196 p = p[part]
197 }
198 if (p === out && nl === l) {
199 continue
200 }
201
202 p[nl] = out[k]
203 remove.push(k)
204 }
205 for (const del of remove) {
206 delete out[del]
207 }
208
209 return out
210}
211
212const isQuoted = val => {
213 return (val.startsWith('"') && val.endsWith('"')) ||
214 (val.startsWith("'") && val.endsWith("'"))
215}
216
217const safe = val => {
218 if (
219 typeof val !== 'string' ||
220 val.match(/[=\r\n]/) ||
221 val.match(/^\[/) ||
222 (val.length > 1 && isQuoted(val)) ||
223 val !== val.trim()
224 ) {
225 return JSON.stringify(val)
226 }
227 return val.split(';').join('\\;').split('#').join('\\#')
228}
229
230const unsafe = val => {
231 val = (val || '').trim()
232 if (isQuoted(val)) {
233 // remove the single quotes before calling JSON.parse
234 if (val.charAt(0) === "'") {
235 val = val.slice(1, -1)
236 }
237 try {
238 val = JSON.parse(val)
239 } catch {
240 // ignore errors
241 }
242 } else {
243 // walk the val to find the first not-escaped ; character
244 let esc = false
245 let unesc = ''
246 for (let i = 0, l = val.length; i < l; i++) {
247 const c = val.charAt(i)
248 if (esc) {
249 if ('\\;#'.indexOf(c) !== -1) {
250 unesc += c
251 } else {
252 unesc += '\\' + c
253 }
254
255 esc = false
256 } else if (';#'.indexOf(c) !== -1) {
257 break
258 } else if (c === '\\') {
259 esc = true
260 } else {
261 unesc += c
262 }
263 }
264 if (esc) {
265 unesc += '\\'
266 }
267
268 return unesc.trim()
269 }
270 return val
271}
272
273module.exports = {
274 parse: decode,
275 decode,
276 stringify: encode,
277 encode,
278 safe,
279 unsafe,
280}
281 