@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.
Files changed (227) hide show
  1. package/android/proguard-rules.pro +17 -2
  2. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +651 -637
  4. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +10 -0
  5. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
  6. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
  7. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  8. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  9. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
  10. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  11. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  12. package/android/src/newarch/HotUpdater.kt +7 -9
  13. package/android/src/newarch/HotUpdaterModule.kt +40 -20
  14. package/android/src/oldarch/HotUpdater.kt +7 -9
  15. package/android/src/oldarch/HotUpdaterModule.kt +40 -20
  16. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
  17. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
  18. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
  19. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
  20. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
  21. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  22. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  23. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
  24. package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
  25. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  26. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  27. package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
  28. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +149 -51
  29. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  30. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
  32. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  33. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  34. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  35. package/lib/commonjs/checkForUpdate.js +32 -23
  36. package/lib/commonjs/checkForUpdate.js.map +1 -1
  37. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +97 -28
  38. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  39. package/lib/commonjs/error.js +4 -13
  40. package/lib/commonjs/error.js.map +1 -1
  41. package/lib/commonjs/fetchJSON.js +9 -2
  42. package/lib/commonjs/fetchJSON.js.map +1 -1
  43. package/lib/commonjs/httpClient.js +46 -29
  44. package/lib/commonjs/httpClient.js.map +1 -1
  45. package/lib/commonjs/httpClient.spec.js +57 -5
  46. package/lib/commonjs/httpClient.spec.js.map +1 -1
  47. package/lib/commonjs/index.js +31 -14
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/index.spec.js +56 -11
  50. package/lib/commonjs/index.spec.js.map +1 -1
  51. package/lib/commonjs/native.js +15 -14
  52. package/lib/commonjs/native.js.map +1 -1
  53. package/lib/commonjs/native.spec.js +128 -22
  54. package/lib/commonjs/native.spec.js.map +1 -1
  55. package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +28 -21
  56. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  57. package/lib/commonjs/notifyAppReadyInsights.js +188 -0
  58. package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
  59. package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +161 -29
  60. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
  61. package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  62. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
  63. package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +14 -7
  64. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
  65. package/lib/commonjs/sdkVersion.js +1 -1
  66. package/lib/commonjs/sdkVersion.js.map +1 -1
  67. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  68. package/lib/commonjs/store.js +3 -10
  69. package/lib/commonjs/store.js.map +1 -1
  70. package/lib/commonjs/store.notification.spec.js +12 -4
  71. package/lib/commonjs/store.notification.spec.js.map +1 -1
  72. package/lib/commonjs/store.spec.js +2 -136
  73. package/lib/commonjs/store.spec.js.map +1 -1
  74. package/lib/commonjs/wrap.js +11 -12
  75. package/lib/commonjs/wrap.js.map +1 -1
  76. package/lib/commonjs/wrap.spec.js +97 -25
  77. package/lib/commonjs/wrap.spec.js.map +1 -1
  78. package/lib/module/checkForUpdate.js +32 -23
  79. package/lib/module/checkForUpdate.js.map +1 -1
  80. package/lib/module/checkForUpdate.releaseCatalog.spec.js +97 -28
  81. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  82. package/lib/module/error.js +4 -13
  83. package/lib/module/error.js.map +1 -1
  84. package/lib/module/fetchJSON.js +7 -1
  85. package/lib/module/fetchJSON.js.map +1 -1
  86. package/lib/module/httpClient.js +48 -31
  87. package/lib/module/httpClient.js.map +1 -1
  88. package/lib/module/httpClient.spec.js +57 -5
  89. package/lib/module/httpClient.spec.js.map +1 -1
  90. package/lib/module/index.js +32 -15
  91. package/lib/module/index.js.map +1 -1
  92. package/lib/module/index.spec.js +56 -11
  93. package/lib/module/index.spec.js.map +1 -1
  94. package/lib/module/native.js +13 -12
  95. package/lib/module/native.js.map +1 -1
  96. package/lib/module/native.spec.js +128 -22
  97. package/lib/module/native.spec.js.map +1 -1
  98. package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +23 -16
  99. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  100. package/lib/module/notifyAppReadyInsights.js +181 -0
  101. package/lib/module/notifyAppReadyInsights.js.map +1 -0
  102. package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +153 -21
  103. package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
  104. package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  105. package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
  106. package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +11 -4
  107. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
  108. package/lib/module/sdkVersion.js +1 -1
  109. package/lib/module/sdkVersion.js.map +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/module/store.js +3 -10
  112. package/lib/module/store.js.map +1 -1
  113. package/lib/module/store.notification.spec.js +12 -4
  114. package/lib/module/store.notification.spec.js.map +1 -1
  115. package/lib/module/store.spec.js +2 -136
  116. package/lib/module/store.spec.js.map +1 -1
  117. package/lib/module/wrap.js +11 -12
  118. package/lib/module/wrap.js.map +1 -1
  119. package/lib/module/wrap.spec.js +99 -25
  120. package/lib/module/wrap.spec.js.map +1 -1
  121. package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -3
  122. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/error.d.ts +4 -13
  124. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
  126. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/httpClient.d.ts +6 -6
  128. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/index.d.ts +4 -7
  130. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/native.d.ts +5 -9
  132. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
  134. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
  136. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  138. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
  140. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/store.d.ts +1 -3
  142. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/wrap.types.d.ts +5 -8
  144. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  145. package/lib/typescript/module/checkForUpdate.d.ts +2 -3
  146. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  147. package/lib/typescript/module/error.d.ts +4 -13
  148. package/lib/typescript/module/error.d.ts.map +1 -1
  149. package/lib/typescript/module/fetchJSON.d.ts +4 -0
  150. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  151. package/lib/typescript/module/httpClient.d.ts +6 -6
  152. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  153. package/lib/typescript/module/index.d.ts +4 -7
  154. package/lib/typescript/module/index.d.ts.map +1 -1
  155. package/lib/typescript/module/native.d.ts +5 -9
  156. package/lib/typescript/module/native.d.ts.map +1 -1
  157. package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
  158. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
  159. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
  160. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  161. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  162. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  163. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
  164. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  165. package/lib/typescript/module/store.d.ts +1 -3
  166. package/lib/typescript/module/store.d.ts.map +1 -1
  167. package/lib/typescript/module/wrap.types.d.ts +5 -8
  168. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  169. package/package.json +3 -3
  170. package/src/checkForUpdate.releaseCatalog.spec.ts +137 -61
  171. package/src/checkForUpdate.ts +35 -29
  172. package/src/error.ts +4 -14
  173. package/src/fetchJSON.ts +12 -1
  174. package/src/httpClient.spec.ts +70 -5
  175. package/src/httpClient.ts +78 -64
  176. package/src/index.spec.ts +59 -11
  177. package/src/index.ts +41 -17
  178. package/src/native.spec.ts +133 -22
  179. package/src/native.ts +21 -32
  180. package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +25 -17
  181. package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +141 -22
  182. package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
  183. package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +13 -5
  184. package/src/notifyAppReadyInsights.ts +293 -0
  185. package/src/sdkVersion.ts +1 -1
  186. package/src/specs/NativeHotUpdater.ts +8 -36
  187. package/src/store.notification.spec.ts +13 -3
  188. package/src/store.spec.ts +0 -157
  189. package/src/store.ts +2 -23
  190. package/src/wrap.spec.ts +109 -31
  191. package/src/wrap.tsx +11 -11
  192. package/src/wrap.types.ts +5 -8
  193. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  194. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  195. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  196. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  197. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  198. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  199. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  200. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  201. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  202. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  203. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  204. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  205. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  206. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  207. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  208. package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
  209. package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
  210. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
  211. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  212. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  213. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  214. package/lib/module/notifyAppReadyAnalytics.js +0 -128
  215. package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
  216. package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
  217. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  218. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  219. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
  220. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
  221. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  222. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  223. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
  224. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
  225. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  226. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  227. 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
- fileUrl: String?,
220
- fileHash: String?,
221
- manifestUrl: String?,
222
- manifestFileHash: String?,
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
+ }