Skip to content

Commit

Permalink
fix: quote and backslash escaping (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Hambourger authored and piotr-oles committed Jun 29, 2022
1 parent 2ea21bb commit 61c109d
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 55 deletions.
36 changes: 9 additions & 27 deletions packages/emitter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,18 @@ import {
ValueNode,
} from "@rsql/ast";

function escapeQuotes(value: string, quote: string) {
let escapedValue = value;
let previousPosition = 0;
let currentPosition = value.indexOf(quote);

while (currentPosition !== -1) {
// scan back for escape characters
let escaped = false;
for (
let scanPosition = currentPosition - 1;
escapedValue[scanPosition] === "\\" && scanPosition > previousPosition;
scanPosition--
) {
escaped = !escaped;
}

// if it's not escaped - add backslash
if (!escaped) {
escapedValue = escapedValue.slice(0, currentPosition - 1) + "\\" + escapedValue.slice(currentPosition);
}
type Quote = '"' | "'";

// move position forward
previousPosition = currentPosition;
currentPosition = value.indexOf(quote, previousPosition + 1);
}
const NEEDS_ESCAPING: { [Q in Quote]: RegExp } = {
'"': /"|\\/g,
"'": /'|\\/g,
};

return escapedValue;
function escapeQuotes(value: string, quote: Quote) {
return value.replace(NEEDS_ESCAPING[quote], "\\$&");
}

function escapeValue(value: string, quote: '"' | "'" = '"') {
function escapeValue(value: string, quote: Quote) {
if (value === "") {
return quote + quote;
}
Expand All @@ -59,7 +41,7 @@ function emitSelector(node: SelectorNode) {
return node.selector;
}

function emitValue(node: ValueNode, quote: '"' | "'" = '"') {
function emitValue(node: ValueNode, quote: Quote = '"') {
return Array.isArray(node.value)
? `(${node.value.map((value) => escapeValue(value, quote)).join(",")})`
: escapeValue(node.value, quote);
Expand Down
12 changes: 7 additions & 5 deletions packages/parser/src/ParserProduction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const selectorProduction: ParserProduction = (stack) => {

const singleValueProduction: ParserProduction = (stack) => {
const token = stack[stack.length - 1] as UnquotedToken | QuotedToken;
const value = isQuotedToken(token) ? token.value.slice(1, -1) : token.value;
const value = resolveValueTokenValue(token);

return {
consumed: 1,
Expand All @@ -51,13 +51,15 @@ const multiValueProduction: ParserProduction = (stack) => {

return {
consumed: closeParenthesisIndex - openParenthesisIndex + 1,
produced: createValueNode(
valueTokens.map((valueToken) => (isQuotedToken(valueToken) ? valueToken.value.slice(1, -1) : valueToken.value)),
true
),
produced: createValueNode(valueTokens.map(resolveValueTokenValue), true),
};
};

const ESCAPE_SEQUENCE = /\\([\s\S])/g;

const resolveValueTokenValue = (valueToken: UnquotedToken | QuotedToken) =>
isQuotedToken(valueToken) ? valueToken.value.slice(1, -1).replace(ESCAPE_SEQUENCE, "$1") : valueToken.value;

const comparisonExpressionProduction: ParserProduction = (stack) => {
const selector = stack[stack.length - 3] as SelectorNode;
const operator = stack[stack.length - 2] as OperatorToken;
Expand Down
29 changes: 18 additions & 11 deletions tests/emitter/emit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import builder from "@rsql/builder";
import { parse } from "@rsql/parser";
import { emit } from "@rsql/emitter";

Expand Down Expand Up @@ -33,17 +34,23 @@ describe("emit", () => {
expect(emittedRsql).toEqual(rsql);
});

it.each(['"hi there!"', '"hi \\"there!"', "'Pěkný den!'", '"Flynn\'s *"', "\"o)'O'(o\"", '"6*7=42"'])(
'emits quoted value with any chars "%p"',
(value) => {
const rsql = `selector==${value}`;
const ast = parse(rsql);
const emittedRsql = emit(ast);
const expectedRsql = `selector=="${value.slice(1, -1)}"`;
it.each([
["hi there!", '"hi there!"'],
['hi "there\\!', '"hi \\"there\\\\!"'],
["Pěkný den!", '"Pěkný den!"'],
["Flynn's *", '"Flynn\'s *"'],
["o)'O'(o", "\"o)'O'(o\""],
["6*7=42", '"6*7=42"'],
])('emits quoted value with any chars "%p"', (value, escapedValue) => {
const ast = builder.comparison("selector", "==", value);
const emittedRsql = emit(ast);
const expectedRsql = `selector==${escapedValue}`;

expect(emittedRsql).toEqual(expectedRsql);
}
);
expect(emittedRsql).toEqual(expectedRsql);

const parsedAst = parse(emittedRsql);
expect(parsedAst).toEqual(ast);
});

test('Empty string will be emitted as ""', () => {
const rsql = `selector==""`;
Expand All @@ -52,7 +59,7 @@ describe("emit", () => {
const expectedRsql = `selector==""`;

expect(emittedRsql).toEqual(expectedRsql);
})
});

it.each([
["(s0==a0,s1==a1);s2==a2", "(s0==a0,s1==a1);s2==a2"],
Expand Down
32 changes: 20 additions & 12 deletions tests/parser/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,21 @@ describe("parse", () => {
);

it.each([
"'10\\' 15\"'",
"'10\\' 15\\\"'",
"'w\\\\ \\'Flyn\\n\\''",
"'\\\\(^_^)/'",
"'\\\\'",
'"10\' 15\\""',
'"10\\\' 15\\""',
'"w\\\\ \\"Flyn\\n\\""',
'"\\\\(^_^)/"',
'"\\\\"',
])('parses escaped quoted value "%p"', (value) => {
["'10\\' 15\"'", "10' 15\""],
["'10\\' 15\\\"'", "10' 15\""],
["'10\\'\\\n15\\\"'", "10'\n15\""],
["'w\\\\ \\'Flyn\\n\\''", "w\\ 'Flynn'"],
["'\\\\(^_^)/'", "\\(^_^)/"],
["'\\\\'", "\\"],
["'\\\u2081\\''", "\u2081'"],
['"10\' 15\\""', "10' 15\""],
['"10\\\' 15\\""', "10' 15\""],
['"10\\\'\\\n15\\""', "10'\n15\""],
['"w\\\\ \\"Flyn\\n\\""', 'w\\ "Flynn"'],
['"\\\\(^_^)/"', "\\(^_^)/"],
['"\\\\"', "\\"],
['"\\\u2081\\\'"', "\u2081'"],
])('parses escaped quoted value "%p"', (value, unescapedValue) => {
const rsql = `selector==${value}`;
const comparison = parse(rsql);

Expand All @@ -173,7 +177,7 @@ describe("parse", () => {

expect(comparison.operator).toEqual("==");
expect(comparison.left.selector).toEqual("selector");
expect(comparison.right.value).toEqual(value.slice(1, -1));
expect(comparison.right.value).toEqual(unescapedValue);
});

it.each([
Expand All @@ -187,6 +191,10 @@ describe("parse", () => {
],
[["meh"], ["meh"]],
[['")o("'], [")o("]],
[
['"So I said, \\"We need more commas!\\""', '",,,,,,,"'],
['So I said, "We need more commas!"', ",,,,,,,"],
],
])('parses values group "%p"', (values, expectedValues) => {
const rsql = `selector=in=(${values.join(",")})`;
const comparison = parse(rsql);
Expand Down

0 comments on commit 61c109d

Please sign in to comment.