[vm] Native asset path resolution symlinks with spaces

This CL fixes symlink resolution by properly decoding a URI to a file
path before doing symlink resolution (on the file path).
File paths have spaces, but URIs can be encoded with percent encoding.

Before resolving the relative paths, the symlink-resolved script path
is encoded as URI again. This encoding is trivial (percentage
encoding is optional), so it only needs to prepend a scheme.

Also, this CL improves the error message for the relative path
resolution if the dylib cannot be found to include the script uri.

TEST=tests/ffi/native_assets/asset_relative_test.dart

Closes: https://github.com/dart-lang/sdk/issues/56053
Change-Id: I3ac9748e971e6eacbe14f3485bf3a3943d587d7d
Cq-Include-Trybots: dart/try:vm-aot-asan-linux-release-x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-msan-linux-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-tsan-linux-release-x64-try,vm-aot-ubsan-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64-try,vm-aot-win-debug-x64c-try,vm-asan-linux-release-arm64-try,vm-msan-linux-release-arm64-try,vm-tsan-linux-release-arm64-try,vm-ubsan-linux-release-arm64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/372421
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
diff --git a/runtime/bin/native_assets_api_impl.cc b/runtime/bin/native_assets_api_impl.cc
index 870604d..ab86e14 100644
--- a/runtime/bin/native_assets_api_impl.cc
+++ b/runtime/bin/native_assets_api_impl.cc
@@ -5,6 +5,7 @@
 #include "include/bin/native_assets_api.h"
 
 #include "platform/globals.h"
+#include "platform/utils.h"
 
 #if defined(DART_HOST_OS_WINDOWS)
 #include <Psapi.h>
@@ -45,39 +46,34 @@
 
 // Get a file uri with only forward slashes from the script path.
 // Returned string must be freed by caller.
-static char* CleanScriptUri(const char* script_uri) {
-  const char* path = script_uri;
-#if defined(DART_TARGET_OS_WINDOWS)
-  // Isolate.spawnUri sets a `source` including the file schema.
-  // And on Windows we get an extra forward slash in that case.
-  const char* file_schema_slash = "file:///";
-  const int file_schema_slash_length = 8;
+CStringUniquePtr CleanScriptUri(const char* script_uri) {
+  CStringUniquePtr script_path;
 
-  if (strlen(script_uri) > file_schema_slash_length &&
-      strncmp(script_uri, file_schema_slash, file_schema_slash_length) == 0) {
-    path = (script_uri + file_schema_slash_length);
-  }
-#else
-  // Isolate.spawnUri sets a `source` including the file schema.
   if (strlen(script_uri) > file_schema_length &&
       strncmp(script_uri, file_schema, file_schema_length) == 0) {
-    path = (script_uri + file_schema_length);
+    // Isolate.spawnUri sets a `source` including the file schema,
+    // e.g. Isolate.spawnUri may make the embedder pass a file:// uri.
+    script_path = File::UriToPath(script_uri);
+  } else {
+    // Dart and Flutter embedders set a source without a file schema.
+    script_path = CStringUniquePtr(strdup(script_uri));
   }
-#endif
 
   // Resolve symlinks.
   char canon_path[PATH_MAX];
-  File::GetCanonicalPath(nullptr, path, canon_path, PATH_MAX);
+  File::GetCanonicalPath(nullptr, script_path.get(), canon_path, PATH_MAX);
 
-  // Replace backward slashes with forward slashes.
+  // Convert path to Uri. Inspired by sdk/lib/core/uri.dart _makeFileUri.
+  // Only has a single case, the path is always absolute and always a file.
   const intptr_t len = strlen(canon_path);
   char* path_copy =
       reinterpret_cast<char*>(malloc(file_schema_length + len + 1));
   snprintf(path_copy, len + 1, "%s%s", file_schema, canon_path);
 #if defined(DART_TARGET_OS_WINDOWS)
+  // Replace backward slashes with forward slashes.
   ReplaceBackSlashes(path_copy);
 #endif
-  return path_copy;
+  return CStringUniquePtr(path_copy);
 }
 
 // If an error occurs populates |error| (if provided) with an error message
@@ -111,6 +107,18 @@
   }
 }
 
+static void WrapErrorRelative(const char* path,
+                              const char* script_uri,
+                              char** error) {
+  if (*error != nullptr) {
+    char* inner_error = *error;
+    SET_ERROR_MSG(error,
+                  "Failed to load dynamic library '%s' relative to '%s': %s",
+                  path, script_uri, inner_error);
+    free(inner_error);
+  }
+}
+
 void* NativeAssets::DlopenAbsolute(const char* path, char** error) {
   // If we'd want to be strict, it should not take into account include paths.
   void* handle = LoadDynamicLibrary(path, error);
@@ -122,24 +130,23 @@
                                    const char* script_uri,
                                    char** error) {
   void* handle = nullptr;
-  char* platform_script_cstr = CleanScriptUri(script_uri);
+  CStringUniquePtr platform_script_cstr = CleanScriptUri(script_uri);
   const intptr_t len = strlen(path);
   char* path_copy = reinterpret_cast<char*>(malloc(len + 1));
   snprintf(path_copy, len + 1, "%s", path);
 #if defined(DART_TARGET_OS_WINDOWS)
   ReplaceBackSlashes(path_copy);
 #endif
-  auto target_uri = ResolveUri(path_copy, platform_script_cstr);
+  auto target_uri = ResolveUri(path_copy, platform_script_cstr.get());
   if (!target_uri) {
     SET_ERROR_MSG(error, "Failed to resolve '%s' relative to '%s'.", path_copy,
-                  platform_script_cstr);
+                  platform_script_cstr.get());
   } else {
     const char* target_path = target_uri.get() + file_schema_length;
     handle = LoadDynamicLibrary(target_path, error);
   }
   free(path_copy);
-  free(platform_script_cstr);
-  WrapError(path, error);
+  WrapErrorRelative(path, script_uri, error);
   return handle;
 }
 
diff --git a/tests/ffi/native_assets/helpers.dart b/tests/ffi/native_assets/helpers.dart
index 2f238ad..c13ebce 100644
--- a/tests/ffi/native_assets/helpers.dart
+++ b/tests/ffi/native_assets/helpers.dart
@@ -362,7 +362,7 @@
         aotCompile: aotCompile,
       );
       if (useSymlink) {
-        await withTempDir(prefix: 'link_dir', (tempDir) async {
+        await withTempDir(prefix: 'link dir', (tempDir) async {
           final link = Link.fromUri(tempDir.resolve('my_link'));
           await link.create(snapshotUri.toFilePath());
           await runDartAotRuntime(
@@ -387,7 +387,7 @@
         arguments: runArguments,
       );
       if (useSymlink) {
-        await withTempDir(prefix: 'link_dir', (tempDir) async {
+        await withTempDir(prefix: 'link dir', (tempDir) async {
           final link = Link.fromUri(tempDir.resolve('my_link'));
           await link.create(outDillUri.toFilePath());
           await runDart(
@@ -403,7 +403,7 @@
       }
     case Runtime.jit:
       if (useSymlink) {
-        await withTempDir(prefix: 'link_dir', (tempDir) async {
+        await withTempDir(prefix: 'link dir', (tempDir) async {
           final link = Link.fromUri(tempDir.resolve('my_link'));
           await link.create(outDillUri.toFilePath());
           await runDart(