From e3abbc26c051bbd241effd037c7b0afb6902e98e Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Thu, 22 Jun 2023 00:10:48 -0700 Subject: [PATCH] Prevent LogBox from crashing on long messages (#38005) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38005 Fixes https://github.com/facebook/react-native/issues/32093 by guarding the expensive `BABEL_CODE_FRAME_ERROR_FORMAT` regex with a cheaper initial scan. (Longer term, we should reduce our reliance on string parsing and propagate more structured errors.) Changelog: [General][Fixed] Prevent LogBox from crashing on very long messages Reviewed By: GijsWeterings Differential Revision: D46892454 fbshipit-source-id: df0ab3c042c5b9be9c3933f429a06e666a2a673b --- flow-typed/npm/ansi-regex_v5.x.x.js | 14 ++++ .../Libraries/LogBox/Data/parseLogBoxLog.js | 72 +++++++++++++------ packages/react-native/package.json | 1 + 3 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 flow-typed/npm/ansi-regex_v5.x.x.js diff --git a/flow-typed/npm/ansi-regex_v5.x.x.js b/flow-typed/npm/ansi-regex_v5.x.x.js new file mode 100644 index 00000000000000..150902f4a12e6c --- /dev/null +++ b/flow-typed/npm/ansi-regex_v5.x.x.js @@ -0,0 +1,14 @@ +/** + * @flow strict + * @format + */ + +declare module 'ansi-regex' { + declare export type Options = { + /** + * Match only the first ANSI escape. + */ + +onlyFirst?: boolean, + }; + declare export default function ansiRegex(options?: Options): RegExp; +} diff --git a/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js b/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js index b41bade7fd0be5..744fa5e4e367ef 100644 --- a/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js +++ b/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js @@ -14,12 +14,38 @@ import type {LogBoxLogData} from './LogBoxLog'; import parseErrorStack from '../../Core/Devtools/parseErrorStack'; import UTFSequence from '../../UTFSequence'; import stringifySafe from '../../Utilities/stringifySafe'; +import ansiRegex from 'ansi-regex'; + +const ANSI_REGEX = ansiRegex().source; const BABEL_TRANSFORM_ERROR_FORMAT = /^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/; + +// https://github.com/babel/babel/blob/33dbb85e9e9fe36915273080ecc42aee62ed0ade/packages/babel-code-frame/src/index.ts#L183-L184 +const BABEL_CODE_FRAME_MARKER_PATTERN = new RegExp( + [ + // Beginning of a line (per 'm' flag) + '^', + // Optional ANSI escapes for colors + `(?:${ANSI_REGEX})*`, + // Marker + '>', + // Optional ANSI escapes for colors + `(?:${ANSI_REGEX})*`, + // Left padding for line number + ' +', + // Line number + '[0-9]+', + // Gutter + ' \\|', + ].join(''), + 'm', +); + const BABEL_CODE_FRAME_ERROR_FORMAT = // eslint-disable-next-line no-control-regex /^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u; + const METRO_ERROR_FORMAT = /^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u; @@ -243,28 +269,32 @@ export function parseLogBoxException( }; } - const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); + // Perform a cheap match first before trying to parse the full message, which + // can get expensive for arbitrary input. + if (BABEL_CODE_FRAME_MARKER_PATTERN.test(message)) { + const babelCodeFrameError = message.match(BABEL_CODE_FRAME_ERROR_FORMAT); - if (babelCodeFrameError) { - // Codeframe errors are thrown from any use of buildCodeFrameError. - const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); - return { - level: 'syntax', - stack: [], - isComponentError: false, - componentStack: [], - codeFrame: { - fileName, - location: null, // We are not given the location. - content: codeFrame, - }, - message: { - content, - substitutions: [], - }, - category: `${fileName}-${1}-${1}`, - extraData: error.extraData, - }; + if (babelCodeFrameError) { + // Codeframe errors are thrown from any use of buildCodeFrameError. + const [fileName, content, codeFrame] = babelCodeFrameError.slice(1); + return { + level: 'syntax', + stack: [], + isComponentError: false, + componentStack: [], + codeFrame: { + fileName, + location: null, // We are not given the location. + content: codeFrame, + }, + message: { + content, + substitutions: [], + }, + category: `${fileName}-${1}-${1}`, + extraData: error.extraData, + }; + } } if (message.match(/^TransformError /)) { diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 67225d5695f41f..d7fb83ab397cbf 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -105,6 +105,7 @@ "@react-native/virtualized-lists": "^0.73.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", + "ansi-regex": "^5.0.0", "base64-js": "^1.5.1", "deprecated-react-native-prop-types": "4.1.0", "event-target-shim": "^5.0.1",