From 2e049bb3deecc7f1b3eea02efb1fbf5ab7739780 Mon Sep 17 00:00:00 2001 From: Luigi Gubello <27034386+luigigubello@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:21:06 +0000 Subject: [PATCH] chore: Remove xregexp from the FTP client (#40676) Co-authored-by: Claude Opus 5.5 Co-authored-by: RomanDavydchuk --- .../nodes/Ftp/__test__/listParser.test.ts | 51 ++++++++++++++ patches/@icetee__ftp@0.3.15.patch | 69 +++++++++++++++++++ pnpm-lock.yaml | 13 ++-- pnpm-workspace.yaml | 3 + 4 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 packages/nodes-base/nodes/Ftp/__test__/listParser.test.ts create mode 100644 patches/@icetee__ftp@0.3.15.patch diff --git a/packages/nodes-base/nodes/Ftp/__test__/listParser.test.ts b/packages/nodes-base/nodes/Ftp/__test__/listParser.test.ts new file mode 100644 index 00000000000..d51413aaff2 --- /dev/null +++ b/packages/nodes-base/nodes/Ftp/__test__/listParser.test.ts @@ -0,0 +1,51 @@ +import { createRequire } from 'node:module'; + +// promise-ftp parses LIST output with @icetee/ftp. A pnpm patch makes that parser use native RegExp. +const requireFromPromiseFtp = createRequire(createRequire(__filename).resolve('promise-ftp')); +const { parseListEntry } = requireFromPromiseFtp('@icetee/ftp/lib/parser') as { + parseListEntry: (line: string) => unknown; +}; + +describe('FTP LIST parser', () => { + it('parses a Unix file entry', () => { + expect( + parseListEntry('-rw-r--r--+ 1 ftp ftp 12345678 Mar 3 2021 archive 2021.tar.gz'), + ).toEqual({ + type: '-', + name: 'archive 2021.tar.gz', + target: undefined, + sticky: false, + rights: { user: 'rw', group: 'r', other: 'r' }, + acl: true, + owner: 'ftp', + group: 'ftp', + size: 12345678, + date: new Date('2021-03-03'), + }); + }); + + it('parses a Unix symbolic link with a sticky bit', () => { + expect( + parseListEntry('lrwxrwxrwt 1 root root 7 Feb 1 2024 latest -> v2.1.0'), + ).toMatchObject({ + type: 'l', + name: 'latest', + target: 'v2.1.0', + sticky: true, + rights: { user: 'rwx', group: 'rwx', other: 'rwx' }, + }); + }); + + it('parses an MS-DOS directory entry', () => { + expect(parseListEntry('10-07-26 02:15PM Reports')).toEqual({ + name: 'Reports', + type: 'd', + size: 0, + date: new Date(2026, 9, 7, 14, 15), + }); + }); + + it('returns a line that it cannot parse unchanged', () => { + expect(parseListEntry('not a listing line')).toBe('not a listing line'); + }); +}); diff --git a/patches/@icetee__ftp@0.3.15.patch b/patches/@icetee__ftp@0.3.15.patch new file mode 100644 index 00000000000..94d419de3d4 --- /dev/null +++ b/patches/@icetee__ftp@0.3.15.patch @@ -0,0 +1,69 @@ +diff --git a/lib/connection.js b/lib/connection.js +index 0afc364e7fcf955b2d373ede71d77acdeddeabf7..807542043645cb0b85db0610731f481b3c60c524 100644 +--- a/lib/connection.js ++++ b/lib/connection.js +@@ -8,9 +8,13 @@ var fs = require('fs'), + StringDecoder = require('string_decoder').StringDecoder; + + var Parser = require('./parser'); +-var XRegExp = require('xregexp').XRegExp; ++// Run a regex with named groups and return the groups, or null when it does not match. ++function execNamed(re, str) { ++ var m = re.exec(str); ++ return m ? m.groups : null; ++} + +-var REX_TIMEVAL = XRegExp.cache('^(?\\d{4})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d+)(?:.\\d+)?$'), ++var REX_TIMEVAL = new RegExp('^(?\\d{4})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d+)(?:.\\d+)?$'), + RE_PASV = /([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/, + RE_EPSV = /([\d]+)/, + RE_EOL = /\r?\n/g, +@@ -865,7 +869,7 @@ FTP.prototype.lastMod = function(path, cb) { + }, true); + } else if (err) + return cb(err); +- var val = XRegExp.exec(text, REX_TIMEVAL), ret; ++ var val = execNamed(REX_TIMEVAL, text), ret; + if (!val) + return cb(new Error('Invalid date/time format from server')); + ret = new Date(val.year + '-' + val.month + '-' + val.date + 'T' + val.hour +diff --git a/lib/parser.js b/lib/parser.js +index 3eae00a4ded905c9aef34c5cda743ec63b18b521..2505bc7a17445052f9deda395f90514cf3405f79 100644 +--- a/lib/parser.js ++++ b/lib/parser.js +@@ -3,10 +3,14 @@ var WritableStream = require('stream').Writable + inherits = require('util').inherits, + inspect = require('util').inspect; + +-var XRegExp = require('xregexp').XRegExp; ++// Run a regex with named groups and return the groups, or null when it does not match. ++function execNamed(re, str) { ++ var m = re.exec(str); ++ return m ? m.groups : null; ++} + +-var REX_LISTUNIX = XRegExp.cache('^(?[\\-ld])(?([\\-r][\\-w][\\-xstT]){3})(?(\\+))?\\s+(?\\d+)\\s+(?\\S+)\\s+(?\\S+)\\s+(?\\d+)\\s+(?((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{1,2}):(?\\d{2}))|((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{4})))\\s+(?.+)$'), +- REX_LISTMSDOS = XRegExp.cache('^(?\\d{2})(?:\\-|\\/)(?\\d{2})(?:\\-|\\/)(?\\d{2,4})\\s+(?\\d{2}):(?\\d{2})\\s{0,1}(?[AaMmPp]{1,2})\\s+(?:(?\\d+)|(?\\))\\s+(?.+)$'), ++var REX_LISTUNIX = new RegExp('^(?[\\-ld])(?([\\-r][\\-w][\\-xstT]){3})(?(\\+))?\\s+(?\\d+)\\s+(?\\S+)\\s+(?\\S+)\\s+(?\\d+)\\s+(?((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{1,2}):(?\\d{2}))|((?\\w{3})\\s+(?\\d{1,2})\\s+(?\\d{4})))\\s+(?.+)$'), ++ REX_LISTMSDOS = new RegExp('^(?\\d{2})(?:\\-|\\/)(?\\d{2})(?:\\-|\\/)(?\\d{2,4})\\s+(?\\d{2}):(?\\d{2})\\s{0,1}(?[AaMmPp]{1,2})\\s+(?:(?\\d+)|(?\\))\\s+(?.+)$'), + RE_ENTRY_TOTAL = /^total/, + RE_RES_END = /(?:^|\r?\n)(\d{3}) [^\r\n]*\r?\n/, + RE_EOL = /\r?\n/g, +@@ -82,7 +86,7 @@ Parser.parseListEntry = function(line) { + month, day, year, + hour, mins; + +- if (ret = XRegExp.exec(line, REX_LISTUNIX)) { ++ if (ret = execNamed(REX_LISTUNIX, line)) { + info = { + type: ret.type, + name: undefined, +@@ -182,7 +186,7 @@ Parser.parseListEntry = function(line) { + } else + info.name = ret.name; + ret = info; +- } else if (ret = XRegExp.exec(line, REX_LISTMSDOS)) { ++ } else if (ret = execNamed(REX_LISTMSDOS, line)) { + info = { + name: ret.name, + type: (ret.isdir ? 'd' : '-'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a032984fe4b..2da832adaf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1061,11 +1061,13 @@ overrides: source-map-js: 1.2.2 '@grpc/grpc-js@<1.14.5': 1.14.5 prosemirror-view@<1.42.3: 1.42.3 + '@icetee/ftp>xregexp': npm:empty-npm-package@1.0.0 patchedDependencies: '@chat-adapter/teams@4.37.0': af5fb1d38a2af55399bd8b6934e77348a9b061fec6177c101dd9d7a787ae8a6c '@chat-adapter/whatsapp@4.37.0': 950356dafb986b5cc9bd53069f7653b47b790a60b3590495cf53392ea4df2de4 '@confluentinc/kafka-javascript@1.9.1': c597d3e3d9d6d0d33588b7dc1e88d55f270ba155721aad464bd7fb03e28f5003 + '@icetee/ftp@0.3.15': 19c547038e6400d80f724f77c3fad97093f9233d7596bf2dba38d3e0adb7dab9 '@langchain/google-common@2.1.24': b50436a6c7b0a82d54472dff331ba1b42ab8b816995f83295eef2ee4b482a87d '@langchain/openai@1.4.4': e16bcc93cf9037198faacf494fb81afba6e9abba749dc9a09c3abfb33e68d5c2 '@lezer/highlight': 97f85e6fe46f23015ea0dd420e33d584bc2dc71633910cf131321da31b27ca8c @@ -25014,9 +25016,6 @@ packages: resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} engines: {node: '>=0.6.0'} - xregexp@2.0.0: - resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} - xss@1.0.15: resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} engines: {node: '>= 0.10.0'} @@ -28596,10 +28595,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@icetee/ftp@0.3.15': + '@icetee/ftp@0.3.15(patch_hash=19c547038e6400d80f724f77c3fad97093f9233d7596bf2dba38d3e0adb7dab9)': dependencies: readable-stream: 1.1.14 - xregexp: 2.0.0 + xregexp: empty-npm-package@1.0.0 '@iconify-json/lucide@1.2.114': dependencies: @@ -41831,7 +41830,7 @@ snapshots: promise-ftp@1.3.5(promise-ftp-common@1.1.5): dependencies: - '@icetee/ftp': 0.3.15 + '@icetee/ftp': 0.3.15(patch_hash=19c547038e6400d80f724f77c3fad97093f9233d7596bf2dba38d3e0adb7dab9) bluebird: 2.11.0 promise-ftp-common: 1.1.5 @@ -45407,8 +45406,6 @@ snapshots: xpath@0.0.34: {} - xregexp@2.0.0: {} - xss@1.0.15: dependencies: commander: 2.20.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7acdb1aafe3..2664695d5d0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -484,6 +484,8 @@ overrides: source-map-js: 1.2.2 '@grpc/grpc-js@<1.14.5': 1.14.5 'prosemirror-view@<1.42.3': 1.42.3 + # The @icetee/ftp patch replaces xregexp with native RegExp. + '@icetee/ftp>xregexp': npm:empty-npm-package@1.0.0 patchedDependencies: bull@4.16.4: patches/bull@4.16.4.patch @@ -508,6 +510,7 @@ patchedDependencies: imapflow@1.7.1: patches/imapflow@1.7.1.patch '@chat-adapter/teams@4.37.0': patches/@chat-adapter__teams@4.37.0.patch '@chat-adapter/whatsapp@4.37.0': patches/@chat-adapter__whatsapp@4.37.0.patch + '@icetee/ftp@0.3.15': patches/@icetee__ftp@0.3.15.patch allowBuilds: '@biomejs/biome': false