diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index e0ad38e8f..2b70cbe57 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import * as utils from "../test-utils/test-utils"; import { type IActionsObject, PushProcessor } from "../../src/pushprocessor"; import { @@ -1004,3 +1020,25 @@ describe("rewriteDefaultRules", () => { ]); }); }); + +describe("getPushRuleGlobRegex", () => { + it("should not confuse flags in cache", () => { + const pattern = "Test"; + const regex1 = PushProcessor.getPushRuleGlobRegex(pattern, false, "i"); + const regex2 = PushProcessor.getPushRuleGlobRegex(pattern, false, "g"); + const regex3 = PushProcessor.getPushRuleGlobRegex(pattern, false, "i"); + + expect(regex1.flags).toBe("i"); + expect(regex2.flags).toBe("g"); + + expect(regex1).not.toEqual(regex2); + expect(regex1).toEqual(regex3); + }); + + it("should not include word boundary in match", () => { + const pattern = "@room"; + const regex = PushProcessor.getPushRuleGlobRegex(pattern, true); + const input = "Foo @room Bar"; + expect(input.split(regex)).toEqual(["Foo ", "@room", " Bar"]); + }); +}); diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index c0cdb3182..4ba9513e8 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -313,19 +313,21 @@ export class PushProcessor { * No cache invalidation is present currently, * as this will be inherently bounded to the size of the user's own push rules. * @param pattern - the glob pattern to convert to a RegExp - * @param alignToWordBoundary - whether to align the pattern to word boundaries, as specified for `content.body` matches + * @param alignToWordBoundary - whether to align the pattern to word boundaries, + * as specified for `content.body` matches, will use lookaround assertions to ensure the match only includes the pattern * @param flags - the flags to pass to the RegExp constructor, defaults to case-insensitive */ public static getPushRuleGlobRegex(pattern: string, alignToWordBoundary = false, flags = "i"): RegExp { - const [prefix, suffix] = alignToWordBoundary ? ["(^|\\W)", "(\\W|$)"] : ["^", "$"]; + const [prefix, suffix] = alignToWordBoundary ? ["(?<=^|\\W)", "(?=\\W|$)"] : ["^", "$"]; + const cacheKey = `${alignToWordBoundary}-${flags}-${pattern}`; - if (!PushProcessor.cachedGlobToRegex[pattern]) { - PushProcessor.cachedGlobToRegex[pattern] = new RegExp( + if (!PushProcessor.cachedGlobToRegex[cacheKey]) { + PushProcessor.cachedGlobToRegex[cacheKey] = new RegExp( prefix + "(" + globToRegexp(pattern) + ")" + suffix, flags, ); } - return PushProcessor.cachedGlobToRegex[pattern]; + return PushProcessor.cachedGlobToRegex[cacheKey]; } /**