mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
fix(mobile): reject unsafe cache file reads
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+39
@@ -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 }
|
||||
}
|
||||
+6
-20
@@ -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) {
|
||||
|
||||
+49
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user