From 9c2da469663f79bea400d419a9aae22f79a5f270 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 2 Oct 2024 12:48:53 -0700 Subject: [PATCH] filter: Fix unparse to round-trip Unicode whitespace operands. Previously [{operator: "topic", operand: "one\xa0two"}] would be unparsed to "topic:one\xa0two" which parses as [{operator: "topic", operand: "one"}, {operator: "search", operand: "two"}], leading to exceptions in the search pill system. Signed-off-by: Anders Kaseorg --- web/src/filter.ts | 19 ++++++++++--------- web/tests/filter.test.js | 8 ++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/web/src/filter.ts b/web/src/filter.ts index cc5a960159..718c1a1550 100644 --- a/web/src/filter.ts +++ b/web/src/filter.ts @@ -448,20 +448,19 @@ export class Filter { This is just for the search bar, not for saving the narrow in the URL fragment. There we do use full URI encoding to avoid problematic characters. */ - static encodeOperand(operand: string): string { - return operand - .replaceAll("%", "%25") - .replaceAll("+", "%2B") - .replaceAll(" ", "+") - .replaceAll('"', "%22"); + static encodeOperand(operand: string, operator: string): string { + if (USER_OPERATORS.has(operator)) { + return operand.replaceAll(/[\s"%]/g, (c) => encodeURIComponent(c)); + } + return operand.replaceAll(/[\s"%+]/g, (c) => (c === " " ? "+" : encodeURIComponent(c))); } static decodeOperand(encoded: string, operator: string): string { - encoded = encoded.replaceAll('"', ""); + encoded = encoded.trim().replaceAll('"', ""); if (!USER_OPERATORS.has(operator)) { encoded = encoded.replaceAll("+", " "); } - return util.robust_url_decode(encoded).trim(); + return util.robust_url_decode(encoded); } // Parse a string into a list of terms (see below). @@ -618,7 +617,9 @@ export class Filter { return term.operand; } const operator = Filter.canonicalize_operator(term.operator); - return sign + operator + ":" + Filter.encodeOperand(term.operand.toString()); + return ( + sign + operator + ":" + Filter.encodeOperand(term.operand.toString(), term.operator) + ); }); return term_strings.join(" "); } diff --git a/web/tests/filter.test.js b/web/tests/filter.test.js index f7e80192fc..f11dda06d2 100644 --- a/web/tests/filter.test.js +++ b/web/tests/filter.test.js @@ -1461,6 +1461,14 @@ test("unparse", () => { ]; string = `channel:${foo_stream_id} topic:Bar`; assert.deepEqual(Filter.unparse(terms), string); + + terms = [ + {operator: "dm", operand: '\t "%+.\u00A0'}, + {operator: "topic", operand: '\t "%+.\u00A0'}, + ]; + string = "dm:%09%20%22%25+.%C2%A0 topic:%09+%22%25%2B.%C2%A0"; + assert.equal(Filter.unparse(terms), string); + assert_same_terms(Filter.parse(string), terms); }); test("describe", ({mock_template}) => {