@hot-updater/react-native 1.0.0-rc.14 → 1.0.0-rc.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/proguard-rules.pro +2 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/lib/commonjs/checkForUpdate.js +7 -10
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +151 -39
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +247 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +15 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +101 -0
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +7 -8
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +21 -16
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.js +4 -1
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/uuidv7.js +41 -0
- package/lib/commonjs/uuidv7.js.map +1 -0
- package/lib/commonjs/uuidv7.spec.js +30 -0
- package/lib/commonjs/uuidv7.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +1 -3
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +7 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +7 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +153 -41
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +248 -6
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +15 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +102 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +7 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +21 -16
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.js +5 -2
- package/lib/module/notifyAppReadyInsights.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.spec.js +59 -0
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/uuidv7.js +36 -0
- package/lib/module/uuidv7.js.map +1 -0
- package/lib/module/uuidv7.spec.js +30 -0
- package/lib/module/uuidv7.spec.js.map +1 -0
- package/lib/module/wrap.js +1 -3
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +7 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +0 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
- package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +0 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/uuidv7.d.ts +10 -0
- package/lib/typescript/module/uuidv7.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +7 -4
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
- package/src/checkForUpdate.ts +5 -11
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +289 -8
- package/src/httpClient.ts +224 -67
- package/src/index.spec.ts +70 -1
- package/src/index.ts +18 -7
- package/src/native.spec.ts +23 -16
- package/src/native.ts +12 -24
- package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
- package/src/notifyAppReadyInsights.spec.ts +60 -0
- package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
- package/src/notifyAppReadyInsights.ts +13 -1
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/uuidv7.spec.ts +25 -0
- package/src/uuidv7.ts +38 -0
- package/src/wrap.spec.ts +8 -0
- package/src/wrap.tsx +0 -2
- package/src/wrap.types.ts +9 -4
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
|
@@ -216,11 +216,10 @@ class HotUpdaterImplTest {
|
|
|
216
216
|
|
|
217
217
|
override suspend fun updateBundle(
|
|
218
218
|
bundleId: String,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
changedAssets: Map<String, ChangedAssetDescriptor>?,
|
|
219
|
+
manifestUrl: String,
|
|
220
|
+
manifestFileHash: String,
|
|
221
|
+
assets: Map<String, ChangedAssetDescriptor>,
|
|
222
|
+
archiveUrl: String?,
|
|
224
223
|
progressCallback: (UpdateProgressPayload) -> Unit,
|
|
225
224
|
) = Unit
|
|
226
225
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertArrayEquals
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertThrows
|
|
6
|
+
import org.junit.Rule
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.rules.TemporaryFolder
|
|
9
|
+
import java.io.ByteArrayInputStream
|
|
10
|
+
import java.io.ByteArrayOutputStream
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.io.IOException
|
|
13
|
+
import java.util.Base64
|
|
14
|
+
|
|
15
|
+
class TarBrArchiveExtractorTest {
|
|
16
|
+
@get:Rule
|
|
17
|
+
val temporaryFolder = TemporaryFolder()
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `extracts exact regular file set`() {
|
|
21
|
+
val destination = extract(VALID, 3072, mapOf("assets/image.png" to 5, "index.android.bundle" to 6))
|
|
22
|
+
|
|
23
|
+
assertArrayEquals("image".toByteArray(), File(destination, "assets/image.png").readBytes())
|
|
24
|
+
assertArrayEquals("bundle".toByteArray(), File(destination, "index.android.bundle").readBytes())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun `supports a POSIX PAX long path`() {
|
|
29
|
+
val path = "assets/${"x".repeat(120)}.bin"
|
|
30
|
+
val destination = extract(PAX_LONG_PATH, 3072, mapOf(path to 4))
|
|
31
|
+
|
|
32
|
+
assertArrayEquals("long".toByteArray(), File(destination, path).readBytes())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `rejects duplicate unsafe linked and malformed archives`() {
|
|
37
|
+
val cases =
|
|
38
|
+
listOf(
|
|
39
|
+
Triple(DUPLICATE, 3072L, mapOf("index.android.bundle" to 6L)),
|
|
40
|
+
Triple(TRAVERSAL, 2048L, mapOf("../escape" to 3L)),
|
|
41
|
+
Triple(SYMLINK, 1536L, mapOf("index.android.bundle" to 0L)),
|
|
42
|
+
Triple(SINGLE_END_BLOCK, 1536L, mapOf("index.android.bundle" to 6L)),
|
|
43
|
+
Triple(TRAILING_NON_ZERO, 2560L, mapOf("index.android.bundle" to 6L)),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
cases.forEachIndexed { index, (fixture, tarSize, files) ->
|
|
47
|
+
val archive = temporaryFolder.newFile("invalid-$index.tar.br").apply { writeBytes(decode(fixture)) }
|
|
48
|
+
val destination = File(temporaryFolder.root, "invalid-$index")
|
|
49
|
+
|
|
50
|
+
assertThrows(Exception::class.java) {
|
|
51
|
+
TarBrArchiveExtractor.extract(archive, destination, tarSize, files)
|
|
52
|
+
}
|
|
53
|
+
check(!destination.exists())
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `rejects dangling consecutive duplicate and unsupported extension metadata`() {
|
|
59
|
+
val pathRecord = paxRecord("path", "index.android.bundle")
|
|
60
|
+
val malformedShortRecord = "3 \n".toByteArray()
|
|
61
|
+
val cases =
|
|
62
|
+
listOf(
|
|
63
|
+
tarArchive(tarEntry("PaxHeader", 'x', pathRecord)),
|
|
64
|
+
tarArchive(
|
|
65
|
+
tarEntry("PaxHeader", 'x', pathRecord),
|
|
66
|
+
tarEntry("PaxHeader", 'x', pathRecord),
|
|
67
|
+
),
|
|
68
|
+
tarArchive(
|
|
69
|
+
tarEntry("PaxHeader", 'x', pathRecord + paxRecord("path", "other")),
|
|
70
|
+
regularEntry("ignored", "bundle"),
|
|
71
|
+
),
|
|
72
|
+
tarArchive(
|
|
73
|
+
tarEntry("PaxHeader", 'x', malformedShortRecord),
|
|
74
|
+
regularEntry("ignored", "bundle"),
|
|
75
|
+
),
|
|
76
|
+
tarArchive(
|
|
77
|
+
tarEntry("PaxHeader", 'x', paxRecord("linkpath", "target")),
|
|
78
|
+
regularEntry("index.android.bundle", "bundle"),
|
|
79
|
+
),
|
|
80
|
+
tarArchive(tarEntry("LongLink", 'L', "name\u0000".toByteArray())),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
cases.forEach { bytes ->
|
|
84
|
+
assertThrows(IOException::class.java) { consumeTar(bytes) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun `preserves exact header paths and applies PAX size`() {
|
|
90
|
+
val spacedPathArchive = tarArchive(regularEntry("index.android.bundle ", "bundle"))
|
|
91
|
+
TarArchiveInputStream(ByteArrayInputStream(spacedPathArchive)).use { input ->
|
|
92
|
+
assertEquals("index.android.bundle ", input.getNextEntry()?.name)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
val payload = "data".toByteArray()
|
|
96
|
+
val paxSizeArchive =
|
|
97
|
+
tarArchive(
|
|
98
|
+
tarEntry("PaxHeader", 'x', paxRecord("size", payload.size.toString())),
|
|
99
|
+
tarEntry("index.android.bundle", '0', payload, declaredSize = 1),
|
|
100
|
+
)
|
|
101
|
+
TarArchiveInputStream(ByteArrayInputStream(paxSizeArchive)).use { input ->
|
|
102
|
+
val entry = input.getNextEntry()
|
|
103
|
+
assertEquals(payload.size.toLong(), entry?.size)
|
|
104
|
+
assertArrayEquals(payload, input.readBytes())
|
|
105
|
+
assertEquals(null, input.getNextEntry())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun extract(
|
|
110
|
+
fixture: String,
|
|
111
|
+
tarSize: Long,
|
|
112
|
+
files: Map<String, Long>,
|
|
113
|
+
): File {
|
|
114
|
+
val archive = temporaryFolder.newFile("fixture-${System.nanoTime()}.tar.br").apply { writeBytes(decode(fixture)) }
|
|
115
|
+
val destination = File(temporaryFolder.root, "extracted-${System.nanoTime()}")
|
|
116
|
+
TarBrArchiveExtractor.extract(archive, destination, tarSize, files)
|
|
117
|
+
return destination
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun decode(value: String): ByteArray = Base64.getDecoder().decode(value)
|
|
121
|
+
|
|
122
|
+
private fun consumeTar(bytes: ByteArray) {
|
|
123
|
+
TarArchiveInputStream(ByteArrayInputStream(bytes)).use { input ->
|
|
124
|
+
while (input.getNextEntry() != null) input.readBytes()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun paxRecord(
|
|
129
|
+
key: String,
|
|
130
|
+
value: String,
|
|
131
|
+
): ByteArray {
|
|
132
|
+
var length = key.toByteArray().size + value.toByteArray().size + 3
|
|
133
|
+
while (true) {
|
|
134
|
+
val record = "$length $key=$value\n".toByteArray()
|
|
135
|
+
if (record.size == length) return record
|
|
136
|
+
length = record.size
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun regularEntry(
|
|
141
|
+
name: String,
|
|
142
|
+
content: String,
|
|
143
|
+
): ByteArray = tarEntry(name, '0', content.toByteArray())
|
|
144
|
+
|
|
145
|
+
private fun tarEntry(
|
|
146
|
+
name: String,
|
|
147
|
+
type: Char,
|
|
148
|
+
payload: ByteArray,
|
|
149
|
+
declaredSize: Int = payload.size,
|
|
150
|
+
): ByteArray {
|
|
151
|
+
val header = ByteArray(512)
|
|
152
|
+
name.toByteArray().copyInto(header, endIndex = name.toByteArray().size.coerceAtMost(100))
|
|
153
|
+
writeOctal(header, 100, 8, 420)
|
|
154
|
+
writeOctal(header, 108, 8, 0)
|
|
155
|
+
writeOctal(header, 116, 8, 0)
|
|
156
|
+
writeOctal(header, 124, 12, declaredSize)
|
|
157
|
+
writeOctal(header, 136, 12, 0)
|
|
158
|
+
for (index in 148 until 156) header[index] = ' '.code.toByte()
|
|
159
|
+
header[156] = type.code.toByte()
|
|
160
|
+
"ustar".toByteArray().copyInto(header, 257)
|
|
161
|
+
writeOctal(header, 148, 8, header.sumOf { it.toInt() and 0xff })
|
|
162
|
+
|
|
163
|
+
return ByteArrayOutputStream()
|
|
164
|
+
.apply {
|
|
165
|
+
write(header)
|
|
166
|
+
write(payload)
|
|
167
|
+
write(ByteArray((512 - payload.size % 512) % 512))
|
|
168
|
+
}.toByteArray()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fun tarArchive(vararg entries: ByteArray): ByteArray =
|
|
172
|
+
ByteArrayOutputStream()
|
|
173
|
+
.apply {
|
|
174
|
+
entries.forEach(::write)
|
|
175
|
+
write(ByteArray(1024))
|
|
176
|
+
}.toByteArray()
|
|
177
|
+
|
|
178
|
+
private fun writeOctal(
|
|
179
|
+
target: ByteArray,
|
|
180
|
+
offset: Int,
|
|
181
|
+
length: Int,
|
|
182
|
+
value: Int,
|
|
183
|
+
) {
|
|
184
|
+
val digits = value.toString(8).padStart(length - 2, '0')
|
|
185
|
+
digits.toByteArray().copyInto(target, offset)
|
|
186
|
+
target[offset + length - 2] = 0
|
|
187
|
+
target[offset + length - 1] = ' '.code.toByte()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
companion object {
|
|
191
|
+
const val VALID =
|
|
192
|
+
"G/8LYI7UVnlCO6lhAMTqIo5tMQLiY7NptuP57iYyogHNeF9BlHNiXHeDMLCAJL3h9im3UyYnSs3aLbJIIBsjml0YwV1+Hd11htDfIA8GgPKHFrFNtD7SOfZo/h9mbgDYuSAJIPmy9Z2b"
|
|
193
|
+
private const val DUPLICATE =
|
|
194
|
+
"G/8L4I/UVnlCO6lh6sQAIm5ssV7ksdnUyXHelkgImbdLFCl08DwgyVOGdSmhfs8LgniesZ82/bHw7qIYoXwdSTsMVSkOBkDMM0vqHj11Ew=="
|
|
195
|
+
private const val TRAVERSAL =
|
|
196
|
+
"G/8H+AfKbRfTdvBDW1YMVZ0c3w7fLhJCyAFHCIm3xTUACIRyzK7qNZRgrBkUGlOQ+PtOBlH/uDAUZ9s9Dw=="
|
|
197
|
+
private const val SYMLINK =
|
|
198
|
+
"G/8FoI7UYs0ZrwQiHgYxvWizaUNMJ8cnP4h9lyiDwKKELcCA8pSZdZEoyN91S9BvNObx0n8IcrtxYn2sUe6QrffCuHk8LCgA"
|
|
199
|
+
private const val SINGLE_END_BLOCK =
|
|
200
|
+
"G/8F4I/UVnlCO6lh6sQAIm5ssV7ksdnUyfH9WyIhZN4uUaQSeB6Q5DGGvrXYkguCOs/YT5v+GLy7ECOUryNph6EqxcEAinlmEw=="
|
|
201
|
+
private const val TRAILING_NON_ZERO =
|
|
202
|
+
"G/8J4Adqc2ds55CPYbC2qk6O87ZEQsi0tUsUKUXN80TyGEPfWmyJBUGcZ6y72vQfJjUzyChogu7Xu5ODMLcUB0MBYu7ZOLkF"
|
|
203
|
+
private const val PAX_LONG_PATH =
|
|
204
|
+
"G/8LYAbCtn2mBfjh0GTohqrNYvNuZjueb2Ij3vmAJjz8C6jLIEtYgqi7gAMNAII62ZkiZILxgnbd1QSM8usRpO6ZyCgYZiz8dTRcx0YB/gG5MBRqZPyVyrk8J47iYenP+zXoZcGa8QLgayISZoflgdi14Q=="
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import Compression
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum BrotliFileDecompressor {
|
|
5
|
+
private static let bufferSize = 64 * 1024
|
|
6
|
+
|
|
7
|
+
static func decompress(
|
|
8
|
+
from sourcePath: String,
|
|
9
|
+
to outputPath: String,
|
|
10
|
+
expectedOutputByteSize: Int64? = nil
|
|
11
|
+
) throws {
|
|
12
|
+
if let expectedOutputByteSize, expectedOutputByteSize < 0 {
|
|
13
|
+
throw error(6, "Invalid expected Brotli output size")
|
|
14
|
+
}
|
|
15
|
+
let fileManager = FileManager.default
|
|
16
|
+
let parentDirectory = (outputPath as NSString).deletingLastPathComponent
|
|
17
|
+
try fileManager.createDirectory(
|
|
18
|
+
atPath: parentDirectory,
|
|
19
|
+
withIntermediateDirectories: true
|
|
20
|
+
)
|
|
21
|
+
if fileManager.fileExists(atPath: outputPath) {
|
|
22
|
+
try fileManager.removeItem(atPath: outputPath)
|
|
23
|
+
}
|
|
24
|
+
guard fileManager.createFile(atPath: outputPath, contents: nil) else {
|
|
25
|
+
throw error(1, "Failed to create Brotli output file")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let input = try FileHandle(forReadingFrom: URL(fileURLWithPath: sourcePath))
|
|
29
|
+
let output = try FileHandle(forWritingTo: URL(fileURLWithPath: outputPath))
|
|
30
|
+
defer {
|
|
31
|
+
try? input.close()
|
|
32
|
+
try? output.close()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var stream = compression_stream(
|
|
36
|
+
dst_ptr: UnsafeMutablePointer<UInt8>(bitPattern: 1)!,
|
|
37
|
+
dst_size: 0,
|
|
38
|
+
src_ptr: UnsafePointer<UInt8>(bitPattern: 1)!,
|
|
39
|
+
src_size: 0,
|
|
40
|
+
state: nil
|
|
41
|
+
)
|
|
42
|
+
guard compression_stream_init(
|
|
43
|
+
&stream,
|
|
44
|
+
COMPRESSION_STREAM_DECODE,
|
|
45
|
+
COMPRESSION_BROTLI
|
|
46
|
+
) == COMPRESSION_STATUS_OK else {
|
|
47
|
+
throw error(2, "Failed to initialize Brotli decompression")
|
|
48
|
+
}
|
|
49
|
+
defer { compression_stream_destroy(&stream) }
|
|
50
|
+
|
|
51
|
+
let outputBuffer = UnsafeMutablePointer<UInt8>.allocate(
|
|
52
|
+
capacity: bufferSize
|
|
53
|
+
)
|
|
54
|
+
defer { outputBuffer.deallocate() }
|
|
55
|
+
|
|
56
|
+
var reachedEnd = false
|
|
57
|
+
var totalOutputByteSize: UInt64 = 0
|
|
58
|
+
let maximumOutputByteSize = expectedOutputByteSize.map(UInt64.init)
|
|
59
|
+
while !reachedEnd {
|
|
60
|
+
let chunk = try FileUtilities.readUpToCount(
|
|
61
|
+
from: input,
|
|
62
|
+
count: bufferSize
|
|
63
|
+
) ?? Data()
|
|
64
|
+
let status: compression_status
|
|
65
|
+
if chunk.isEmpty {
|
|
66
|
+
stream.src_ptr = UnsafePointer<UInt8>(bitPattern: 1)!
|
|
67
|
+
stream.src_size = 0
|
|
68
|
+
status = try process(
|
|
69
|
+
&stream,
|
|
70
|
+
output: output,
|
|
71
|
+
outputBuffer: outputBuffer,
|
|
72
|
+
finalize: true,
|
|
73
|
+
totalOutputByteSize: &totalOutputByteSize,
|
|
74
|
+
maximumOutputByteSize: maximumOutputByteSize
|
|
75
|
+
)
|
|
76
|
+
} else {
|
|
77
|
+
status = try chunk.withUnsafeBytes { bytes in
|
|
78
|
+
guard let source = bytes.baseAddress?
|
|
79
|
+
.assumingMemoryBound(to: UInt8.self) else {
|
|
80
|
+
return COMPRESSION_STATUS_OK
|
|
81
|
+
}
|
|
82
|
+
stream.src_ptr = source
|
|
83
|
+
stream.src_size = chunk.count
|
|
84
|
+
return try process(
|
|
85
|
+
&stream,
|
|
86
|
+
output: output,
|
|
87
|
+
outputBuffer: outputBuffer,
|
|
88
|
+
finalize: false,
|
|
89
|
+
totalOutputByteSize: &totalOutputByteSize,
|
|
90
|
+
maximumOutputByteSize: maximumOutputByteSize
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if status == COMPRESSION_STATUS_END {
|
|
96
|
+
guard stream.src_size == 0 else {
|
|
97
|
+
throw error(7, "Brotli stream contains trailing compressed bytes")
|
|
98
|
+
}
|
|
99
|
+
let trailing = try FileUtilities.readUpToCount(from: input, count: 1)
|
|
100
|
+
guard trailing?.isEmpty != false else {
|
|
101
|
+
throw error(7, "Brotli stream contains trailing compressed bytes")
|
|
102
|
+
}
|
|
103
|
+
reachedEnd = true
|
|
104
|
+
} else if chunk.isEmpty {
|
|
105
|
+
throw error(3, "Brotli input ended before the stream completed")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if let maximumOutputByteSize,
|
|
110
|
+
totalOutputByteSize != maximumOutputByteSize {
|
|
111
|
+
throw error(
|
|
112
|
+
8,
|
|
113
|
+
"Brotli output size mismatch: expected \(maximumOutputByteSize), got \(totalOutputByteSize)"
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static func process(
|
|
119
|
+
_ stream: inout compression_stream,
|
|
120
|
+
output: FileHandle,
|
|
121
|
+
outputBuffer: UnsafeMutablePointer<UInt8>,
|
|
122
|
+
finalize: Bool,
|
|
123
|
+
totalOutputByteSize: inout UInt64,
|
|
124
|
+
maximumOutputByteSize: UInt64?
|
|
125
|
+
) throws -> compression_status {
|
|
126
|
+
let flags = finalize ? Int32(COMPRESSION_STREAM_FINALIZE.rawValue) : 0
|
|
127
|
+
var status = COMPRESSION_STATUS_OK
|
|
128
|
+
repeat {
|
|
129
|
+
let previousSourceSize = stream.src_size
|
|
130
|
+
stream.dst_ptr = outputBuffer
|
|
131
|
+
stream.dst_size = bufferSize
|
|
132
|
+
status = compression_stream_process(&stream, flags)
|
|
133
|
+
|
|
134
|
+
guard status == COMPRESSION_STATUS_OK ||
|
|
135
|
+
status == COMPRESSION_STATUS_END else {
|
|
136
|
+
throw error(4, "Brotli decompression failed")
|
|
137
|
+
}
|
|
138
|
+
let produced = bufferSize - stream.dst_size
|
|
139
|
+
if produced > 0 {
|
|
140
|
+
let (nextOutputByteSize, overflowed) = totalOutputByteSize
|
|
141
|
+
.addingReportingOverflow(UInt64(produced))
|
|
142
|
+
guard !overflowed,
|
|
143
|
+
maximumOutputByteSize.map({ nextOutputByteSize <= $0 }) ?? true else {
|
|
144
|
+
throw error(6, "Brotli output exceeds expected size")
|
|
145
|
+
}
|
|
146
|
+
output.write(Data(bytes: outputBuffer, count: produced))
|
|
147
|
+
totalOutputByteSize = nextOutputByteSize
|
|
148
|
+
}
|
|
149
|
+
if finalize,
|
|
150
|
+
status == COMPRESSION_STATUS_OK,
|
|
151
|
+
produced == 0,
|
|
152
|
+
previousSourceSize == stream.src_size {
|
|
153
|
+
throw error(5, "Brotli decompression stalled")
|
|
154
|
+
}
|
|
155
|
+
} while stream.src_size > 0 || stream.dst_size == 0 ||
|
|
156
|
+
(finalize && status == COMPRESSION_STATUS_OK)
|
|
157
|
+
return status
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static func error(_ code: Int, _ message: String) -> NSError {
|
|
161
|
+
NSError(
|
|
162
|
+
domain: "BrotliFileDecompressor",
|
|
163
|
+
code: code,
|
|
164
|
+
userInfo: [NSLocalizedDescriptionKey: message]
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
protocol BuiltInAssetResolver: AnyObject {
|
|
4
|
+
func use(bundle: Bundle)
|
|
5
|
+
func copyIfMatches(assetPath: String, expectedHash: String, destination: String) -> Bool
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
final class IOSBuiltInAssetResolver: BuiltInAssetResolver {
|
|
9
|
+
private struct CachedIndex: Codable {
|
|
10
|
+
let schemaVersion: Int
|
|
11
|
+
let packageIdentity: String
|
|
12
|
+
var locators: [String: String]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private let cacheURL: URL
|
|
16
|
+
private let lock = NSLock()
|
|
17
|
+
private var bundle: Bundle = .main
|
|
18
|
+
|
|
19
|
+
init(cacheURL: URL) {
|
|
20
|
+
self.cacheURL = cacheURL
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func use(bundle: Bundle) {
|
|
24
|
+
lock.lock()
|
|
25
|
+
self.bundle = bundle
|
|
26
|
+
lock.unlock()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func copyIfMatches(
|
|
30
|
+
assetPath: String,
|
|
31
|
+
expectedHash: String,
|
|
32
|
+
destination: String
|
|
33
|
+
) -> Bool {
|
|
34
|
+
lock.lock()
|
|
35
|
+
defer { lock.unlock() }
|
|
36
|
+
|
|
37
|
+
guard FileUtilities.normalizedRelativePath(from: assetPath) == assetPath else {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
let packageIdentity = packageIdentity(for: bundle)
|
|
41
|
+
var index = readIndex(packageIdentity: packageIdentity)
|
|
42
|
+
|
|
43
|
+
if let cachedLocator = index.locators[assetPath],
|
|
44
|
+
copyLocatorIfMatches(
|
|
45
|
+
cachedLocator,
|
|
46
|
+
expectedHash: expectedHash,
|
|
47
|
+
destination: destination
|
|
48
|
+
) {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
guard let locator = resolveLocator(assetPath: assetPath),
|
|
53
|
+
copyLocatorIfMatches(
|
|
54
|
+
locator,
|
|
55
|
+
expectedHash: expectedHash,
|
|
56
|
+
destination: destination
|
|
57
|
+
) else {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
index.locators[assetPath] = locator
|
|
62
|
+
writeIndex(index)
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private func resolveLocator(assetPath: String) -> String? {
|
|
67
|
+
if assetPath == "index.ios.bundle" {
|
|
68
|
+
guard let bundleURL = bundle.url(forResource: "main", withExtension: "jsbundle"),
|
|
69
|
+
let relativePath = relativePath(of: bundleURL, inside: bundle.bundleURL) else {
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
return "bundle:\(relativePath)"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
guard let resourceURL = bundle.resourceURL else { return nil }
|
|
76
|
+
let sourceURL = resourceURL.appendingPathComponent(assetPath).standardizedFileURL
|
|
77
|
+
guard relativePath(of: sourceURL, inside: resourceURL) == assetPath,
|
|
78
|
+
isRegularFile(sourceURL) else {
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
return "resource:\(assetPath)"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func copyLocatorIfMatches(
|
|
85
|
+
_ locator: String,
|
|
86
|
+
expectedHash: String,
|
|
87
|
+
destination: String
|
|
88
|
+
) -> Bool {
|
|
89
|
+
guard let sourceURL = sourceURL(for: locator), isRegularFile(sourceURL) else {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
let destinationURL = URL(fileURLWithPath: destination)
|
|
93
|
+
let temporaryURL = destinationURL
|
|
94
|
+
.deletingLastPathComponent()
|
|
95
|
+
.appendingPathComponent("\(destinationURL.lastPathComponent).builtin.tmp")
|
|
96
|
+
do {
|
|
97
|
+
try FileManager.default.createDirectory(
|
|
98
|
+
at: destinationURL.deletingLastPathComponent(),
|
|
99
|
+
withIntermediateDirectories: true
|
|
100
|
+
)
|
|
101
|
+
try? FileManager.default.removeItem(at: temporaryURL)
|
|
102
|
+
try FileManager.default.copyItem(at: sourceURL, to: temporaryURL)
|
|
103
|
+
guard HashUtils.verifyHash(fileURL: temporaryURL, expectedHash: expectedHash) else {
|
|
104
|
+
try? FileManager.default.removeItem(at: temporaryURL)
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
try? FileManager.default.removeItem(at: destinationURL)
|
|
108
|
+
try FileManager.default.moveItem(at: temporaryURL, to: destinationURL)
|
|
109
|
+
return true
|
|
110
|
+
} catch {
|
|
111
|
+
try? FileManager.default.removeItem(at: temporaryURL)
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func sourceURL(for locator: String) -> URL? {
|
|
117
|
+
if locator.hasPrefix("bundle:") {
|
|
118
|
+
let relativePath = String(locator.dropFirst("bundle:".count))
|
|
119
|
+
let sourceURL = bundle.bundleURL.appendingPathComponent(relativePath).standardizedFileURL
|
|
120
|
+
return self.relativePath(of: sourceURL, inside: bundle.bundleURL) == relativePath
|
|
121
|
+
? sourceURL
|
|
122
|
+
: nil
|
|
123
|
+
}
|
|
124
|
+
if locator.hasPrefix("resource:"), let resourceURL = bundle.resourceURL {
|
|
125
|
+
let relativePath = String(locator.dropFirst("resource:".count))
|
|
126
|
+
let sourceURL = resourceURL.appendingPathComponent(relativePath).standardizedFileURL
|
|
127
|
+
return self.relativePath(of: sourceURL, inside: resourceURL) == relativePath
|
|
128
|
+
? sourceURL
|
|
129
|
+
: nil
|
|
130
|
+
}
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func relativePath(of fileURL: URL, inside directoryURL: URL) -> String? {
|
|
135
|
+
let directory = directoryURL.standardizedFileURL.resolvingSymlinksInPath().path
|
|
136
|
+
let file = fileURL.standardizedFileURL.resolvingSymlinksInPath().path
|
|
137
|
+
let prefix = directory.hasSuffix("/") ? directory : "\(directory)/"
|
|
138
|
+
guard file.hasPrefix(prefix) else { return nil }
|
|
139
|
+
return String(file.dropFirst(prefix.count))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func isRegularFile(_ url: URL) -> Bool {
|
|
143
|
+
(try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private func packageIdentity(for bundle: Bundle) -> String {
|
|
147
|
+
let values = try? bundle.bundleURL.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey])
|
|
148
|
+
let bundleIdentifier = bundle.bundleIdentifier ?? ""
|
|
149
|
+
let shortVersion = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
|
150
|
+
let bundleVersion = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
|
|
151
|
+
let bundlePath = bundle.bundleURL.standardizedFileURL.path
|
|
152
|
+
let fileSize = String(values?.fileSize ?? -1)
|
|
153
|
+
let modificationTime = String(values?.contentModificationDate?.timeIntervalSince1970 ?? -1)
|
|
154
|
+
|
|
155
|
+
return [
|
|
156
|
+
bundleIdentifier,
|
|
157
|
+
shortVersion,
|
|
158
|
+
bundleVersion,
|
|
159
|
+
bundlePath,
|
|
160
|
+
fileSize,
|
|
161
|
+
modificationTime,
|
|
162
|
+
].joined(separator: "|")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func readIndex(packageIdentity: String) -> CachedIndex {
|
|
166
|
+
if let data = try? Data(contentsOf: cacheURL),
|
|
167
|
+
let index = try? JSONDecoder().decode(CachedIndex.self, from: data),
|
|
168
|
+
index.schemaVersion == Self.schemaVersion,
|
|
169
|
+
index.packageIdentity == packageIdentity {
|
|
170
|
+
return index
|
|
171
|
+
}
|
|
172
|
+
return CachedIndex(
|
|
173
|
+
schemaVersion: Self.schemaVersion,
|
|
174
|
+
packageIdentity: packageIdentity,
|
|
175
|
+
locators: [:]
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func writeIndex(_ index: CachedIndex) {
|
|
180
|
+
do {
|
|
181
|
+
try FileManager.default.createDirectory(
|
|
182
|
+
at: cacheURL.deletingLastPathComponent(),
|
|
183
|
+
withIntermediateDirectories: true
|
|
184
|
+
)
|
|
185
|
+
let temporaryURL = cacheURL.appendingPathExtension("tmp")
|
|
186
|
+
let data = try JSONEncoder().encode(index)
|
|
187
|
+
try data.write(to: temporaryURL, options: .atomic)
|
|
188
|
+
try? FileManager.default.removeItem(at: cacheURL)
|
|
189
|
+
try FileManager.default.moveItem(at: temporaryURL, to: cacheURL)
|
|
190
|
+
} catch {
|
|
191
|
+
// Cache persistence must never block a valid install.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static let schemaVersion = 1
|
|
196
|
+
}
|