diff --git a/test/unit/autolinker_spec.js b/test/unit/autolinker_spec.js index 5348ab1eb..4ea9a5b64 100644 --- a/test/unit/autolinker_spec.js +++ b/test/unit/autolinker_spec.js @@ -220,4 +220,24 @@ describe("autolinker", function () { ["john.doe@uni-cityname.tld", "mailto:john.doe@uni-cityname.tld"], ]); }); + + it("should find emails with the longest parts allowed by the RFCs", function () { + const local = "a".repeat(64); + const label = "b".repeat(63); + testLinks([[`${local}@${label}.com`, `mailto:${local}@${label}.com`]]); + }); + + it("shouldn't find emails with parts longer than allowed by the RFCs", function () { + expect( + Autolinker.findLinks(`${"a".repeat(107)}@${"a".repeat(80)}.com`) + ).toEqual([]); + }); + + it("should handle a long run of characters before an @ efficiently", function () { + const text = `${"a".repeat(50000)}@`; + + const startTime = performance.now(); + expect(Autolinker.findLinks(text)).toEqual([]); + expect(performance.now() - startTime).toBeLessThan(1000); + }); }); diff --git a/web/autolinker.js b/web/autolinker.js index 407025fb2..e51288557 100644 --- a/web/autolinker.js +++ b/web/autolinker.js @@ -136,10 +136,13 @@ class Autolinker { static #numericTLDRegex; static findLinks(text) { - // Regex can be tested and verified at https://regex101.com/r/rXoLiT/2. + // Regex can be tested and verified at https://regex101.com/r/riHjvK/1. + // The email parts are bounded to keep the scan linear: a local part can't + // exceed 64 characters (RFC 5321, section 4.5.3.1.1) and a domain label + // can't exceed 63 (RFC 1035, section 2.3.4). this.#regex ??= // eslint-disable-next-line regexp/no-super-linear-backtracking - /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]+@([\S--[[\p{P}--\-]<>]]+(?:\.[\S--[[\p{P}--\-]<>]]+)+)/gv; + /\b(?:https?:\/\/|mailto:|www\.)(?:[\S--[\p{P}<>]]|\/|[\S--[\[\]]]+[\S--[\p{P}<>]])+|(?=\p{L})[\S--[@\p{Ps}\p{Pe}<>]]{1,64}@([\S--[[\p{P}--\-]<>]]{1,63}(?:\.[\S--[[\p{P}--\-]<>]]{1,63})+)/gv; const [normalizedText, diffs] = normalize(text, { ignoreDashEOL: true }); const matches = normalizedText.matchAll(this.#regex);