Skip to content

Commit d83d856

Browse files
motiz88meta-codesync[bot]
authored andcommitted
Fix truncated filenames in call stack frames (#56565)
Summary: Pull Request resolved: #56565 Stack frame file paths in RedBox 2.0 were displaying as truncated jsc-safe-url fragments (e.g. `dev=true` instead of `index.bundle`). This happened because `lastPathComponent` treats the `//&` path-encoded query as additional path segments. Here, we port the `jsc-safe-url` npm package to shared C++ (matching the JS implementation line-for-line, including the RFC 3986 appendix B regex) and use it to normalize stack frame URLs before extracting filenames. Query strings are also stripped after normalization. Changelog: [Internal] Reviewed By: robhogan Differential Revision: D101796395 fbshipit-source-id: 38d5a8ccb111e78f847be4a551e359dd075e6a0f
1 parent 301543c commit d83d856

6 files changed

Lines changed: 449 additions & 3 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
/**
11+
* Converts between standard URLs and JSC-safe URLs.
12+
*
13+
* JSC (JavaScriptCore) strips query strings from source URLs in stack traces
14+
* as of iOS 16.4. Metro works around this by encoding the query string into
15+
* the URL path. These methods convert between the two formats.
16+
*/
17+
@interface RCTJscSafeUrl : NSObject
18+
19+
+ (nonnull NSString *)normalUrlFromJscSafeUrl:(nonnull NSString *)url;
20+
+ (nonnull NSString *)jscSafeUrlFromNormalUrl:(nonnull NSString *)url;
21+
+ (BOOL)isJscSafeUrl:(nonnull NSString *)url;
22+
23+
@end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTJscSafeUrl+Internal.h"
9+
10+
#import <React/RCTDefines.h>
11+
#import <react/debug/redbox/JscSafeUrl.h>
12+
13+
#if RCT_DEV_MENU
14+
15+
using facebook::react::unstable_redbox::isJscSafeUrl;
16+
using facebook::react::unstable_redbox::toJscSafeUrl;
17+
using facebook::react::unstable_redbox::toNormalUrl;
18+
19+
@implementation RCTJscSafeUrl
20+
21+
+ (NSString *)normalUrlFromJscSafeUrl:(NSString *)url
22+
{
23+
return [NSString stringWithUTF8String:toNormalUrl(url.UTF8String).c_str()];
24+
}
25+
26+
+ (NSString *)jscSafeUrlFromNormalUrl:(NSString *)url
27+
{
28+
return [NSString stringWithUTF8String:toJscSafeUrl(url.UTF8String).c_str()];
29+
}
30+
31+
+ (BOOL)isJscSafeUrl:(NSString *)url
32+
{
33+
return isJscSafeUrl(url.UTF8String);
34+
}
35+
36+
@end
37+
38+
#endif

packages/react-native/React/CoreModules/RCTRedBox2Controller.mm

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include <array>
1616

17+
#import "RCTJscSafeUrl+Internal.h"
1718
#import "RCTRedBox2AnsiParser+Internal.h"
1819
#import "RCTRedBox2ErrorParser+Internal.h"
1920
#import "RCTRedBoxHMRClient+Internal.h"
@@ -410,7 +411,13 @@ - (void)copyStack
410411

411412
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
412413
{
413-
NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @"<unknown file>";
414+
NSString *file = [RCTJscSafeUrl normalUrlFromJscSafeUrl:stackFrame.file];
415+
// Strip query string (e.g. ?platform=ios&dev=true) before extracting the filename.
416+
NSRange queryRange = [file rangeOfString:@"?"];
417+
if (queryRange.location != NSNotFound) {
418+
file = [file substringToIndex:queryRange.location];
419+
}
420+
NSString *fileName = RCTNilIfNull(file) ? [file lastPathComponent] : @"<unknown file>";
414421
NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld", fileName, (long long)stackFrame.lineNumber];
415422

416423
if (stackFrame.column != 0) {
@@ -619,8 +626,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forCodeFrame:(RCTRedBox2E
619626
// File name label below the code frame
620627
UILabel *fileLabel = [[UILabel alloc] init];
621628
fileLabel.translatesAutoresizingMaskIntoConstraints = NO;
622-
NSString *fileName = errorData.codeFrameFileName.lastPathComponent ? errorData.codeFrameFileName.lastPathComponent
623-
: errorData.codeFrameFileName;
629+
NSString *fileName = errorData.codeFrameFileName.lastPathComponent ?: errorData.codeFrameFileName;
624630
if (errorData.codeFrameRow > 0) {
625631
fileLabel.text = [NSString
626632
stringWithFormat:@"%@ (%ld:%ld)", fileName, (long)errorData.codeFrameRow, (long)errorData.codeFrameColumn + 1];
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "JscSafeUrl.h"
9+
10+
#include <cassert>
11+
#include <regex> // NOLINT(facebook-hte-BadInclude-regex)
12+
#include <stdexcept>
13+
#include <string_view>
14+
15+
// @lint-ignore-every CLANGTIDY facebook-hte-StdRegexIsAwful
16+
17+
namespace facebook::react::unstable_redbox {
18+
19+
namespace {
20+
21+
// We use regex-based URL parsing as defined in RFC 3986 because it's easier to
22+
// determine whether the input is a complete URI, a path-absolute or a
23+
// path-rootless (as defined in the spec), and be as faithful to the input as
24+
// possible. This will match any string, and does not imply validity.
25+
//
26+
// https://www.rfc-editor.org/rfc/rfc3986#appendix-B
27+
const std::regex& uriRegex() {
28+
static const std::regex re(
29+
R"(^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)");
30+
return re;
31+
}
32+
33+
struct ParsedUri {
34+
std::string_view schemeAndAuthority;
35+
std::string_view path;
36+
bool hasQueryPart = false;
37+
std::string_view queryWithoutQuestionMark;
38+
std::string_view fragmentWithHash;
39+
};
40+
41+
ParsedUri rfc3986Parse(std::string_view url) {
42+
std::cmatch match;
43+
if (!std::regex_match(
44+
url.data(), url.data() + url.size(), match, uriRegex())) {
45+
throw std::runtime_error("Unexpected error - failed to regex-match URL");
46+
}
47+
48+
// match[1] = scheme with colon (e.g. "http:")
49+
// match[3] = authority with slashes (e.g. "//host")
50+
// match[5] = path
51+
// match[6] = query with question mark (e.g. "?key=val")
52+
// match[7] = query without question mark
53+
// match[8] = fragment with hash (e.g. "#frag")
54+
55+
auto viewOf = [&](int group) -> std::string_view {
56+
if (!match[group].matched) {
57+
return {};
58+
}
59+
return {match[group].first, static_cast<size_t>(match[group].length())};
60+
};
61+
62+
// schemeAndAuthority = (match[1] || "") + (match[3] || "")
63+
// These are contiguous when both present, but may be individually absent.
64+
std::string_view schemeAndAuthority;
65+
if (match[1].matched && match[3].matched) {
66+
assert(match[1].second == match[3].first);
67+
schemeAndAuthority = {
68+
match[1].first, static_cast<size_t>(match[3].second - match[1].first)};
69+
} else if (match[1].matched) {
70+
schemeAndAuthority = viewOf(1);
71+
} else if (match[3].matched) {
72+
schemeAndAuthority = viewOf(3);
73+
}
74+
75+
return ParsedUri{
76+
.schemeAndAuthority = schemeAndAuthority,
77+
.path = viewOf(5),
78+
.hasQueryPart = match[6].matched,
79+
.queryWithoutQuestionMark = viewOf(7),
80+
.fragmentWithHash = viewOf(8),
81+
};
82+
}
83+
84+
} // namespace
85+
86+
bool isJscSafeUrl(std::string_view url) {
87+
return !rfc3986Parse(url).hasQueryPart;
88+
}
89+
90+
std::string toNormalUrl(std::string url) {
91+
auto parsed = rfc3986Parse(url);
92+
auto markerPos = parsed.path.find("//&");
93+
if (markerPos == std::string_view::npos) {
94+
return url;
95+
}
96+
97+
// path before //&, then ?, then path after //&
98+
std::string_view pathBefore = parsed.path.substr(0, markerPos);
99+
std::string_view pathAfter = parsed.path.substr(markerPos + 3);
100+
101+
// We don't expect JSC urls to also have query strings, but interpret
102+
// liberally and append them.
103+
bool hasExistingQuery = !parsed.queryWithoutQuestionMark.empty();
104+
105+
// Likewise, JSC URLs will usually have their fragments stripped, but
106+
// preserve if we find one.
107+
size_t totalSize = parsed.schemeAndAuthority.size() + pathBefore.size() +
108+
1 /* ? */ + pathAfter.size() +
109+
(hasExistingQuery ? 1 + parsed.queryWithoutQuestionMark.size() : 0) +
110+
parsed.fragmentWithHash.size();
111+
112+
std::string result;
113+
result.reserve(totalSize);
114+
result += parsed.schemeAndAuthority;
115+
result += pathBefore;
116+
result += '?';
117+
result += pathAfter;
118+
if (hasExistingQuery) {
119+
result += '&';
120+
result += parsed.queryWithoutQuestionMark;
121+
}
122+
result += parsed.fragmentWithHash;
123+
assert(result.size() == totalSize);
124+
return result;
125+
}
126+
127+
std::string toJscSafeUrl(std::string url) {
128+
if (!rfc3986Parse(url).hasQueryPart) {
129+
return url;
130+
}
131+
url = toNormalUrl(std::move(url));
132+
auto parsed = rfc3986Parse(url);
133+
if (!parsed.queryWithoutQuestionMark.empty() &&
134+
(parsed.path.empty() || parsed.path == "/")) {
135+
throw std::invalid_argument(
136+
"The given URL \"" + url +
137+
"\" has an empty path and cannot be converted to a JSC-safe format.");
138+
}
139+
140+
// Query strings may contain '?' (e.g. in key or value names) - these
141+
// must be percent-encoded to form a valid path, and not be stripped.
142+
// Count them first so we can preallocate exactly.
143+
bool hasQuery = !parsed.queryWithoutQuestionMark.empty();
144+
size_t questionMarks = 0;
145+
if (hasQuery) {
146+
for (char c : parsed.queryWithoutQuestionMark) {
147+
if (c == '?') {
148+
questionMarks++;
149+
}
150+
}
151+
}
152+
153+
// Each '?' becomes "%3F" (+2 bytes), plus "//&" delimiter (+3 bytes)
154+
size_t totalSize = parsed.schemeAndAuthority.size() + parsed.path.size() +
155+
(hasQuery ? 3 + parsed.queryWithoutQuestionMark.size() + questionMarks * 2
156+
: 0) +
157+
// We expect JSC to strip this - we don't handle fragments for now.
158+
parsed.fragmentWithHash.size();
159+
160+
std::string result;
161+
result.reserve(totalSize);
162+
result += parsed.schemeAndAuthority;
163+
result += parsed.path;
164+
if (hasQuery) {
165+
result += "//&";
166+
for (char c : parsed.queryWithoutQuestionMark) {
167+
if (c == '?') {
168+
result += "%3F";
169+
} else {
170+
result += c;
171+
}
172+
}
173+
}
174+
result += parsed.fragmentWithHash;
175+
assert(result.size() == totalSize);
176+
return result;
177+
}
178+
179+
} // namespace facebook::react::unstable_redbox
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <string>
11+
#include <string_view>
12+
13+
namespace facebook::react::unstable_redbox {
14+
15+
/**
16+
* These functions are for handling of query-string free URLs, necessitated
17+
* by query string stripping of URLs in JavaScriptCore stack traces
18+
* introduced in iOS 16.4. This is a direct port of https://www.npmjs.com/package/jsc-safe-url.
19+
*
20+
* See https://github.com/facebook/react-native/issues/36794 for context.
21+
*/
22+
23+
bool isJscSafeUrl(std::string_view url);
24+
std::string toNormalUrl(std::string url);
25+
std::string toJscSafeUrl(std::string url);
26+
27+
} // namespace facebook::react::unstable_redbox

0 commit comments

Comments
 (0)