From d45ba853128db57d2b71892761e3f2af35452831 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 20 May 2026 12:18:21 +0200 Subject: [PATCH] fix(cocoapods): escape local file URIs for unicode paths ## Summary #56878 shows that using unicode characters in paths fails with pod install. Expo fixed this internally here: https://github.com/expo/expo/pull/45779 #fixes Unicode filesystem paths break iOS precompiled xcframework resolution due to unescaped URI handling Fixes #56878 ## How Build local file:// sources through a shared helper that percent-encodes filesystem paths before passing them to URI::File.build. This avoids Ruby/CocoaPods URI failures when React Native projects or local prebuilt tarballs live under paths containing Unicode characters or spaces. Apply the helper to RNCore and ReactNativeDependencies local tarball sources, including RNCore dSYM-processed prebuilt paths, while leaving remote Maven and Sonatype URLs unchanged. Add focused coverage for Unicode, spaces, ASCII paths, and the raw URI::File.build regression case. --- .../scripts/cocoapods/__tests__/utils-test.rb | 47 +++++++++++++++++++ .../react-native/scripts/cocoapods/rncore.rb | 6 +-- .../scripts/cocoapods/rndependencies.rb | 2 +- .../react-native/scripts/cocoapods/utils.rb | 6 +++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index 455562c1a19f..844a4e941443 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -39,6 +39,53 @@ def teardown $RN_PLATFORMS = nil end + # ===================== # + # TEST - localFileUri # + # ===================== # + + def test_localFileUri_whenPathContainsUnicode_returnsEscapedFileUri + # Arrange + path = "/tmp/rn-unicode/💻dev/React-Core-prebuilt.tar.gz" + + # Act + result = ReactNativePodsUtils.local_file_uri(path) + + # Assert + assert_equal("file:///tmp/rn-unicode/%F0%9F%92%BBdev/React-Core-prebuilt.tar.gz", result) + end + + def test_localFileUri_whenPathContainsSpaces_returnsEscapedFileUri + # Arrange + path = "/tmp/rn space/React Core.tar.gz" + + # Act + result = ReactNativePodsUtils.local_file_uri(path) + + # Assert + assert_equal("file:///tmp/rn%20space/React%20Core.tar.gz", result) + end + + def test_localFileUri_whenPathContainsOnlyAscii_returnsFileUri + # Arrange + path = "/tmp/rn-ascii/React-Core-prebuilt.tar.gz" + + # Act + result = ReactNativePodsUtils.local_file_uri(path) + + # Assert + assert_equal("file:///tmp/rn-ascii/React-Core-prebuilt.tar.gz", result) + end + + def test_localFileUri_whenPathContainsUnicode_withoutEscapingUriFileBuildRaises + # Arrange + path = "/tmp/rn-unicode/💻dev/React-Core-prebuilt.tar.gz" + + # Act & Assert + assert_raise(URI::InvalidComponentError) do + URI::File.build(path: path).to_s + end + end + # ======================= # # TEST - warnIfNotOnArm64 # # ======================= # diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 9a36dd1a5a2a..252588c98432 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -103,7 +103,7 @@ def self.resolve_podspec_source() if ENV["RCT_TESTONLY_RNCORE_TARBALL_PATH"] abort_if_use_local_rncore_with_no_file() rncore_log("Using local xcframework at #{ENV["RCT_TESTONLY_RNCORE_TARBALL_PATH"]}") - return {:http => "file://#{ENV["RCT_TESTONLY_RNCORE_TARBALL_PATH"]}" } + return {:http => ReactNativePodsUtils.local_file_uri(ENV["RCT_TESTONLY_RNCORE_TARBALL_PATH"]) } end if ENV["RCT_USE_PREBUILT_RNCORE"] == "1" @@ -161,7 +161,7 @@ def self.podspec_source_download_prebuild_stable_tarball() rncore_log(" #{Pathname.new(destinationRelease).relative_path_from(Pathname.pwd).to_s}") return {:http => stable_tarball_url(@@react_native_version, :debug) } unless @@download_dsyms - return {:http => URI::File.build(path: destinationDebug).to_s } + return {:http => ReactNativePodsUtils.local_file_uri(destinationDebug) } end def self.podspec_source_download_prebuilt_nightly_tarball() @@ -198,7 +198,7 @@ def self.podspec_source_download_prebuilt_nightly_tarball() rncore_log(" #{Pathname.new(destinationDebug).relative_path_from(Pathname.pwd).to_s}") rncore_log(" #{Pathname.new(destinationRelease).relative_path_from(Pathname.pwd).to_s}") return {:http => nightly_tarball_url(@@react_native_version, :debug) } unless @@download_dsyms - return {:http => URI::File.build(path: destinationDebug).to_s } + return {:http => ReactNativePodsUtils.local_file_uri(destinationDebug) } end def self.process_dsyms(frameworkTarball, dSymsTarball) diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 2f4b7c6a9439..1c7fac1a6cb7 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -70,7 +70,7 @@ def self.resolve_podspec_source() if ENV["RCT_USE_LOCAL_RN_DEP"] abort_if_use_local_rndeps_with_no_file() rndeps_log("Using local xcframework at #{ENV["RCT_USE_LOCAL_RN_DEP"]}") - return {:http => "file://#{ENV["RCT_USE_LOCAL_RN_DEP"]}" } + return {:http => ReactNativePodsUtils.local_file_uri(ENV["RCT_USE_LOCAL_RN_DEP"]) } end if ENV["RCT_USE_RN_DEP"] && ENV["RCT_USE_RN_DEP"] == "1" diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 6576f6e559b1..0e388d6a7ef5 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -5,12 +5,18 @@ require 'shellwords' require 'digest' +require 'uri' require_relative "./helpers.rb" require_relative "./jsengine.rb" # Utilities class for React Native Cocoapods class ReactNativePodsUtils + # URI::File.build validates path components as ASCII, so escape the filesystem path first. + def self.local_file_uri(path) + URI::File.build(path: URI::DEFAULT_PARSER.escape(path)).to_s + end + def self.warn_if_not_on_arm64 if SysctlChecker.new().call_sysctl_arm64() == 1 && !Environment.new().ruby_platform().include?('arm64') Pod::UI.warn 'Do not use "pod install" from inside Rosetta2 (x86_64 emulation on arm64).'