From e2a4b2410eb84c1f211f2f2f564b7bb68937f1fd Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:32:45 -0700 Subject: [PATCH] fix(mobile): reject unsafe cache file reads --- ...hybrid-webview-implementation-checklist.md | 10 ++- ...-mobile-hybrid-webview-parity-inventory.md | 5 +- ...bile-hybrid-webview-single-pr-migration.md | 7 +- ...27-mobile-hybrid-webview-remaining-work.md | 13 +++- .../MobileWebCacheFileBoundary.kt | 39 +++++++++++ .../mobilewebshell/MobileWebPackageStore.kt | 26 ++------ .../MobileWebCacheFileBoundaryTest.kt | 49 ++++++++++++++ .../MobileWebCacheFileBoundaryTests.swift | 66 +++++++++++++++++++ .../MobileWebPackageStoreTests.swift | 3 + .../ios/MobileWebCacheFileBoundary.swift | 58 ++++++++++++++++ .../ios/MobileWebPackageStore.swift | 34 ++-------- .../run-ios-mobile-web-store-tests.mjs | 5 ++ 12 files changed, 260 insertions(+), 55 deletions(-) create mode 100644 mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundary.kt create mode 100644 mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundaryTest.kt create mode 100644 mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebCacheFileBoundaryTests.swift create mode 100644 mobile/packages/expo-mobile-web-shell/ios/MobileWebCacheFileBoundary.swift diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md index 4c4c8f995f8..679734deeae 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md @@ -1572,8 +1572,12 @@ copy. primary/canonical manifests now read at most 256 KiB plus one overflow byte, activation metadata at most 1 KiB plus one, and assets at most their declared length plus one on both platforms. Mirrored Swift/Kotlin faults preserve the - stable generation and activation errors. Broader generated mutation, CSP - behavior, and generation-directory cache fuzzing remains open. + stable generation and activation errors. Every persisted read now also + requires a regular descendant of the native cache root. A mirrored corpus + rejects outside-root files, file and ancestor-directory symlinks, + directories, and missing files with the requested stable error. Broader + generated mutation, CSP behavior, and cache mutation/cleanup fuzzing remains + open. - [ ] Fuzz bridge envelopes, schemas, sizes, IDs, ordering, cancellation, and subscription lifecycle. - [ ] Attempt cross-host, cross-build, cross-workspace, and cross-session races. @@ -2648,4 +2652,6 @@ and diff hygiene pass. A fresh production RNW build remains | 2026-07-28 | Complete | Post-runbook validation passes 569 mobile files / 3,378 tests with 2 expected skips. Mobile and mobile-web typechecks/lints, reliability, max-lines, formatting, diff hygiene, and the unchanged `b17ead7a…` 49-asset package verification pass. | | 2026-07-28 | Finding | Swift `Decodable` ignored unknown activation fields, both stores admitted an explicit null or identical previous generation, and Android `JSONObject` accepted a valid activation object followed by trailing tokens. Both native stores now consume one exact activation object with only distinct lowercase string hashes. | | 2026-07-28 | Complete | The mirrored two-valid/eleven-invalid activation corpus passes the iOS native fault executable and Android Debug unit/Release Kotlin gates. Full mobile validation remains 569 files / 3,378 tests with 2 expected skips; typechecks, lints, reliability, max-lines, formatting, diff hygiene, and unchanged `b17ead7a…` package verification pass. | +| 2026-07-28 | Finding | Persisted cache reads bounded allocation and verified hashes but still followed a symlinked file or ancestor directory. Both stores now require a regular descendant of the native cache root before opening any staged asset, manifest, activation record, or committed asset. | +| 2026-07-28 | Complete | The mirrored cache-file corpus accepts an in-root regular file and rejects an outside-root file, file symlink, ancestor-directory symlink, directory, and missing file with the requested stable error. The iOS native fault executable, Android Debug unit/Release Kotlin gates, 569-file mobile suite, typechecks, lints, reliability, max-lines, formatting, diff hygiene, and unchanged `b17ead7a…` package verification pass. | | 2026-07-28 | Next | Complete the remaining gated cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. | diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md index 77d6af2b625..65d3ce7eb41 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md @@ -524,7 +524,10 @@ failing with its stable error, and mirrored Swift/Kotlin oversized-file faults pass. Activation parsing is also exact on both platforms: only `active` and an optional distinct `previous` string hash are accepted, while the mirrored shape/type/trailing-token corpus fails with -`mobile_web_activation_invalid`. +`mobile_web_activation_invalid`. Persisted cache reads additionally require a +regular descendant of the native cache root. The mirrored boundary corpus +rejects outside-root, symlinked, non-regular, and missing inputs before any +manifest, activation, or executable asset bytes are consumed. The standalone renderer-based workspace, session, files, terminal, source-control, and review presentation is also removed with its Vite-only package path. A production-source boundary requires `src/mobile-web/` to remain diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md index 056042a3c02..2588a7d533d 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md @@ -2753,8 +2753,11 @@ exact directive sequence. Both native stores now cap persisted primary and canonical manifest reads at 256 KiB, activation records at 1 KiB, and asset reads at the manifest-declared length before hashing or parsing. Each reader consumes at most one overflow byte, and mirrored Swift/Kotlin faults preserve -the stable generation and activation errors. Generated mutation and the -remaining CSP/generation-directory cache corpus are still required. +the stable generation and activation errors. Every persisted read also +requires a regular descendant of the native cache root; the mirrored corpus +rejects outside-root files, file and ancestor-directory symlinks, directories, +and missing files with the requested stable error. Generated mutation and the +remaining CSP/cache mutation and cleanup corpus are still required. ## App Store Gate diff --git a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md index e582ab13f13..2c38160378f 100644 --- a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md +++ b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md @@ -211,6 +211,11 @@ The mirrored two-valid/eleven-invalid corpus closes unknown fields, null and non-string hashes, identical active/previous generations, and trailing tokens. The iOS native fault executable, Android Debug unit/Release Kotlin gates, and the same broader validation pass. +Persisted native cache reads now require a regular descendant of the cache +root before opening staged assets, manifests, activation metadata, or committed +assets. Mirrored Swift/Kotlin faults reject outside-root, symlinked, +non-regular, and missing paths with a stable error. Mutation and cleanup fuzzing +remains open below. The remaining security work below is release-app corpus testing, fuzzing, cross-scope races, privacy/authorization audit, and independent review. @@ -263,9 +268,11 @@ cross-scope races, privacy/authorization audit, and independent review. shared extension/MIME/role map now has exact native source parity and mirrored valid/mutated coverage. Persisted primary/canonical manifests, activation metadata, and assets now use bounded `limit + 1` readers on - both platforms, with mirrored oversized-file faults and stable errors. A - fresh exact-app rerun, generated mutation, and the other listed - boundaries remain. + both platforms, with mirrored oversized-file faults and stable errors. + Every cache read also rejects outside-root files, file or ancestor + symlinks, directories, and missing files before opening bytes. A fresh + exact-app rerun, generated mutation, cache mutation/cleanup faults, and + the other listed boundaries remain. - [ ] Attempt cross-host, cross-build, cross-workspace, cross-session, replay, reconnect, process-loss, and host-removal races. - [ ] Verify no credential or privileged host identity reaches URLs, DOM state, diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundary.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundary.kt new file mode 100644 index 00000000000..08a309b840c --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundary.kt @@ -0,0 +1,39 @@ +package expo.modules.mobilewebshell + +import java.io.File +import java.io.FileInputStream + +internal fun readMobileWebFile( + file: File, + withinRoot: File, + byteLimit: Int, + overflowCode: String +): ByteArray { + try { + requireMobileWebRegularFile(file, withinRoot, overflowCode) + val bytes = ByteArray(byteLimit + 1) + var offset = 0 + FileInputStream(file).use { input -> + while (offset < bytes.size) { + val read = input.read(bytes, offset, bytes.size - offset) + if (read <= 0) break + offset += read + } + } + require(offset <= byteLimit) { overflowCode } + return bytes.copyOf(offset) + } catch (error: Exception) { + if (error is IllegalArgumentException && error.message == overflowCode) throw error + throw IllegalArgumentException(overflowCode) + } +} + +private fun requireMobileWebRegularFile(file: File, root: File, errorCode: String) { + val rootPath = root.absoluteFile.path.trimEnd(File.separatorChar) + val prefix = rootPath + File.separator + val filePath = file.absoluteFile.path + require(filePath.startsWith(prefix)) { errorCode } + val relativePath = filePath.removePrefix(prefix) + val expectedPath = File(root.canonicalFile, relativePath).absoluteFile.path + require(file.canonicalFile.path == expectedPath && file.isFile) { errorCode } +} diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt index a92d9b9bb1f..8924059ff24 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt @@ -5,7 +5,6 @@ import android.system.Os import org.json.JSONArray import org.json.JSONObject import java.io.File -import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.security.MessageDigest @@ -168,6 +167,7 @@ internal class MobileWebPackageStore internal constructor( val file = assetFile(stage.root, path) val bytes = readMobileWebFile( file, + cacheRoot, asset.byteLength, "mobile_web_stage_asset_invalid" ) @@ -314,7 +314,7 @@ internal class MobileWebPackageStore internal constructor( ?: throw IllegalArgumentException("mobile_web_asset_unavailable") val file = assetFile(session.root, asset.path) val bytes = try { - readMobileWebFile(file, asset.byteLength, "mobile_web_generation_invalid") + readMobileWebFile(file, cacheRoot, asset.byteLength, "mobile_web_generation_invalid") } catch (_: Exception) { throw IllegalArgumentException("mobile_web_generation_invalid") } @@ -429,6 +429,7 @@ internal class MobileWebPackageStore internal constructor( val file = assetFile(root, asset.path) val bytes = readMobileWebFile( file, + cacheRoot, asset.byteLength, "mobile_web_generation_invalid" ) @@ -442,11 +443,13 @@ internal class MobileWebPackageStore internal constructor( val manifest = parseManifest( readMobileWebFile( File(root, "manifest.json"), + cacheRoot, MANIFEST_JSON_BYTE_LIMIT, "mobile_web_generation_invalid" ).toString(Charsets.UTF_8), readMobileWebFile( File(root, "canonical-manifest.json"), + cacheRoot, MANIFEST_JSON_BYTE_LIMIT, "mobile_web_generation_invalid" ).toString(Charsets.UTF_8) @@ -461,6 +464,7 @@ internal class MobileWebPackageStore internal constructor( parseMobileWebActivationRecord( readMobileWebFile( File(hostRoot, "activation.json"), + cacheRoot, ACTIVATION_JSON_BYTE_LIMIT, "mobile_web_activation_invalid" ).toString(Charsets.UTF_8) @@ -693,24 +697,6 @@ private fun storageException(error: Exception, fallback: String): IllegalArgumen ) } -private fun readMobileWebFile( - file: File, - byteLimit: Int, - overflowCode: String -): ByteArray { - val bytes = ByteArray(byteLimit + 1) - var offset = 0 - FileInputStream(file).use { input -> - while (offset < bytes.size) { - val read = input.read(bytes, offset, bytes.size - offset) - if (read <= 0) break - offset += read - } - } - require(offset <= byteLimit) { overflowCode } - return bytes.copyOf(offset) -} - private fun isStorageUnavailable(error: Throwable?): Boolean { if (error == null) return false if (error is IOException && error.message?.contains("ENOSPC", ignoreCase = true) == true) { diff --git a/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundaryTest.kt b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundaryTest.kt new file mode 100644 index 00000000000..55139faf5af --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebCacheFileBoundaryTest.kt @@ -0,0 +1,49 @@ +package expo.modules.mobilewebshell + +import java.io.File +import java.nio.file.Files +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class MobileWebCacheFileBoundaryTest { + @get:Rule + val temporary = TemporaryFolder() + + @Test + fun rejectsFilesOutsideTheCacheAndSymbolicLinks() { + val root = temporary.newFolder("cache") + val regular = File(root, "regular").apply { writeText("valid") } + assertArrayEquals( + "valid".toByteArray(), + readMobileWebFile(regular, root, 5, "mobile_web_generation_invalid") + ) + + val outside = temporary.newFile("outside").apply { writeText("valid") } + assertRejected(outside, root) + + val fileLink = File(root, "file-link") + Files.createSymbolicLink(fileLink.toPath(), outside.toPath()) + assertRejected(fileLink, root) + + val externalDirectory = temporary.newFolder("external-dir") + File(externalDirectory, "asset").writeText("valid") + val directoryLink = File(root, "directory-link") + Files.createSymbolicLink(directoryLink.toPath(), externalDirectory.toPath()) + assertRejected(File(directoryLink, "asset"), root) + + val directory = File(root, "directory").apply { mkdir() } + assertRejected(directory, root) + assertRejected(File(root, "missing"), root) + } + + private fun assertRejected(file: File, root: File) { + val error = assertThrows(IllegalArgumentException::class.java) { + readMobileWebFile(file, root, 5, "mobile_web_generation_invalid") + } + assertEquals("mobile_web_generation_invalid", error.message) + } +} diff --git a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebCacheFileBoundaryTests.swift b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebCacheFileBoundaryTests.swift new file mode 100644 index 00000000000..9b2a2db7477 --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebCacheFileBoundaryTests.swift @@ -0,0 +1,66 @@ +import Foundation + +enum MobileWebCacheFileBoundaryTests { + static func run(root: URL) throws { + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + let regular = root.appendingPathComponent("regular") + try Data("valid".utf8).write(to: regular) + let bytes = try readMobileWebFile( + regular, + within: root, + byteLimit: 5, + overflowCode: "mobile_web_generation_invalid" + ) + precondition(bytes == Data("valid".utf8)) + + let outside = root.deletingLastPathComponent().appendingPathComponent("outside") + try Data("valid".utf8).write(to: outside) + defer { try? FileManager.default.removeItem(at: outside) } + try assertRejected(outside, within: root) + + let fileLink = root.appendingPathComponent("file-link") + try FileManager.default.createSymbolicLink(at: fileLink, withDestinationURL: outside) + try assertRejected(fileLink, within: root) + + let externalDirectory = root.deletingLastPathComponent().appendingPathComponent("external-dir") + try FileManager.default.createDirectory( + at: externalDirectory, + withIntermediateDirectories: true + ) + defer { try? FileManager.default.removeItem(at: externalDirectory) } + try Data("valid".utf8).write(to: externalDirectory.appendingPathComponent("asset")) + let directoryLink = root.appendingPathComponent("directory-link") + try FileManager.default.createSymbolicLink( + at: directoryLink, + withDestinationURL: externalDirectory + ) + try assertRejected(directoryLink.appendingPathComponent("asset"), within: root) + + let directory = root.appendingPathComponent("directory") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try assertRejected(directory, within: root) + try assertRejected(root.appendingPathComponent("missing"), within: root) + } + + private static func assertRejected(_ file: URL, within root: URL) throws { + precondition( + throwsCode("mobile_web_generation_invalid") { + _ = try readMobileWebFile( + file, + within: root, + byteLimit: 5, + overflowCode: "mobile_web_generation_invalid" + ) + } + ) + } + + private static func throwsCode(_ code: String, _ body: () throws -> Void) -> Bool { + do { + try body() + return false + } catch { + return error.localizedDescription == code + } + } +} diff --git a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift index 810bf39dc6c..6e0440b0f2d 100644 --- a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift +++ b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift @@ -28,6 +28,9 @@ enum MobileWebPackageStoreTests { try MobileWebActivationMetadataTests.run( root: root.appendingPathComponent("activation-types") ) + try MobileWebCacheFileBoundaryTests.run( + root: root.appendingPathComponent("cache-file-boundary") + ) try rejectsLowStorage(root: root.appendingPathComponent("low-storage")) try evictsUnprotectedGeneration(root: root.appendingPathComponent("eviction")) try evictsAnotherHostForGlobalQuota(root: root.appendingPathComponent("global-eviction")) diff --git a/mobile/packages/expo-mobile-web-shell/ios/MobileWebCacheFileBoundary.swift b/mobile/packages/expo-mobile-web-shell/ios/MobileWebCacheFileBoundary.swift new file mode 100644 index 00000000000..35babd58b3d --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/ios/MobileWebCacheFileBoundary.swift @@ -0,0 +1,58 @@ +import Foundation + +func readMobileWebFile( + _ url: URL, + within cacheRoot: URL, + byteLimit: Int, + overflowCode: String +) throws -> Data { + do { + try requireMobileWebRegularFile(url, within: cacheRoot, errorCode: overflowCode) + let handle = try FileHandle(forReadingFrom: url) + defer { try? handle.close() } + var data = Data() + data.reserveCapacity(byteLimit + 1) + while data.count <= byteLimit { + let remaining = byteLimit + 1 - data.count + guard + remaining > 0, + let chunk = try handle.read(upToCount: min(64 * 1024, remaining)), + !chunk.isEmpty + else { + break + } + data.append(chunk) + } + guard data.count <= byteLimit else { + throw MobileWebStoreError(overflowCode) + } + return data + } catch let error as MobileWebStoreError { + throw error + } catch { + throw MobileWebStoreError(overflowCode) + } +} + +private func requireMobileWebRegularFile( + _ url: URL, + within cacheRoot: URL, + errorCode: String +) throws { + let root = cacheRoot.standardizedFileURL + let file = url.standardizedFileURL + let prefix = root.path.hasSuffix("/") ? root.path : root.path + "/" + guard file.path.hasPrefix(prefix) else { + throw MobileWebStoreError(errorCode) + } + let relativePath = String(file.path.dropFirst(prefix.count)) + let resolvedRoot = root.resolvingSymlinksInPath().standardizedFileURL + let expected = relativePath.split(separator: "/").reduce(resolvedRoot) { parent, component in + parent.appendingPathComponent(String(component), isDirectory: false) + } + let resolvedFile = file.resolvingSymlinksInPath().standardizedFileURL + let values = try resolvedFile.resourceValues(forKeys: [.isRegularFileKey]) + guard resolvedFile.path == expected.standardizedFileURL.path, values.isRegularFile == true else { + throw MobileWebStoreError(errorCode) + } +} diff --git a/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift b/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift index e9654cc229a..572c6757452 100644 --- a/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift +++ b/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift @@ -219,6 +219,7 @@ final class MobileWebPackageStore { let file = assetUrl(root: stage.root, path: path) let bytes = try readMobileWebFile( file, + within: try cacheRoot(), byteLimit: asset.byteLength, overflowCode: "mobile_web_stage_asset_invalid" ) @@ -443,6 +444,7 @@ final class MobileWebPackageStore { do { data = try readMobileWebFile( file, + within: try cacheRoot(), byteLimit: asset.byteLength, overflowCode: "mobile_web_generation_invalid" ) @@ -598,6 +600,7 @@ final class MobileWebPackageStore { for asset in manifest.assets.values { let bytes = try readMobileWebFile( assetUrl(root: root, path: asset.path), + within: try cacheRoot(), byteLimit: asset.byteLength, overflowCode: "mobile_web_generation_invalid" ) @@ -611,11 +614,13 @@ final class MobileWebPackageStore { do { let manifestData = try readMobileWebFile( root.appendingPathComponent("manifest.json"), + within: try cacheRoot(), byteLimit: manifestJsonByteLimit, overflowCode: "mobile_web_generation_invalid" ) let canonicalManifestData = try readMobileWebFile( root.appendingPathComponent("canonical-manifest.json"), + within: try cacheRoot(), byteLimit: manifestJsonByteLimit, overflowCode: "mobile_web_generation_invalid" ) @@ -640,6 +645,7 @@ final class MobileWebPackageStore { do { let data = try readMobileWebFile( hostRoot.appendingPathComponent("activation.json"), + within: try cacheRoot(), byteLimit: activationJsonByteLimit, overflowCode: "mobile_web_activation_invalid" ) @@ -895,7 +901,7 @@ final class MobileWebPackageStore { let sharedMobileWebPackageStore = MobileWebPackageStore() #endif -private struct MobileWebStoreError: LocalizedError { +struct MobileWebStoreError: LocalizedError { let code: String var errorDescription: String? { code } @@ -911,32 +917,6 @@ private func storageError(_ error: Error, fallback: String) -> MobileWebStoreErr return MobileWebStoreError(code) } -private func readMobileWebFile( - _ url: URL, - byteLimit: Int, - overflowCode: String -) throws -> Data { - let handle = try FileHandle(forReadingFrom: url) - defer { try? handle.close() } - var data = Data() - data.reserveCapacity(byteLimit + 1) - while data.count <= byteLimit { - let remaining = byteLimit + 1 - data.count - guard - remaining > 0, - let chunk = try handle.read(upToCount: min(64 * 1024, remaining)), - !chunk.isEmpty - else { - break - } - data.append(chunk) - } - guard data.count <= byteLimit else { - throw MobileWebStoreError(overflowCode) - } - return data -} - private func mobileWebAvailableStorageBytes(_ root: URL) -> Int64? { try? root.resourceValues( forKeys: [.volumeAvailableCapacityForImportantUsageKey] diff --git a/mobile/scripts/run-ios-mobile-web-store-tests.mjs b/mobile/scripts/run-ios-mobile-web-store-tests.mjs index 8ab3e2f8988..1df5c732b78 100644 --- a/mobile/scripts/run-ios-mobile-web-store-tests.mjs +++ b/mobile/scripts/run-ios-mobile-web-store-tests.mjs @@ -20,11 +20,16 @@ try { '-DMOBILE_WEB_PACKAGE_STORE_TESTING', join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebCacheStoragePolicy.swift'), join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift'), + join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebCacheFileBoundary.swift'), join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift'), join( mobileRoot, 'packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift' ), + join( + mobileRoot, + 'packages/expo-mobile-web-shell/ios-tests/MobileWebCacheFileBoundaryTests.swift' + ), join( mobileRoot, 'packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreProcessInterruptionTests.swift'