@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
package/src/wrap.tsx CHANGED
@@ -2,8 +2,8 @@ import React, { useEffect, useState } from "react";
2
2
 
3
3
  import { checkForUpdate } from "./checkForUpdate";
4
4
  import { useEventCallback } from "./hooks/useEventCallback";
5
- import { getBundleId, reload } from "./native";
6
- import { handleNotifyAppReady } from "./notifyAppReadyAnalytics";
5
+ import { getUpdateId, reload } from "./native";
6
+ import { handleNotifyAppReady, reportNoChange } from "./notifyAppReadyInsights";
7
7
  import { useHotUpdaterStore } from "./store";
8
8
  import type {
9
9
  InternalInitOptions,
@@ -45,8 +45,12 @@ export function wrap(
45
45
  try {
46
46
  setUpdateStatus("CHECK_FOR_UPDATE");
47
47
 
48
+ const readiness = handleNotifyAppReady({
49
+ ...restOptions,
50
+ reportUnchanged: false,
51
+ });
48
52
  const updateInfo = await checkForUpdate({
49
- analytics: restOptions.analytics,
53
+ insights: restOptions.insights,
50
54
  client: restOptions.client,
51
55
  updateStrategy: restOptions.updateStrategy,
52
56
  requestHeaders: restOptions.requestHeaders,
@@ -54,14 +58,17 @@ export function wrap(
54
58
  onError: restOptions.onError,
55
59
  });
56
60
 
61
+ const launch = await readiness;
57
62
  setMessage(updateInfo?.message ?? null);
58
63
 
59
64
  if (!updateInfo) {
65
+ if (launch?.status === "UNCHANGED")
66
+ void reportNoChange(restOptions);
60
67
  restOptions.onUpdateProcessCompleted?.({
61
68
  status: "UP_TO_DATE",
62
69
  shouldForceUpdate: false,
63
70
  message: null,
64
- id: getBundleId(),
71
+ id: getUpdateId(),
65
72
  });
66
73
  setUpdateStatus("UPDATE_PROCESS_COMPLETED");
67
74
  return;
@@ -115,11 +122,6 @@ export function wrap(
115
122
  restOptions.onProgress?.(progress);
116
123
  }, [progress]);
117
124
 
118
- // Read the native launch report after the first render commit.
119
- useEffect(() => {
120
- void handleNotifyAppReady(restOptions);
121
- }, []);
122
-
123
125
  // Start update check
124
126
  useEffect(() => {
125
127
  initHotUpdater();
@@ -134,11 +136,9 @@ export function wrap(
134
136
  <Fallback
135
137
  artifactType={progressState.artifactType}
136
138
  details={progressState.details}
137
- downloadedBytes={progressState.downloadedBytes}
138
139
  progress={progress}
139
140
  status={updateStatus}
140
141
  message={message}
141
- totalBytes={progressState.totalBytes}
142
142
  />
143
143
  );
144
144
  }
package/src/wrap.types.ts CHANGED
@@ -21,8 +21,6 @@ export type UpdateStatus =
21
21
  export type HotUpdaterFallbackComponentProps = {
22
22
  status: Exclude<UpdateStatus, "UPDATE_PROCESS_COMPLETED">;
23
23
  progress: number;
24
- downloadedBytes: HotUpdaterState["downloadedBytes"];
25
- totalBytes: HotUpdaterState["totalBytes"];
26
24
  message: string | null;
27
25
  artifactType: HotUpdaterState["artifactType"];
28
26
  details: HotUpdaterState["details"];
@@ -31,9 +29,10 @@ export type HotUpdaterFallbackComponentProps = {
31
29
  interface CommonHotUpdaterOptions {
32
30
  /**
33
31
  * Sends app-ready transitions and same-Bundle Release adoptions to the
34
- * configured server. Omit or set to `false` to disable client reporting.
32
+ * configured server. Defaults to `true`; set to `false` to disable client
33
+ * reporting.
35
34
  */
36
- analytics?: boolean;
35
+ insights?: boolean;
37
36
  /** Base URL of a server exposing the Hot Updater v1 client HTTP protocol. */
38
37
  baseURL: HotUpdaterBaseURL;
39
38
  requestHeaders?: Record<string, string>;
@@ -55,7 +54,7 @@ export type HotUpdaterInitOptions = CommonHotUpdaterOptions;
55
54
  export type HotUpdaterOptions = AutoUpdateOptions;
56
55
 
57
56
  type InternalCommonOptions = {
58
- analytics?: boolean;
57
+ insights?: boolean;
59
58
  client: HotUpdaterHttpClient;
60
59
  requestHeaders?: Record<string, string>;
61
60
  requestTimeout?: number;
@@ -71,8 +70,6 @@ type InternalAutoUpdateOptions = InternalCommonOptions & {
71
70
  onUpdateProcessCompleted?: (response: RunUpdateProcessResponse) => void;
72
71
  };
73
72
 
74
- export type InternalInitOptions = InternalCommonOptions & {
75
- analytics?: boolean;
76
- };
73
+ export type InternalInitOptions = InternalCommonOptions;
77
74
 
78
75
  export type InternalWrapOptions = InternalAutoUpdateOptions;
@@ -1,89 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.util.Log
4
- import java.io.File
5
-
6
- /**
7
- * Unified decompression service that uses Strategy pattern to handle multiple compression formats.
8
- * Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
9
- */
10
- class DecompressService {
11
- companion object {
12
- private const val TAG = "DecompressService"
13
- }
14
-
15
- // Strategies with reliable file signatures that can be validated cheaply.
16
- // TAR.BR is attempted only as the final fallback because Brotli has no reliable magic bytes.
17
- private val signatureStrategies =
18
- listOf(
19
- ZipDecompressionStrategy(),
20
- TarGzDecompressionStrategy(),
21
- )
22
- private val tarBrStrategy = TarBrDecompressionStrategy()
23
-
24
- /**
25
- * Extracts a compressed file to the destination directory.
26
- * Automatically detects compression format by trying each strategy's validation.
27
- * @param filePath Path to the compressed file
28
- * @param destinationPath Path to the destination directory
29
- * @param progressCallback Callback for progress updates (0.0 - 1.0)
30
- * @return true if extraction was successful, false otherwise
31
- */
32
- fun extractZipFile(
33
- filePath: String,
34
- destinationPath: String,
35
- progressCallback: (Double) -> Unit,
36
- ): Boolean {
37
- // Collect file information for better error messages
38
- val file = File(filePath)
39
- val fileName = file.name
40
- val fileSize = if (file.exists()) file.length() else 0L
41
-
42
- // Try each signature-based strategy first.
43
- for (strategy in signatureStrategies) {
44
- if (strategy.isValid(filePath)) {
45
- Log.d(TAG, "Using strategy for $fileName")
46
- return strategy.decompress(filePath, destinationPath, progressCallback)
47
- }
48
- }
49
-
50
- Log.d(TAG, "No ZIP/TAR.GZ signature matched for $fileName, trying TAR.BR fallback")
51
-
52
- if (tarBrStrategy.decompress(filePath, destinationPath, progressCallback)) {
53
- Log.d(TAG, "Using TAR.BR fallback for $fileName")
54
- return true
55
- }
56
-
57
- val errorMessage = createInvalidArchiveMessage(fileName, fileSize)
58
- Log.e(TAG, errorMessage)
59
- return false
60
- }
61
-
62
- /**
63
- * Validates if a file matches one of the signature-based archive formats.
64
- * @param filePath Path to the file to validate
65
- * @return true if the file is a valid compressed archive
66
- */
67
- fun isValidZipFile(filePath: String): Boolean {
68
- for (strategy in signatureStrategies) {
69
- if (strategy.isValid(filePath)) {
70
- return true
71
- }
72
- }
73
- Log.d(TAG, "No ZIP/TAR.GZ signature matched for file: $filePath. TAR.BR is handled during extraction fallback.")
74
- return false
75
- }
76
-
77
- private fun createInvalidArchiveMessage(
78
- fileName: String,
79
- fileSize: Long,
80
- ): String =
81
- """
82
- The downloaded bundle file is not a valid compressed archive: $fileName ($fileSize bytes)
83
-
84
- Supported formats:
85
- - ZIP archives (.zip)
86
- - GZIP compressed TAR archives (.tar.gz)
87
- - Brotli compressed TAR archives (.tar.br)
88
- """.trimIndent()
89
- }
@@ -1,26 +0,0 @@
1
- package com.hotupdater
2
-
3
- /**
4
- * Interface for decompression strategies
5
- */
6
- interface DecompressionStrategy {
7
- /**
8
- * Validates if a file can be decompressed by this strategy
9
- * @param filePath Path to the file to validate
10
- * @return true if the file is valid for this strategy
11
- */
12
- fun isValid(filePath: String): Boolean
13
-
14
- /**
15
- * Decompresses a file to the destination directory
16
- * @param filePath Path to the compressed file
17
- * @param destinationPath Path to the destination directory
18
- * @param progressCallback Callback for progress updates (0.0 - 1.0)
19
- * @return true if decompression was successful, false otherwise
20
- */
21
- fun decompress(
22
- filePath: String,
23
- destinationPath: String,
24
- progressCallback: (Double) -> Unit,
25
- ): Boolean
26
- }
@@ -1,117 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.util.Log
4
- import com.hotupdater.vendor.brotli.dec.BrotliInputStream
5
- import java.io.BufferedInputStream
6
- import java.io.File
7
- import java.io.FileInputStream
8
- import java.io.FileOutputStream
9
-
10
- /**
11
- * Strategy for handling TAR+Brotli compressed files
12
- * Uses native Brotli decoder and custom TAR parser
13
- */
14
- class TarBrDecompressionStrategy : DecompressionStrategy {
15
- companion object {
16
- private const val TAG = "TarBrStrategy"
17
- private const val MIN_FILE_SIZE = 10L
18
- }
19
-
20
- override fun isValid(filePath: String): Boolean {
21
- val file = File(filePath)
22
-
23
- if (!file.exists() || file.length() < MIN_FILE_SIZE) {
24
- Log.d(TAG, "Invalid file: doesn't exist or too small (${file.length()} bytes)")
25
- return false
26
- }
27
-
28
- return try {
29
- FileInputStream(file).use { fileInputStream ->
30
- BufferedInputStream(fileInputStream).use { bufferedInputStream ->
31
- BrotliInputStream(bufferedInputStream).use { brotliInputStream ->
32
- TarArchiveInputStream(brotliInputStream).use { tarInputStream ->
33
- val firstEntry = tarInputStream.getNextEntry()
34
- val isValid = firstEntry != null
35
-
36
- if (!isValid) {
37
- Log.d(TAG, "Invalid file: tar archive has no entries")
38
- }
39
-
40
- isValid
41
- }
42
- }
43
- }
44
- }
45
- } catch (e: Exception) {
46
- Log.d(TAG, "Invalid file: Brotli/TAR validation failed - ${e.message}")
47
- false
48
- }
49
- }
50
-
51
- override fun decompress(
52
- filePath: String,
53
- destinationPath: String,
54
- progressCallback: (Double) -> Unit,
55
- ): Boolean =
56
- try {
57
- val destinationDir = File(destinationPath)
58
- if (!destinationDir.exists()) {
59
- destinationDir.mkdirs()
60
- }
61
-
62
- val sourceFile = File(filePath)
63
- val totalSize = sourceFile.length()
64
- var processedBytes = 0L
65
-
66
- Log.d(TAG, "Extracting tar.br file: $filePath")
67
-
68
- FileInputStream(filePath).use { fileInputStream ->
69
- BufferedInputStream(fileInputStream).use { bufferedInputStream ->
70
- BrotliInputStream(bufferedInputStream).use { brotliInputStream ->
71
- TarArchiveInputStream(brotliInputStream).use { tarInputStream ->
72
- var entry = tarInputStream.getNextEntry()
73
-
74
- while (entry != null) {
75
- val file = RelativePathResolver.resolveInside(destinationDir, entry.name)
76
-
77
- if (file == null) {
78
- Log.w(TAG, "Skipping potentially malicious tar entry: ${entry.name}")
79
- entry = tarInputStream.getNextEntry()
80
- continue
81
- }
82
-
83
- if (entry.isDirectory) {
84
- file.mkdirs()
85
- } else {
86
- file.parentFile?.mkdirs()
87
-
88
- FileOutputStream(file).use { output ->
89
- val buffer = ByteArray(8 * 1024)
90
- var bytesRead: Int
91
-
92
- while (tarInputStream.read(buffer).also { bytesRead = it } != -1) {
93
- output.write(buffer, 0, bytesRead)
94
- processedBytes += bytesRead
95
- }
96
- }
97
- }
98
-
99
- val progress = processedBytes.toDouble() / (totalSize * 2.0)
100
- progressCallback.invoke(progress.coerceIn(0.0, 1.0))
101
-
102
- entry = tarInputStream.getNextEntry()
103
- }
104
- }
105
- }
106
- }
107
- }
108
-
109
- Log.d(TAG, "Successfully extracted tar.br file")
110
- progressCallback.invoke(1.0)
111
- true
112
- } catch (e: Exception) {
113
- Log.d(TAG, "Failed to extract tar.br file: ${e.message}")
114
- e.printStackTrace()
115
- false
116
- }
117
- }
@@ -1,117 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.util.Log
4
- import java.io.BufferedInputStream
5
- import java.io.File
6
- import java.io.FileInputStream
7
- import java.io.FileOutputStream
8
- import java.util.zip.GZIPInputStream
9
-
10
- /**
11
- * Strategy for handling TAR+GZIP compressed files
12
- * Uses native GZIP decoder and custom TAR parser
13
- */
14
- class TarGzDecompressionStrategy : DecompressionStrategy {
15
- companion object {
16
- private const val TAG = "TarGzStrategy"
17
- private const val MIN_FILE_SIZE = 10L
18
- }
19
-
20
- override fun isValid(filePath: String): Boolean {
21
- val file = File(filePath)
22
-
23
- if (!file.exists() || file.length() < MIN_FILE_SIZE) {
24
- Log.d(TAG, "Invalid file: doesn't exist or too small (${file.length()} bytes)")
25
- return false
26
- }
27
-
28
- try {
29
- FileInputStream(file).use { fis ->
30
- val header = ByteArray(2)
31
- if (fis.read(header) != 2) {
32
- Log.d(TAG, "Invalid file: cannot read header")
33
- return false
34
- }
35
-
36
- // Check GZIP magic bytes (0x1F 0x8B)
37
- val isGzip = header[0] == 0x1F.toByte() && header[1] == 0x8B.toByte()
38
- if (!isGzip) {
39
- Log.d(
40
- TAG,
41
- "Invalid file: wrong magic bytes (expected 0x1F 0x8B, got 0x${header[0].toString(16)} 0x${header[1].toString(16)})",
42
- )
43
- }
44
- return isGzip
45
- }
46
- } catch (e: Exception) {
47
- Log.d(TAG, "Invalid file: error reading header: ${e.message}")
48
- return false
49
- }
50
- }
51
-
52
- override fun decompress(
53
- filePath: String,
54
- destinationPath: String,
55
- progressCallback: (Double) -> Unit,
56
- ): Boolean =
57
- try {
58
- val destinationDir = File(destinationPath)
59
- if (!destinationDir.exists()) {
60
- destinationDir.mkdirs()
61
- }
62
-
63
- val sourceFile = File(filePath)
64
- val totalSize = sourceFile.length()
65
- var processedBytes = 0L
66
-
67
- Log.d(TAG, "Extracting tar.gz file: $filePath")
68
-
69
- FileInputStream(filePath).use { fileInputStream ->
70
- BufferedInputStream(fileInputStream).use { bufferedInputStream ->
71
- GZIPInputStream(bufferedInputStream).use { gzipInputStream ->
72
- TarArchiveInputStream(gzipInputStream).use { tarInputStream ->
73
- var entry = tarInputStream.getNextEntry()
74
-
75
- while (entry != null) {
76
- val file = RelativePathResolver.resolveInside(destinationDir, entry.name)
77
-
78
- if (file == null) {
79
- Log.w(TAG, "Skipping potentially malicious tar entry: ${entry.name}")
80
- entry = tarInputStream.getNextEntry()
81
- continue
82
- }
83
-
84
- if (entry.isDirectory) {
85
- file.mkdirs()
86
- } else {
87
- file.parentFile?.mkdirs()
88
-
89
- FileOutputStream(file).use { output ->
90
- val buffer = ByteArray(8 * 1024)
91
- var bytesRead: Int
92
-
93
- while (tarInputStream.read(buffer).also { bytesRead = it } != -1) {
94
- output.write(buffer, 0, bytesRead)
95
- processedBytes += bytesRead
96
- }
97
- }
98
- }
99
-
100
- val progress = processedBytes.toDouble() / (totalSize * 2.0)
101
- progressCallback.invoke(progress.coerceIn(0.0, 1.0))
102
-
103
- entry = tarInputStream.getNextEntry()
104
- }
105
- }
106
- }
107
- }
108
- }
109
-
110
- Log.d(TAG, "Successfully extracted tar.gz file")
111
- progressCallback.invoke(1.0)
112
- true
113
- } catch (e: Exception) {
114
- Log.e(TAG, "Error extracting tar.gz file: ${e.message}", e)
115
- false
116
- }
117
- }
@@ -1,192 +0,0 @@
1
- package com.hotupdater
2
-
3
- import android.util.Log
4
- import java.io.BufferedInputStream
5
- import java.io.File
6
- import java.io.FileInputStream
7
- import java.io.FileOutputStream
8
- import java.io.IOException
9
- import java.util.zip.CRC32
10
- import java.util.zip.ZipEntry
11
- import java.util.zip.ZipException
12
- import java.util.zip.ZipFile
13
- import java.util.zip.ZipInputStream
14
-
15
- /**
16
- * Strategy for handling ZIP compressed files
17
- */
18
- class ZipDecompressionStrategy : DecompressionStrategy {
19
- companion object {
20
- private const val TAG = "ZipStrategy"
21
- private const val MIN_ZIP_SIZE = 22L
22
- }
23
-
24
- override fun isValid(filePath: String): Boolean {
25
- val file = File(filePath)
26
-
27
- if (!file.exists() || file.length() < MIN_ZIP_SIZE) {
28
- Log.d(TAG, "Invalid ZIP: file doesn't exist or too small (${file.length()} bytes)")
29
- return false
30
- }
31
-
32
- try {
33
- FileInputStream(file).use { fis ->
34
- val header = ByteArray(4)
35
- if (fis.read(header) != 4) {
36
- Log.d(TAG, "Invalid ZIP: cannot read header")
37
- return false
38
- }
39
-
40
- // ZIP magic bytes: 0x50 0x4B 0x03 0x04 ("PK\u0003\u0004")
41
- val expectedMagic = byteArrayOf(0x50.toByte(), 0x4B.toByte(), 0x03.toByte(), 0x04.toByte())
42
-
43
- if (!header.contentEquals(expectedMagic)) {
44
- val headerHex = header.joinToString(" ") { "0x%02X".format(it) }
45
- Log.d(TAG, "Invalid ZIP: wrong magic bytes (expected 0x50 0x4B 0x03 0x04, got $headerHex)")
46
- return false
47
- }
48
- }
49
- } catch (e: Exception) {
50
- Log.d(TAG, "Invalid ZIP: error reading file: ${e.message}")
51
- return false
52
- }
53
-
54
- try {
55
- ZipFile(file).use { zipFile ->
56
- val entries = zipFile.entries()
57
- if (!entries.hasMoreElements()) {
58
- Log.d(TAG, "Invalid ZIP: no entries found")
59
- return false
60
- }
61
-
62
- val firstEntry = entries.nextElement()
63
- zipFile.getInputStream(firstEntry).use { stream ->
64
- val buffer = ByteArray(1024)
65
- stream.read(buffer)
66
- }
67
- }
68
- return true
69
- } catch (e: ZipException) {
70
- Log.d(TAG, "Invalid ZIP: ZIP structure error: ${e.message}")
71
- return false
72
- } catch (e: Exception) {
73
- Log.d(TAG, "Invalid ZIP: validation error: ${e.message}")
74
- return false
75
- }
76
- }
77
-
78
- override fun decompress(
79
- filePath: String,
80
- destinationPath: String,
81
- progressCallback: (Double) -> Unit,
82
- ): Boolean {
83
- return try {
84
- val destinationDir = File(destinationPath)
85
- if (!destinationDir.exists()) {
86
- destinationDir.mkdirs()
87
- }
88
-
89
- // Calculate total bytes to extract for accurate progress reporting
90
- val totalBytes =
91
- try {
92
- ZipFile(File(filePath)).use { zipFile ->
93
- zipFile
94
- .entries()
95
- .asSequence()
96
- .filter { !it.isDirectory }
97
- .sumOf { it.size }
98
- }
99
- } catch (e: Exception) {
100
- Log.d(TAG, "Failed to calculate total bytes: ${e.message}")
101
- 0L
102
- }
103
-
104
- if (totalBytes == 0L) {
105
- Log.d(TAG, "No content found in ZIP")
106
- return false
107
- }
108
-
109
- Log.d(TAG, "Extracting $totalBytes bytes from ZIP")
110
-
111
- var extractedFileCount = 0
112
- var extractedBytes = 0L
113
- var lastReportedProgress = 0.0
114
-
115
- FileInputStream(filePath).use { fileInputStream ->
116
- BufferedInputStream(fileInputStream).use { bufferedInputStream ->
117
- ZipInputStream(bufferedInputStream).use { zipInputStream ->
118
- var entry: ZipEntry? = zipInputStream.nextEntry
119
- while (entry != null) {
120
- val file =
121
- try {
122
- RelativePathResolver.resolveInside(destinationDir, entry.name)
123
- } catch (e: IOException) {
124
- Log.w(TAG, "Failed to resolve canonical path for zip entry: ${entry.name}", e)
125
- null
126
- }
127
-
128
- if (file == null) {
129
- Log.w(TAG, "Skipping potentially malicious zip entry: ${entry.name}")
130
- entry = zipInputStream.nextEntry
131
- continue
132
- }
133
-
134
- if (entry.isDirectory) {
135
- file.mkdirs()
136
- } else {
137
- file.parentFile?.mkdirs()
138
-
139
- val crc = CRC32()
140
- FileOutputStream(file).use { output ->
141
- val buffer = ByteArray(8 * 1024)
142
- var bytesRead: Int
143
-
144
- while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
145
- output.write(buffer, 0, bytesRead)
146
- crc.update(buffer, 0, bytesRead)
147
-
148
- // Track bytes written for byte-based progress
149
- extractedBytes += bytesRead
150
-
151
- // Report progress more frequently (every 1%)
152
- val currentProgress = extractedBytes.toDouble() / totalBytes
153
- if (currentProgress - lastReportedProgress >= 0.01) {
154
- progressCallback.invoke(currentProgress)
155
- lastReportedProgress = currentProgress
156
- }
157
- }
158
- }
159
-
160
- if (entry.crc != -1L && crc.value != entry.crc) {
161
- Log.w(TAG, "CRC mismatch for ${entry.name}: expected ${entry.crc}, got ${crc.value}")
162
- file.delete()
163
- return false
164
- }
165
-
166
- extractedFileCount++
167
- }
168
-
169
- zipInputStream.closeEntry()
170
- entry = zipInputStream.nextEntry
171
- }
172
- }
173
- }
174
- }
175
-
176
- if (extractedFileCount == 0) {
177
- Log.d(TAG, "No files extracted from ZIP")
178
- return false
179
- }
180
-
181
- Log.d(TAG, "Successfully extracted $extractedFileCount files ($extractedBytes bytes)")
182
- progressCallback.invoke(1.0)
183
- true
184
- } catch (e: ZipException) {
185
- Log.d(TAG, "ZIP extraction failed: ${e.message}")
186
- false
187
- } catch (e: Exception) {
188
- Log.d(TAG, "Failed to unzip file: ${e.message}")
189
- false
190
- }
191
- }
192
- }