cson-parser
Version:
Safe parsing of CSON files
236 lines (224 loc) • 7.03 kB
JavaScript
/*
* Copyright (c) 2014, Groupon, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of GROUPON nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
;
const { runInThisContext } = require('vm');
const { nodes } = require('coffeescript');
function defaultReviver(key, value) {
return value;
}
function getFunctionNameIE(fn) {
return fn.toString().match(/^function\s*([^( ]+)/)[1];
}
function nodeTypeString(csNode) {
const ref = csNode.constructor.name;
return ref != null ? ref : getFunctionNameIE(csNode.constructor);
}
function syntaxErrorMessage(csNode, msg) {
const ref = csNode.locationData;
const lineIdx = ref.first_line;
const columnIdx = ref.first_column;
let line = '';
let column = '';
if (lineIdx != null) {
line = lineIdx + 1;
}
if (columnIdx != null) {
column = columnIdx + 1;
}
return `Syntax error on line ${line}, column ${column}: ${msg}`;
}
function parseStringLiteral(literal) {
return runInThisContext(literal);
}
function parseRegExpLiteral(literal) {
return runInThisContext(literal);
}
function transformKey(csNode) {
const type = nodeTypeString(csNode);
if (type !== 'Value') {
throw new SyntaxError(syntaxErrorMessage(csNode, `${type} used as key`));
}
const value = csNode.base.value;
switch (value.charAt(0)) {
case "'":
case '"':
return parseStringLiteral(value);
default:
return value;
}
}
const nodeTransforms = {
Block: function Block(node, transformNode) {
const expressions = node.expressions;
if (!expressions || expressions.length !== 1) {
throw new SyntaxError(
syntaxErrorMessage(node, 'One top level value expected')
);
}
return transformNode(expressions[0]);
},
Value: function Value(node, transformNode) {
return transformNode(node.base);
},
Bool: function Bool(node) {
return node.val === 'true';
},
BooleanLiteral: function BooleanLiteral(node) {
return node.value === 'true';
},
Null: function Null() {
return null;
},
NullLiteral: function NullLiteral() {
return null;
},
Literal: function Literal(node) {
const value = node.value;
try {
switch (value.charAt(0)) {
case "'":
case '"':
return parseStringLiteral(value);
case '/':
return parseRegExpLiteral(value);
default:
return JSON.parse(value);
}
} catch (error) {
throw new SyntaxError(syntaxErrorMessage(node, error.message));
}
},
NumberLiteral: function NumberLiteral(node) {
return Number(node.value);
},
StringLiteral: function StringLiteral(node) {
return parseStringLiteral(node.value);
},
RegexLiteral: function RegexLiteral(node) {
return parseRegExpLiteral(node.value);
},
Arr: function Arr(node, transformNode) {
return node.objects.map(transformNode);
},
Obj: function Obj(node, transformNode, reviver) {
return node.properties.reduce((outObject, property) => {
const variable = property.variable;
let value = property.value;
if (!variable) {
return outObject;
}
const keyName = transformKey(variable);
value = transformNode(value);
outObject[keyName] = reviver.call(outObject, keyName, value);
return outObject;
}, {});
},
Op: function Op(node, transformNode) {
if (node.second != null) {
const left = transformNode(node.first);
const right = transformNode(node.second);
switch (node.operator) {
case '-':
return left - right;
case '+':
return left + right;
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
case '&':
return left & right;
case '|':
return left | right;
case '^':
return left ^ right;
case '<<':
return left << right;
case '>>>':
return left >>> right;
case '>>':
return left >> right;
default:
throw new SyntaxError(
syntaxErrorMessage(node, `Unknown binary operator ${node.operator}`)
);
}
} else {
switch (node.operator) {
case '-':
return -transformNode(node.first);
case '~':
return ~transformNode(node.first);
default:
throw new SyntaxError(
syntaxErrorMessage(node, `Unknown unary operator ${node.operator}`)
);
}
}
},
Parens: function Parens(node, transformNode) {
const expressions = node.body.expressions;
if (!expressions || expressions.length !== 1) {
throw new SyntaxError(
syntaxErrorMessage(node, 'Parenthesis may only contain one expression')
);
}
return transformNode(expressions[0]);
},
};
function parse(source, reviver) {
if (reviver == null) {
reviver = defaultReviver;
}
function transformNode(csNode) {
const type = nodeTypeString(csNode);
const transform = nodeTransforms[type];
if (!transform) {
throw new SyntaxError(syntaxErrorMessage(csNode, `Unexpected ${type}`));
}
return transform(csNode, transformNode, reviver);
}
if (typeof reviver !== 'function') {
throw new TypeError('reviver has to be a function');
}
const coffeeAst = nodes(source.toString('utf8'));
const parsed = transformNode(coffeeAst);
if (reviver === defaultReviver) {
return parsed;
}
const contextObj = {};
contextObj[''] = parsed;
return reviver.call(contextObj, '', parsed);
}
module.exports = parse;