Skip to content

Commit

Permalink
Merge upstream/master; include prototype pollution safeguard (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericyd authored Jun 21, 2024
1 parent be956f3 commit 9b8342d
Show file tree
Hide file tree
Showing 7 changed files with 3,242 additions and 2,210 deletions.
75 changes: 65 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# flat [![Build Status](https://secure.travis-ci.org/hughsk/flat.png?branch=master)](http://travis-ci.org/hughsk/flat)
# flat

> [!NOTE]
> Originally forked to fix a bug where dot notation keys in a nested array were not unflattened,
> see https://github.com/activeprospect/flat/commit/27b5bfaaab986028dd9d8bb8027d5dce2e5a874b.
>
> Fork is maintained because LeadConduit is not set up to easily consume ES modules, and the
> upstream package only ships an ESM package. This lib contains equivalent code, but maintains
> a CommonJS file format for compatibility with all LeadConduit integrations and applications.
Take a nested Javascript object and flatten it, or unflatten an object with
delimited keys.
Expand All @@ -17,7 +25,7 @@ Flattens the object - it'll return an object one level deep, regardless of how
nested the original object was:

``` javascript
var flatten = require('flat')
const { flatten } = require('flat')

flatten({
key1: {
Expand All @@ -38,10 +46,10 @@ flatten({

### unflatten(original, options)

Flattening is reversible too, you can call `flatten.unflatten()` on an object:
Flattening is reversible too, you can call `unflatten` on an object:

``` javascript
var unflatten = require('flat').unflatten
const { unflatten } = require('flat')

unflatten({
'three.levels.deep': 42,
Expand Down Expand Up @@ -71,7 +79,7 @@ Use a custom delimiter for (un)flattening your objects, instead of `.`.
When enabled `flatten` will preserve arrays and their contents. This is disabled by default.

``` javascript
var flatten = require('flat')
const { flatten } = require('flat')

flatten({
this: [
Expand Down Expand Up @@ -121,7 +129,7 @@ When enabled, existing keys in the unflattened object may be overwritten if they
```javascript
unflatten({
'TRAVIS': 'true',
'TRAVIS_DIR': '/home/travis/build/kvz/environmental'
'TRAVIS.DIR': '/home/travis/build/kvz/environmental'
}, { overwrite: true })

// TRAVIS: {
Expand All @@ -139,7 +147,7 @@ This only makes sense on ordered arrays, and since we're overwriting data, shoul
Maximum number of nested objects to flatten.

``` javascript
var flatten = require('flat')
const { flatten } = require('flat')

flatten({
key1: {
Expand All @@ -158,10 +166,57 @@ flatten({
// }
```

### transformKey

Transform each part of a flat key before and after flattening.

```javascript
const { flatten, unflatten } = require('flat')

flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
}, {
transformKey: function(key){
return '__' + key + '__';
}
})

// {
// '__key1__.__keyA__': 'valueI',
// '__key2__.__keyB__': 'valueII',
// '__key3__.__a__.__b__.__c__': 2
// }

unflatten({
'__key1__.__keyA__': 'valueI',
'__key2__.__keyB__': 'valueII',
'__key3__.__a__.__b__.__c__': 2
}, {
transformKey: function(key){
return key.substring(2, key.length - 2)
}
})

// {
// key1: {
// keyA: 'valueI'
// },
// key2: {
// keyB: 'valueII'
// },
// key3: { a: { b: { c: 2 } } }
// }
```

## Command Line Usage

`flat` is also available as a command line tool. You can run it with
[`npx`](https://ghub.io/npx):
`flat` is also available as a command line tool. You can run it with [`npx`](https://docs.npmjs.com/cli/v8/commands/npx):

```sh
npx flat foo.json
Expand All @@ -183,4 +238,4 @@ Also accepts JSON on stdin:

```sh
cat foo.json | flat
```
```
27 changes: 14 additions & 13 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#!/usr/bin/env node
const fs = require('node:fs')
const path = require('node:path')
const readline = require('node:readline')
const { flatten } = require('./index.js')

const flat = require('.')
const fs = require('fs')
const path = require('path')
const readline = require('readline')

if (process.stdin.isTTY) {
const filepath = process.argv.slice(2)[0]
if (filepath) {
// Read from file
const file = path.resolve(process.cwd(), process.argv.slice(2)[0])
if (!file) usage()
if (!fs.existsSync(file)) usage()
out(require(file))
const file = path.resolve(process.cwd(), filepath)
fs.accessSync(file, fs.constants.R_OK) // allow to throw if not readable
out(JSON.parse(fs.readFileSync(file)))
} else if (process.stdin.isTTY) {
usage(0)
} else {
// Read from newline-delimited STDIN
const lines = []
Expand All @@ -24,16 +25,16 @@ if (process.stdin.isTTY) {
}

function out (data) {
process.stdout.write(JSON.stringify(flat(data), null, 2))
process.stdout.write(JSON.stringify(flatten(data), null, 2))
}

function usage () {
function usage (code) {
console.log(`
Usage:
flat foo.json
cat foo.json | flat
`)

process.exit()
process.exit(code || 0)
}
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface FlattenOptions {
delimiter?: string;
maxDepth?: number;
safe?: boolean;
transformKey?: (key: string) => string;
}

export function flatten<T, R>(target: T, options?: FlattenOptions): R;

export interface UnflattenOptions {
delimiter?: string;
object?: boolean;
overwrite?: boolean;
transformKey?: (key: string) => string;
}

export function unflatten<T, R>(target: T, options?: UnflattenOptions): R;
111 changes: 81 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
var isBuffer = require('is-buffer')

module.exports = flatten
flatten.flatten = flatten
flatten.unflatten = unflatten

function isBuffer (obj) {
return obj &&
obj.constructor &&
(typeof obj.constructor.isBuffer === 'function') &&
obj.constructor.isBuffer(obj)
}

function keyIdentity (key) {
return key
}

function flatten (target, opts) {
opts = opts || {}

var delimiter = opts.delimiter || '.'
var maxDepth = opts.maxDepth
var output = {}
const delimiter = opts.delimiter || '.'
const maxDepth = opts.maxDepth
const transformKey = opts.transformKey || keyIdentity
const output = {}

function step (object, prev, currentDepth) {
currentDepth = currentDepth || 1
Object.keys(object).forEach(function (key) {
var value = object[key]
var isarray = opts.safe && Array.isArray(value)
var type = Object.prototype.toString.call(value)
var isbuffer = isBuffer(value)
var isobject = (
const value = object[key]
const isarray = opts.safe && Array.isArray(value)
const type = Object.prototype.toString.call(value)
const isbuffer = isBuffer(value)
const isobject = (
type === '[object Object]' ||
type === '[object Array]'
)

var newKey = prev
? prev + delimiter + key
: key
const newKey = prev
? prev + delimiter + transformKey(key)
: transformKey(key)

if (!isarray && !isbuffer && isobject && Object.keys(value).length &&
(!opts.maxDepth || currentDepth < maxDepth)) {
Expand All @@ -44,11 +54,12 @@ function flatten (target, opts) {
function unflatten (target, opts) {
opts = opts || {}

var delimiter = opts.delimiter || '.'
var overwrite = opts.overwrite || false
var result = {}
const delimiter = opts.delimiter || '.'
const overwrite = opts.overwrite || false
const transformKey = opts.transformKey || keyIdentity
const result = {}

var isbuffer = isBuffer(target)
const isbuffer = isBuffer(target)
if (Array.isArray(target)) {
return target.map(function (item) {
return unflatten(item, opts)
Expand All @@ -60,29 +71,67 @@ function unflatten (target, opts) {
// safely ensure that the key is
// an integer.
function getkey (key) {
var parsedKey = Number(key)
const parsedKey = Number(key)

return (
isNaN(parsedKey) ||
key.indexOf('.') !== -1 ||
opts.object
) ? key
)
? key
: parsedKey
}

var sortedKeys = Object.keys(target).sort(function (keyA, keyB) {
return keyA.length - keyB.length
})
function addKeys (keyPrefix, recipient, target) {
return Object.keys(target).reduce(function (result, key) {
result[keyPrefix + delimiter + key] = target[key]

sortedKeys.forEach(function (key) {
var split = key.split(delimiter)
var key1 = getkey(split.shift())
var key2 = getkey(split[0])
var recipient = result
return result
}, recipient)
}

function isEmpty (val) {
const type = Object.prototype.toString.call(val)
const isArray = type === '[object Array]'
const isObject = type === '[object Object]'

if (!val) {
return true
} else if (isArray) {
return !val.length
} else if (isObject) {
return !Object.keys(val).length
}
}

target = Object.keys(target).reduce(function (result, key) {
const type = Object.prototype.toString.call(target[key])
const isObject = (type === '[object Object]' || type === '[object Array]')
if (!isObject || isEmpty(target[key])) {
result[key] = target[key]
return result
} else {
return addKeys(
key,
result,
flatten(target[key], opts)
)
}
}, {})

Object.keys(target).forEach(function (key) {
const split = key.split(delimiter).map(transformKey)
let key1 = getkey(split.shift())
let key2 = getkey(split[0])
let recipient = result

while (key2 !== undefined) {
var type = Object.prototype.toString.call(recipient[key1])
var isobject = (
if (key1 === '__proto__') {
return
}

const type = Object.prototype.toString.call(recipient[key1])
const isobject = (
type === '[object Object]' ||
type === '[object Array]'
)
Expand All @@ -95,7 +144,9 @@ function unflatten (target, opts) {
if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) {
recipient[key1] = (
typeof key2 === 'number' &&
!opts.object ? [] : {}
!opts.object
? []
: {}
)
}

Expand Down
Loading

0 comments on commit 9b8342d

Please sign in to comment.