@hot-updater/react-native 1.0.0-rc.1 → 1.0.0-rc.15
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 +17 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +651 -637
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +10 -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/HotUpdaterRecoveryManager.kt +37 -54
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- 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/HotUpdaterRecoveryStateTest.kt +97 -0
- package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
- 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 +149 -51
- 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/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/checkForUpdate.js +32 -23
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +97 -28
- 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 +46 -29
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +57 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +31 -14
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +56 -11
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +15 -14
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +128 -22
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +28 -21
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/commonjs/notifyAppReadyInsights.js +188 -0
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +161 -29
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +14 -7
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +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/wrap.js +11 -12
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +97 -25
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +32 -23
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +97 -28
- 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 +48 -31
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +57 -5
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +32 -15
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +56 -11
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +13 -12
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +128 -22
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +23 -16
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/module/notifyAppReadyInsights.js +181 -0
- package/lib/module/notifyAppReadyInsights.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +153 -21
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +11 -4
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +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/wrap.js +11 -12
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +99 -25
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -3
- 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 -6
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -7
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +5 -9
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +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/wrap.types.d.ts +5 -8
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -3
- 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 -6
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -7
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +5 -9
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +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/wrap.types.d.ts +5 -8
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +137 -61
- package/src/checkForUpdate.ts +35 -29
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +70 -5
- package/src/httpClient.ts +78 -64
- package/src/index.spec.ts +59 -11
- package/src/index.ts +41 -17
- package/src/native.spec.ts +133 -22
- package/src/native.ts +21 -32
- package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +25 -17
- package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +141 -22
- package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
- package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +13 -5
- package/src/notifyAppReadyInsights.ts +293 -0
- 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/wrap.spec.ts +109 -31
- package/src/wrap.tsx +11 -11
- package/src/wrap.types.ts +5 -8
- 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
- package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
- package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.js +0 -128
- package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/src/notifyAppReadyAnalytics.ts +0 -194
|
@@ -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,97 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNotNull
|
|
6
|
+
import org.junit.Assert.assertNull
|
|
7
|
+
import org.junit.Assert.assertTrue
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import java.util.concurrent.CountDownLatch
|
|
10
|
+
import java.util.concurrent.TimeUnit
|
|
11
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
12
|
+
|
|
13
|
+
class HotUpdaterRecoveryStateTest {
|
|
14
|
+
@Test
|
|
15
|
+
fun `content queued after a fatal error cannot verify the failed bundle`() {
|
|
16
|
+
val state = HotUpdaterRecoveryState()
|
|
17
|
+
state.start("crashed-bundle", true)
|
|
18
|
+
|
|
19
|
+
val request = requireNotNull(state.requestRecovery())
|
|
20
|
+
assertNull(state.completeLaunch())
|
|
21
|
+
state.finishRecoveryRequest(request, true)
|
|
22
|
+
|
|
23
|
+
assertNull(state.completeLaunch())
|
|
24
|
+
assertEquals(HotUpdaterRecoveryState.Launch("crashed-bundle", true), state.snapshot())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun `a fatal error after first content preserves the verified bundle`() {
|
|
29
|
+
val state = HotUpdaterRecoveryState()
|
|
30
|
+
state.start("verified-bundle", true)
|
|
31
|
+
|
|
32
|
+
assertNotNull(state.completeLaunch())
|
|
33
|
+
|
|
34
|
+
assertNull(state.requestRecovery())
|
|
35
|
+
assertNull(state.completeLaunch())
|
|
36
|
+
assertEquals(HotUpdaterRecoveryState.Launch("verified-bundle", false), state.snapshot())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `concurrent fatal errors schedule only one restart while the request is in flight`() {
|
|
41
|
+
val state = HotUpdaterRecoveryState()
|
|
42
|
+
state.start("crashed-bundle", true)
|
|
43
|
+
val begin = CountDownLatch(1)
|
|
44
|
+
val finished = CountDownLatch(2)
|
|
45
|
+
val restartRequests = AtomicInteger()
|
|
46
|
+
val handledErrors = AtomicInteger()
|
|
47
|
+
val workers =
|
|
48
|
+
List(2) {
|
|
49
|
+
Thread {
|
|
50
|
+
begin.await()
|
|
51
|
+
val request = state.requestRecovery()
|
|
52
|
+
if (request != null) {
|
|
53
|
+
handledErrors.incrementAndGet()
|
|
54
|
+
if (request.shouldRestart) restartRequests.incrementAndGet()
|
|
55
|
+
}
|
|
56
|
+
finished.countDown()
|
|
57
|
+
}.apply { start() }
|
|
58
|
+
}
|
|
59
|
+
begin.countDown()
|
|
60
|
+
assertTrue(finished.await(5, TimeUnit.SECONDS))
|
|
61
|
+
workers.forEach { it.join() }
|
|
62
|
+
|
|
63
|
+
assertEquals(2, handledErrors.get())
|
|
64
|
+
assertEquals(1, restartRequests.get())
|
|
65
|
+
assertNull(state.completeLaunch())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun `a failed restart keeps the crash unverified and permits another restart request`() {
|
|
70
|
+
val state = HotUpdaterRecoveryState()
|
|
71
|
+
state.start("crashed-bundle", true)
|
|
72
|
+
val request = requireNotNull(state.requestRecovery())
|
|
73
|
+
|
|
74
|
+
state.finishRecoveryRequest(request, false)
|
|
75
|
+
|
|
76
|
+
assertNull(state.completeLaunch())
|
|
77
|
+
val retry = requireNotNull(state.requestRecovery())
|
|
78
|
+
assertTrue(retry.shouldRestart)
|
|
79
|
+
state.finishRecoveryRequest(retry, true)
|
|
80
|
+
assertFalse(requireNotNull(state.requestRecovery()).shouldRestart)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
fun `a previous launch restart completion cannot change the next launch`() {
|
|
85
|
+
val state = HotUpdaterRecoveryState()
|
|
86
|
+
state.start("crashed-bundle", true)
|
|
87
|
+
val request = requireNotNull(state.requestRecovery())
|
|
88
|
+
state.start("safe-bundle", false)
|
|
89
|
+
|
|
90
|
+
state.finishRecoveryRequest(request, true)
|
|
91
|
+
|
|
92
|
+
assertEquals("safe-bundle", state.completeLaunch()?.bundleId)
|
|
93
|
+
state.stop()
|
|
94
|
+
assertNull(state.snapshot())
|
|
95
|
+
assertNull(state.requestRecovery())
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Rule
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.rules.TemporaryFolder
|
|
9
|
+
import java.io.File
|
|
10
|
+
|
|
11
|
+
class PendingCrashRecoveryTest {
|
|
12
|
+
@get:Rule
|
|
13
|
+
val temporaryFolder = TemporaryFolder()
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `a watchdog reader waits until the native crash marker is fully written`() {
|
|
17
|
+
val marker = temporaryFolder.newFile("crash-marker.json")
|
|
18
|
+
marker.writeText("{\"bundleId\":\"crashed-bundle\",\"shouldRollback\":")
|
|
19
|
+
|
|
20
|
+
assertNull(PendingCrashRecovery.loadFromFile(marker))
|
|
21
|
+
|
|
22
|
+
marker.appendText("true}\n")
|
|
23
|
+
assertEquals(
|
|
24
|
+
PendingCrashRecovery("crashed-bundle", true),
|
|
25
|
+
PendingCrashRecovery.loadFromFile(marker),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `missing or empty markers do not request a restart`() {
|
|
31
|
+
assertNull(PendingCrashRecovery.loadFromFile(File(temporaryFolder.root, "missing.json")))
|
|
32
|
+
assertNull(PendingCrashRecovery.loadFromFile(temporaryFolder.newFile("empty.json")))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `a crash after content appeared does not request rollback`() {
|
|
37
|
+
val marker = temporaryFolder.newFile("verified-crash.json")
|
|
38
|
+
marker.writeText("{\"bundleId\":\"verified-bundle\",\"shouldRollback\":false}")
|
|
39
|
+
|
|
40
|
+
val recovery = requireNotNull(PendingCrashRecovery.loadFromFile(marker))
|
|
41
|
+
assertFalse(recovery.shouldRollback)
|
|
42
|
+
assertEquals("verified-bundle", recovery.launchedBundleId)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|