Files
sdk/runtime/bin/uri.cc
Daco Harkes 592e5c144a [native assets] Support PATHEXT invocations with appended snapshot
This CL introduces an `IsolateData::asset_base_path` which contains
the fully resolved base path used for assets which are loaded with a
relative path. (Resolving both PATHEXT and symlinks.)

The asset base path is now only resolved once, instead of on every
relative asset load.

The asset base path is now eagerly resolved.
To prevent existing error messages for files not existing changing in
`dartdev` and `Isolate.spawnUri`, files not existing errors are
ignored.
To prevent `tests/standalone/io/named_pipe_script_test.dart` from
failing, failing to canonicalize a file path is treated as no asset
base path rather than reporting a failure.

TEST=pkg/dartdev/test/native_assets/*.dart

Bug: https://github.com/dart-lang/sdk/issues/60946
Change-Id: I79a9144d220dfb3e54311725d41f8f91c88db3a1
Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-linux-release-try,pkg-mac-release-try,pkg-mac-release-arm64-try,pkg-win-release-arm64-try,pkg-win-release-try,vm-aot-dyn-linux-debug-x64-try,vm-aot-android-release-arm64c-try,vm-aot-linux-debug-x64-try,vm-aot-mac-debug-arm64-try,vm-aot-win-debug-arm64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-msan-linux-release-x64-try,vm-asan-linux-release-x64-try,vm-linux-release-x64-try,vm-linux-debug-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/435421
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
2025-06-26 07:47:32 -07:00

531 lines
16 KiB
C++

// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "bin/uri.h"
#include <memory>
#include <utility>
#include "platform/allocation.h"
#include "platform/utils.h"
namespace dart {
static CStringUniquePtr MakeCopyOfString(const char* str) {
if (str == nullptr) {
return CStringUniquePtr();
}
intptr_t len = strlen(str) + 1; // '\0'-terminated.
char* copy = static_cast<char*>(malloc(len));
strncpy(copy, str, len);
return CStringUniquePtr(copy);
}
static CStringUniquePtr MakeCopyOfStringN(const char* str, intptr_t len) {
ASSERT(len >= 0);
for (intptr_t i = 0; i < len; i++) {
if (str[i] == '\0') {
len = i;
break;
}
}
char* copy = static_cast<char*>(malloc(len + 1)); // +1 for '\0'
strncpy(copy, str, len);
copy[len] = '\0';
return CStringUniquePtr(copy);
}
static CStringUniquePtr PrintToString(const char* format, ...) {
va_list args;
va_start(args, format);
char* buffer = Utils::VSCreate(format, args);
va_end(args);
return CStringUniquePtr(buffer);
}
static bool IsUnreservedChar(intptr_t value) {
return ((value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') ||
(value >= '0' && value <= '9') || value == '-' || value == '.' ||
value == '_' || value == '~');
}
static bool IsDelimiter(intptr_t value) {
switch (value) {
case ':':
case '/':
case '?':
case '#':
case '[':
case ']':
case '@':
case '!':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case ';':
case '=':
return true;
default:
return false;
}
}
static bool IsHexDigit(char value) {
return ((value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') ||
(value >= 'a' && value <= 'f'));
}
static int HexValue(char digit) {
if ((digit >= '0' && digit <= '9')) {
return digit - '0';
}
if ((digit >= 'A' && digit <= 'F')) {
return digit - 'A' + 10;
}
if ((digit >= 'a' && digit <= 'f')) {
return digit - 'a' + 10;
}
UNREACHABLE();
return 0;
}
static int GetEscapedValue(const char* str, intptr_t pos, intptr_t len) {
if (pos + 2 >= len) {
// Not enough room for a valid escape sequence.
return -1;
}
if (str[pos] != '%') {
// Escape sequences start with '%'.
return -1;
}
char digit1 = str[pos + 1];
char digit2 = str[pos + 2];
if (!IsHexDigit(digit1) || !IsHexDigit(digit2)) {
// Invalid escape sequence. Ignore it.
return -1;
}
return HexValue(digit1) * 16 + HexValue(digit2);
}
CStringUniquePtr NormalizeEscapes(const char* str, intptr_t len) {
// Allocate the buffer.
// We multiply len by three because a percent-escape sequence is
// three characters long (e.g. ' ' -> '%20). +1 for '\0'. We could
// take two passes through the string and avoid the excess
// allocation, but it's zone-memory so it doesn't seem necessary.
char* buffer = static_cast<char*>(malloc(len * 3 + 1));
// Copy the string, normalizing as we go.
intptr_t buffer_pos = 0;
intptr_t pos = 0;
while (pos < len) {
int escaped_value = GetEscapedValue(str, pos, len);
if (escaped_value >= 0) {
// If one of the special "unreserved" characters has been
// escaped, revert the escaping. Otherwise preserve the
// escaping.
if (IsUnreservedChar(escaped_value)) {
buffer[buffer_pos] = escaped_value;
buffer_pos++;
} else {
Utils::SNPrint(buffer + buffer_pos, 4, "%%%02X", escaped_value);
buffer_pos += 3;
}
pos += 3;
} else {
char c = str[pos];
// If a delimiter or unreserved character is currently not
// escaped, preserve that. If there is a busted %-sequence in
// the input, preserve that too.
if (c == '%' || IsDelimiter(c) || IsUnreservedChar(c)) {
buffer[buffer_pos] = c;
buffer_pos++;
} else {
// Escape funky characters.
Utils::SNPrint(buffer + buffer_pos, 4, "%%%02X", c);
buffer_pos += 3;
}
pos++;
}
}
buffer[buffer_pos] = '\0';
return CStringUniquePtr(buffer);
}
// Lower-case a string in place.
static void StringLower(char* str) {
const intptr_t len = strlen(str);
intptr_t i = 0;
while (i < len) {
int escaped_value = GetEscapedValue(str, i, len);
if (escaped_value >= 0) {
// Don't lowercase escape sequences.
i += 3;
} else {
// I don't use tolower() because I don't want the locale
// transforming any non-ascii characters.
char c = str[i];
if (c >= 'A' && c <= 'Z') {
str[i] = c + ('a' - 'A');
}
i++;
}
}
}
static intptr_t ParseAuthority(const char* authority, ParsedUri& parsed_uri) {
const char* current = authority;
intptr_t len = 0;
size_t userinfo_len = strcspn(current, "@/");
if (current[userinfo_len] == '@') {
// The '@' character follows the optional userinfo string.
parsed_uri.userinfo = NormalizeEscapes(current, userinfo_len);
current += userinfo_len + 1;
len += userinfo_len + 1;
}
size_t host_len = strcspn(current, ":/");
CStringUniquePtr host = NormalizeEscapes(current, host_len);
StringLower(host.get());
parsed_uri.host = std::move(host);
len += host_len;
if (current[host_len] == ':') {
// The ':' character precedes the optional port string.
const char* port_start = current + host_len + 1; // +1 for ':'
size_t port_len = strcspn(port_start, "/");
parsed_uri.port = MakeCopyOfStringN(port_start, port_len);
len += 1 + port_len; // +1 for ':'
}
return len;
}
// Performs a simple parse of a uri into its components.
// See RFC 3986 Section 3: Syntax.
std::unique_ptr<ParsedUri> ParseUri(const char* uri) {
auto parsed_uri = std::make_unique<ParsedUri>();
// The first ':' separates the scheme from the rest of the uri. If
// a ':' occurs after the first '/' it doesn't count.
size_t scheme_len = strcspn(uri, ":/");
const char* rest = uri;
if (uri[scheme_len] == ':') {
CStringUniquePtr scheme = MakeCopyOfStringN(uri, scheme_len);
StringLower(scheme.get());
parsed_uri->scheme = std::move(scheme);
rest = uri + scheme_len + 1;
}
// The first '#' separates the optional fragment
const char* hash_pos = rest + strcspn(rest, "#");
if (*hash_pos == '#') {
// There is a fragment part.
const char* fragment_start = hash_pos + 1;
parsed_uri->fragment =
NormalizeEscapes(fragment_start, strlen(fragment_start));
}
// The first '?' or '#' separates the hierarchical part from the
// optional query.
const char* question_pos = rest + strcspn(rest, "?#");
if (*question_pos == '?') {
// There is a query part.
const char* query_start = question_pos + 1;
parsed_uri->query = NormalizeEscapes(query_start, (hash_pos - query_start));
}
const char* path_start = rest;
if (rest[0] == '/' && rest[1] == '/') {
// There is an authority part.
const char* authority_start = rest + 2; // 2 for '//'.
intptr_t authority_len = ParseAuthority(authority_start, *parsed_uri.get());
if (authority_len < 0) {
return std::unique_ptr<ParsedUri>();
}
path_start = authority_start + authority_len;
}
// The path is the substring between the authority and the query.
parsed_uri->path = NormalizeEscapes(path_start, (question_pos - path_start));
return parsed_uri;
}
static char* RemoveLastSegment(char* current, char* base) {
if (current == base) {
return current;
}
ASSERT(current > base);
for (current--; current > base; current--) {
if (*current == '/') {
// We have found the beginning of the last segment.
return current;
}
}
ASSERT(current == base);
return current;
}
static intptr_t SegmentLength(const char* input) {
const char* cp = input;
// Include initial slash in the segment, if any.
if (*cp == '/') {
cp++;
}
// Don't include trailing slash in the segment.
cp += strcspn(cp, "/");
return cp - input;
}
// See RFC 3986 Section 5.2.4: Remove Dot Segments.
CStringUniquePtr RemoveDotSegments(const char* path) {
const char* input = path;
// The output path will always be less than or equal to the size of
// the input path.
char* buffer = static_cast<char*>(malloc(strlen(path) + 1)); // +1 for '\0'
char* output = buffer;
while (*input != '\0') {
if (strncmp("../", input, 3) == 0) {
// Discard initial "../" from the input. It's junk.
input += 3;
} else if (strncmp("./", input, 3) == 0) {
// Discard initial "./" from the input. It's junk.
input += 2;
} else if (strncmp("/./", input, 3) == 0) {
// Advance past the "/." part of the input.
input += 2;
} else if (strcmp("/.", input) == 0) {
// Pretend the input just contains a "/".
input = "/";
} else if (strncmp("/../", input, 4) == 0) {
// Advance past the "/.." part of the input and remove one
// segment from the output.
input += 3;
output = RemoveLastSegment(output, buffer);
} else if (strcmp("/..", input) == 0) {
// Pretend the input contains a "/" and remove one segment from
// the output.
input = "/";
output = RemoveLastSegment(output, buffer);
} else if (strcmp("..", input) == 0) {
// The input has been reduced to nothing useful.
input += 2;
} else if (strcmp(".", input) == 0) {
// The input has been reduced to nothing useful.
input += 1;
} else {
intptr_t segment_len = SegmentLength(input);
if (input[0] != '/' && output != buffer) {
*output = '/';
output++;
}
strncpy(output, input, segment_len);
output += segment_len;
input += segment_len;
}
}
*output = '\0';
return CStringUniquePtr(buffer);
}
// See RFC 3986 Section 5.2.3: Merge Paths.
CStringUniquePtr MergePaths(const char* base_path, const char* ref_path) {
if (base_path[0] == '\0') {
// If the base_path is empty, we prepend '/'.
return PrintToString("/%s", ref_path);
}
// We need to find the last '/' in base_path.
const char* last_slash = strrchr(base_path, '/');
if (last_slash == nullptr) {
// There is no slash in the base_path. Return the ref_path unchanged.
return MakeCopyOfString(ref_path);
}
// We found a '/' in the base_path. Cut off everything after it and
// add the ref_path.
intptr_t truncated_base_len = last_slash - base_path;
intptr_t ref_path_len = strlen(ref_path);
intptr_t len = truncated_base_len + ref_path_len + 1; // +1 for '/'
char* buffer = static_cast<char*>(malloc(len + 1)); // +1 for '\0'
// Copy truncated base.
strncpy(buffer, base_path, truncated_base_len);
// Add a slash.
buffer[truncated_base_len] = '/';
// Copy the ref_path.
strncpy((buffer + truncated_base_len + 1), ref_path, ref_path_len + 1);
return CStringUniquePtr(buffer);
}
CStringUniquePtr BuildUri(const ParsedUri& uri) {
ASSERT(uri.path != nullptr);
const char* fragment = uri.fragment == nullptr ? "" : uri.fragment.get();
const char* fragment_separator = uri.fragment == nullptr ? "" : "#";
const char* query = uri.query == nullptr ? "" : uri.query.get();
const char* query_separator = uri.query == nullptr ? "" : "?";
// If there is no scheme for this uri, just build a relative uri of
// the form: "path[?query][#fragment]". This occurs when we resolve
// relative urls inside a "dart:" library.
if (uri.scheme == nullptr) {
ASSERT(uri.userinfo == nullptr && uri.host == nullptr &&
uri.port == nullptr);
return PrintToString("%s%s%s%s%s", uri.path.get(), query_separator, query,
fragment_separator, fragment);
}
// Uri with no authority: "scheme:path[?query][#fragment]"
if (uri.host == nullptr) {
ASSERT(uri.userinfo == nullptr && uri.port == nullptr);
return PrintToString("%s:%s%s%s%s%s", uri.scheme.get(), uri.path.get(),
query_separator, query, fragment_separator, fragment);
}
const char* user = uri.userinfo == nullptr ? "" : uri.userinfo.get();
const char* user_separator = uri.userinfo == nullptr ? "" : "@";
const char* port = uri.port == nullptr ? "" : uri.port.get();
const char* port_separator = uri.port == nullptr ? "" : ":";
// If the path doesn't start with a '/', add one. We need it to
// separate the path from the authority.
const char* path_separator =
((uri.path.get()[0] == '\0' || uri.path.get()[0] == '/') ? "" : "/");
// Uri with authority:
// "scheme://[userinfo@]host[:port][/]path[?query][#fragment]"
return PrintToString(
"%s://%s%s%s%s%s%s%s%s%s%s%s", // There is *nothing* wrong with this.
uri.scheme.get(), user, user_separator, uri.host.get(), port_separator,
port, path_separator, uri.path.get(), query_separator, query,
fragment_separator, fragment);
}
// See RFC 3986 Section 5: Reference Resolution
CStringUniquePtr ResolveUri(const char* ref_uri, const char* base_uri) {
// Parse the reference uri.
std::unique_ptr<ParsedUri> ref = ParseUri(ref_uri);
if (!ref) {
return CStringUniquePtr();
}
ParsedUri target;
if (ref->scheme != nullptr) {
if (strcmp(ref->scheme.get(), "dart") == 0) {
return MakeCopyOfString(ref_uri);
}
// When the ref_uri specifies a scheme, the base_uri is ignored.
target.scheme = std::move(ref->scheme);
target.userinfo = std::move(ref->userinfo);
target.host = std::move(ref->host);
target.port = std::move(ref->port);
target.path = std::move(ref->path);
target.query = std::move(ref->query);
target.fragment = std::move(ref->fragment);
return BuildUri(target);
}
// Parse the base uri.
std::unique_ptr<ParsedUri> base = ParseUri(base_uri);
if (!base) {
return CStringUniquePtr();
}
if ((base->scheme != nullptr) && strcmp(base->scheme.get(), "dart") == 0) {
return MakeCopyOfString(ref_uri);
}
if (ref->host != nullptr) {
// When the ref_uri specifies an authority, we only use the base scheme.
target.scheme = std::move(base->scheme);
target.userinfo = std::move(ref->userinfo);
target.host = std::move(ref->host);
target.port = std::move(ref->port);
target.path = RemoveDotSegments(ref->path.get());
target.query = std::move(ref->query);
target.fragment = std::move(ref->fragment);
return BuildUri(target);
}
if (ref->path.get()[0] == '\0') {
// Empty path. Use most parts of base_uri.
target.scheme = std::move(base->scheme);
target.userinfo = std::move(base->userinfo);
target.host = std::move(base->host);
target.port = std::move(base->port);
target.path = std::move(base->path);
target.query = ((ref->query == nullptr) ? std::move(base->query)
: std::move(ref->query));
target.fragment = std::move(ref->fragment);
return BuildUri(target);
} else if (ref->path.get()[0] == '/') {
// Absolute path. ref_path wins.
target.scheme = std::move(base->scheme);
target.userinfo = std::move(base->userinfo);
target.host = std::move(base->host);
target.port = std::move(base->port);
target.path = RemoveDotSegments(ref->path.get());
target.query = std::move(ref->query);
target.fragment = std::move(ref->fragment);
return BuildUri(target);
} else {
// Relative path. We need to merge the base path and the ref path.
if (base->scheme == nullptr && base->host == nullptr &&
base->path.get()[0] != '/') {
// The dart:core Uri class handles resolving a relative uri
// against a second relative uri specially, in a way not
// described in the RFC. We do not need to support this for
// library resolution. If we need to implement this later, we
// can.
return CStringUniquePtr();
}
target.scheme = std::move(base->scheme);
target.userinfo = std::move(base->userinfo);
target.host = std::move(base->host);
target.port = std::move(base->port);
CStringUniquePtr merged_paths =
MergePaths(base->path.get(), ref->path.get());
target.path = RemoveDotSegments(merged_paths.get());
target.query = std::move(ref->query);
target.fragment = std::move(ref->fragment);
return BuildUri(target);
}
}
CStringUniquePtr ResolvePath(const char* ref_path, const char* base_path) {
CStringUniquePtr merged_paths = MergePaths(base_path, ref_path);
return RemoveDotSegments(merged_paths.get());
}
} // namespace dart