react-native-instantpay-code-push 1.1.0

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 (87) hide show
  1. package/InstantpayCodePush.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +91 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/instantpaycodepush/BundleFileStorageService.kt +835 -0
  8. package/android/src/main/java/com/instantpaycodepush/BundleMetadata.kt +249 -0
  9. package/android/src/main/java/com/instantpaycodepush/CommonHelper.kt +39 -0
  10. package/android/src/main/java/com/instantpaycodepush/DecompressService.kt +85 -0
  11. package/android/src/main/java/com/instantpaycodepush/DecompressionStrategy.kt +24 -0
  12. package/android/src/main/java/com/instantpaycodepush/FileManagerService.kt +105 -0
  13. package/android/src/main/java/com/instantpaycodepush/HashUtils.kt +50 -0
  14. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushModule.kt +182 -0
  15. package/android/src/main/java/com/instantpaycodepush/InstantpayCodePushPackage.kt +33 -0
  16. package/android/src/main/java/com/instantpaycodepush/IpayCodePush.kt +101 -0
  17. package/android/src/main/java/com/instantpaycodepush/IpayCodePushException.kt +135 -0
  18. package/android/src/main/java/com/instantpaycodepush/IpayCodePushImpl.kt +329 -0
  19. package/android/src/main/java/com/instantpaycodepush/OkHttpDownloadService.kt +283 -0
  20. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManager.kt +141 -0
  21. package/android/src/main/java/com/instantpaycodepush/ReactIntegrationManagerBase.kt +35 -0
  22. package/android/src/main/java/com/instantpaycodepush/SignatureVerifier.kt +354 -0
  23. package/android/src/main/java/com/instantpaycodepush/VersionedPreferencesService.kt +70 -0
  24. package/android/src/main/java/com/instantpaycodepush/ZipDecompressionStrategy.kt +198 -0
  25. package/ios/InstantpayCodePush.h +5 -0
  26. package/ios/InstantpayCodePush.mm +21 -0
  27. package/lib/module/DefaultResolver.js +34 -0
  28. package/lib/module/DefaultResolver.js.map +1 -0
  29. package/lib/module/NativeInstantpayCodePush.js +5 -0
  30. package/lib/module/NativeInstantpayCodePush.js.map +1 -0
  31. package/lib/module/checkForUpdate.js +68 -0
  32. package/lib/module/checkForUpdate.js.map +1 -0
  33. package/lib/module/error.js +137 -0
  34. package/lib/module/error.js.map +1 -0
  35. package/lib/module/fetchUpdateInfo.js +36 -0
  36. package/lib/module/fetchUpdateInfo.js.map +1 -0
  37. package/lib/module/global.d.js +8 -0
  38. package/lib/module/global.d.js.map +1 -0
  39. package/lib/module/hooks/useEventCallback.js +13 -0
  40. package/lib/module/hooks/useEventCallback.js.map +1 -0
  41. package/lib/module/index.js +291 -0
  42. package/lib/module/index.js.map +1 -0
  43. package/lib/module/native.js +233 -0
  44. package/lib/module/native.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/store.js +53 -0
  47. package/lib/module/store.js.map +1 -0
  48. package/lib/module/types.js +62 -0
  49. package/lib/module/types.js.map +1 -0
  50. package/lib/module/wrap.js +171 -0
  51. package/lib/module/wrap.js.map +1 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/DefaultResolver.d.ts +10 -0
  54. package/lib/typescript/src/DefaultResolver.d.ts.map +1 -0
  55. package/lib/typescript/src/NativeInstantpayCodePush.d.ts +100 -0
  56. package/lib/typescript/src/NativeInstantpayCodePush.d.ts.map +1 -0
  57. package/lib/typescript/src/checkForUpdate.d.ts +29 -0
  58. package/lib/typescript/src/checkForUpdate.d.ts.map +1 -0
  59. package/lib/typescript/src/error.d.ts +124 -0
  60. package/lib/typescript/src/error.d.ts.map +1 -0
  61. package/lib/typescript/src/fetchUpdateInfo.d.ts +8 -0
  62. package/lib/typescript/src/fetchUpdateInfo.d.ts.map +1 -0
  63. package/lib/typescript/src/hooks/useEventCallback.d.ts +5 -0
  64. package/lib/typescript/src/hooks/useEventCallback.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +203 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/native.d.ts +128 -0
  68. package/lib/typescript/src/native.d.ts.map +1 -0
  69. package/lib/typescript/src/store.d.ts +11 -0
  70. package/lib/typescript/src/store.d.ts.map +1 -0
  71. package/lib/typescript/src/types.d.ts +174 -0
  72. package/lib/typescript/src/types.d.ts.map +1 -0
  73. package/lib/typescript/src/wrap.d.ts +179 -0
  74. package/lib/typescript/src/wrap.d.ts.map +1 -0
  75. package/package.json +174 -0
  76. package/src/DefaultResolver.ts +36 -0
  77. package/src/NativeInstantpayCodePush.ts +111 -0
  78. package/src/checkForUpdate.ts +122 -0
  79. package/src/error.ts +159 -0
  80. package/src/fetchUpdateInfo.ts +47 -0
  81. package/src/global.d.ts +23 -0
  82. package/src/hooks/useEventCallback.ts +30 -0
  83. package/src/index.tsx +379 -0
  84. package/src/native.ts +280 -0
  85. package/src/store.ts +69 -0
  86. package/src/types.ts +227 -0
  87. package/src/wrap.tsx +384 -0
@@ -0,0 +1,249 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.util.Log
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+ import java.io.File
7
+
8
+
9
+ /**
10
+ * Bundle metadata for managing stable/staging bundles and verification state
11
+ */
12
+ data class BundleMetadata(
13
+ val schema: String = SCHEMA_VERSION,
14
+ val isolationKey: String? = null,
15
+ val stableBundleId: String? = null,
16
+ val stagingBundleId: String? = null,
17
+ val verificationPending: Boolean = false,
18
+ val verificationAttemptedAt: Long? = null,
19
+ val stagingExecutionCount: Int? = null,
20
+ val updatedAt: Long = System.currentTimeMillis(),
21
+ ){
22
+
23
+ companion object {
24
+
25
+ private const val CLASS_TAG = "*BundleMetadata"
26
+ const val SCHEMA_VERSION = "metadata-v1"
27
+ const val METADATA_FILENAME = "metadata.json"
28
+
29
+ fun fromJson(json: JSONObject): BundleMetadata =
30
+ BundleMetadata(
31
+ schema = json.optString("schema", SCHEMA_VERSION),
32
+ isolationKey =
33
+ if (json.has("isolationKey") && !json.isNull("isolationKey")) {
34
+ json.getString("isolationKey").takeIf { it.isNotEmpty() }
35
+ } else {
36
+ null
37
+ },
38
+ stableBundleId =
39
+ if (json.has("stableBundleId") && !json.isNull("stableBundleId")) {
40
+ json.getString("stableBundleId").takeIf { it.isNotEmpty() }
41
+ } else {
42
+ null
43
+ },
44
+ stagingBundleId =
45
+ if (json.has("stagingBundleId") && !json.isNull("stagingBundleId")) {
46
+ json.getString("stagingBundleId").takeIf { it.isNotEmpty() }
47
+ } else {
48
+ null
49
+ },
50
+ verificationPending = json.optBoolean("verificationPending", false),
51
+ verificationAttemptedAt =
52
+ if (json.has("verificationAttemptedAt") && !json.isNull("verificationAttemptedAt")) {
53
+ json.getLong("verificationAttemptedAt")
54
+ } else {
55
+ null
56
+ },
57
+ stagingExecutionCount =
58
+ if (json.has("stagingExecutionCount") && !json.isNull("stagingExecutionCount")) {
59
+ json.getInt("stagingExecutionCount")
60
+ } else {
61
+ null
62
+ },
63
+ updatedAt = json.optLong("updatedAt", System.currentTimeMillis()),
64
+ )
65
+
66
+ fun loadFromFile(
67
+ file: File,
68
+ expectedIsolationKey: String,
69
+ ): BundleMetadata? {
70
+ return try {
71
+
72
+ if (!file.exists()) {
73
+ CommonHelper.logPrint(CLASS_TAG, "Metadata file does not exist: ${file.absolutePath}")
74
+ return null
75
+ }
76
+
77
+ val jsonString = file.readText()
78
+ val json = JSONObject(jsonString)
79
+ val metadata = fromJson(json)
80
+
81
+ // Validate isolation key
82
+ val metadataKey = metadata.isolationKey
83
+ if (metadataKey != null) {
84
+ if (metadataKey != expectedIsolationKey) {
85
+ CommonHelper.logPrint(CLASS_TAG, "Isolation key mismatch: expected=$expectedIsolationKey, got=$metadataKey")
86
+ return null
87
+ }
88
+ } else {
89
+ CommonHelper.logPrint(CLASS_TAG, "Missing isolation key in metadata, treating as invalid")
90
+ return null
91
+ }
92
+
93
+ metadata
94
+ } catch (e: Exception) {
95
+ CommonHelper.logPrint(CLASS_TAG, "Failed to load metadata from file $e")
96
+ Log.e(CLASS_TAG, "Failed to load metadata from file", e)
97
+ null
98
+ }
99
+
100
+ }
101
+ }
102
+
103
+ fun toJson(): JSONObject =
104
+ JSONObject().apply {
105
+ put("schema", schema)
106
+ put("isolationKey", isolationKey ?: JSONObject.NULL)
107
+ put("stableBundleId", stableBundleId ?: JSONObject.NULL)
108
+ put("stagingBundleId", stagingBundleId ?: JSONObject.NULL)
109
+ put("verificationPending", verificationPending)
110
+ put("verificationAttemptedAt", verificationAttemptedAt ?: JSONObject.NULL)
111
+ put("stagingExecutionCount", stagingExecutionCount ?: JSONObject.NULL)
112
+ put("updatedAt", updatedAt)
113
+ }
114
+
115
+ fun saveToFile(file: File): Boolean =
116
+ try {
117
+ file.parentFile?.mkdirs()
118
+ file.writeText(toJson().toString(2))
119
+ CommonHelper.logPrint(CLASS_TAG, "Saved metadata to file: ${file.absolutePath}")
120
+ true
121
+ } catch (e: Exception) {
122
+ CommonHelper.logPrint(CLASS_TAG, "Failed to save metadata to file: $e")
123
+ Log.e(CLASS_TAG, "Failed to save metadata to file", e)
124
+ false
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Entry for a crashed bundle in history
130
+ */
131
+ data class CrashedBundleEntry(
132
+ val bundleId: String,
133
+ val crashedAt: Long,
134
+ val crashCount: Int = 1,
135
+ ){
136
+ companion object {
137
+ fun fromJson(json: JSONObject): CrashedBundleEntry =
138
+ CrashedBundleEntry(
139
+ bundleId = json.getString("bundleId"),
140
+ crashedAt = json.getLong("crashedAt"),
141
+ crashCount = json.optInt("crashCount", 1),
142
+ )
143
+ }
144
+
145
+ fun toJson(): JSONObject =
146
+ JSONObject().apply {
147
+ put("bundleId", bundleId)
148
+ put("crashedAt", crashedAt)
149
+ put("crashCount", crashCount)
150
+ }
151
+ }
152
+
153
+ /**
154
+ * History of crashed bundles
155
+ */
156
+ data class CrashedHistory(
157
+ val bundles: MutableList<CrashedBundleEntry> = mutableListOf(),
158
+ val maxHistorySize: Int = DEFAULT_MAX_HISTORY_SIZE,
159
+ ) {
160
+ companion object {
161
+ private const val CLSSS_TAG = "CrashedHistory"
162
+ const val DEFAULT_MAX_HISTORY_SIZE = 10
163
+ const val CRASHED_HISTORY_FILENAME = "crashed-history.json"
164
+
165
+ fun fromJson(json: JSONObject): CrashedHistory {
166
+ val bundlesArray = json.optJSONArray("bundles") ?: JSONArray()
167
+ val bundles = mutableListOf<CrashedBundleEntry>()
168
+ for (i in 0 until bundlesArray.length()) {
169
+ bundles.add(CrashedBundleEntry.fromJson(bundlesArray.getJSONObject(i)))
170
+ }
171
+ return CrashedHistory(
172
+ bundles = bundles,
173
+ maxHistorySize = json.optInt("maxHistorySize", DEFAULT_MAX_HISTORY_SIZE),
174
+ )
175
+ }
176
+
177
+ fun loadFromFile(file: File): CrashedHistory {
178
+ return try {
179
+ if (!file.exists()) {
180
+ CommonHelper.logPrint(CLSSS_TAG, "Crashed history file does not exist, returning empty history")
181
+ return CrashedHistory()
182
+ }
183
+ val jsonString = file.readText()
184
+ val json = JSONObject(jsonString)
185
+ fromJson(json)
186
+ } catch (e: Exception) {
187
+ CommonHelper.logPrint(CLSSS_TAG, "Failed to load crashed history from file: $e")
188
+ Log.e(CLSSS_TAG, "Failed to load crashed history from file", e)
189
+ CrashedHistory()
190
+ }
191
+ }
192
+ }
193
+
194
+ fun toJson(): JSONObject =
195
+ JSONObject().apply {
196
+ val bundlesArray = JSONArray()
197
+ bundles.forEach { bundlesArray.put(it.toJson()) }
198
+ put("bundles", bundlesArray)
199
+ put("maxHistorySize", maxHistorySize)
200
+ }
201
+
202
+ fun saveToFile(file: File): Boolean =
203
+ try {
204
+ file.parentFile?.mkdirs()
205
+ file.writeText(toJson().toString(2))
206
+ CommonHelper.logPrint(CLSSS_TAG, "Saved crashed history to file: ${file.absolutePath}")
207
+ true
208
+ } catch (e: Exception) {
209
+ CommonHelper.logPrint(CLSSS_TAG, "Failed to save crashed history to file: $e")
210
+ Log.e(CLSSS_TAG, "Failed to save crashed history to file", e)
211
+ false
212
+ }
213
+
214
+ fun contains(bundleId: String): Boolean = bundles.any { it.bundleId == bundleId }
215
+
216
+ fun addEntry(bundleId: String) {
217
+ val existingIndex = bundles.indexOfFirst { it.bundleId == bundleId }
218
+ if (existingIndex >= 0) {
219
+ // Update existing entry
220
+ val existing = bundles[existingIndex]
221
+ bundles[existingIndex] =
222
+ existing.copy(
223
+ crashedAt = System.currentTimeMillis(),
224
+ crashCount = existing.crashCount + 1,
225
+ )
226
+ } else {
227
+ // Add new entry
228
+ bundles.add(
229
+ CrashedBundleEntry(
230
+ bundleId = bundleId,
231
+ crashedAt = System.currentTimeMillis(),
232
+ crashCount = 1,
233
+ ),
234
+ )
235
+ }
236
+
237
+ // Trim to max size (keep most recent)
238
+ if (bundles.size > maxHistorySize) {
239
+ bundles.sortBy { it.crashedAt }
240
+ while (bundles.size > maxHistorySize) {
241
+ bundles.removeAt(0)
242
+ }
243
+ }
244
+ }
245
+
246
+ fun clear() {
247
+ bundles.clear()
248
+ }
249
+ }
@@ -0,0 +1,39 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.util.Log
4
+
5
+ object CommonHelper {
6
+
7
+ const val MAIN_LOG_TAG = "*IpayCodePush -> "
8
+
9
+ const val WARNING_LOG = "WARNING_LOG"
10
+
11
+ const val ERROR_LOG = "ERROR_LOG"
12
+
13
+ fun logPrint(classTag:String, value: String?) {
14
+ if (value == null) {
15
+ return
16
+ }
17
+
18
+ val fullTagName = "$MAIN_LOG_TAG $classTag"
19
+
20
+ Log.i(fullTagName, value)
21
+ }
22
+
23
+ fun logPrint(type:String,classTag:String, value: String?) {
24
+ if (value == null) {
25
+ return
26
+ }
27
+
28
+ val fullTagName = "$MAIN_LOG_TAG $classTag"
29
+
30
+ if(type == "WARNING_LOG"){
31
+ Log.i(fullTagName, value)
32
+ } else if(type == "ERROR_LOG") {
33
+ Log.e(fullTagName, value)
34
+ }
35
+ else{
36
+ Log.i(fullTagName, value)
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,85 @@
1
+ package com.instantpaycodepush
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
+
12
+ companion object {
13
+ private const val CLASS_TAG = "*DecompressService"
14
+ }
15
+
16
+ // Array of available strategies in order of detection priority
17
+ // Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
18
+ private val strategies =
19
+ listOf(
20
+ ZipDecompressionStrategy(),
21
+ //TarGzDecompressionStrategy(),
22
+ //TarBrDecompressionStrategy(),
23
+ )
24
+
25
+ /**
26
+ * Extracts a compressed file to the destination directory.
27
+ * Automatically detects compression format by trying each strategy's validation.
28
+ * @param filePath Path to the compressed file
29
+ * @param destinationPath Path to the destination directory
30
+ * @param progressCallback Callback for progress updates (0.0 - 1.0)
31
+ * @return true if extraction was successful, false otherwise
32
+ */
33
+
34
+ fun extractZipFile(
35
+ filePath: String,
36
+ destinationPath: String,
37
+ progressCallback: (Double) -> Unit,
38
+ ): Boolean {
39
+ // Collect file information for better error messages
40
+ val file = File(filePath)
41
+ val fileName = file.name
42
+ val fileSize = if (file.exists()) file.length() else 0L
43
+
44
+ // Try each strategy's validation
45
+ for (strategy in strategies) {
46
+ if (strategy.isValid(filePath)) {
47
+ CommonHelper.logPrint(CLASS_TAG, "Using strategy for $fileName")
48
+ return strategy.decompress(filePath, destinationPath, progressCallback)
49
+ }
50
+ }
51
+
52
+ // No valid strategy found - provide detailed error message
53
+ val errorMessage =
54
+ """
55
+ Failed to decompress file: $fileName ($fileSize bytes)
56
+
57
+ Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
58
+
59
+ Supported formats:
60
+ - ZIP archives (.zip)
61
+ - GZIP compressed TAR archives (.tar.gz)
62
+ - Brotli compressed TAR archives (.tar.br)
63
+
64
+ Please verify the file is not corrupted and matches one of the supported formats.
65
+ """.trimIndent()
66
+
67
+ Log.e(CLASS_TAG, errorMessage)
68
+ return false
69
+ }
70
+
71
+ /**
72
+ * Validates if a file is a valid compressed archive.
73
+ * @param filePath Path to the file to validate
74
+ * @return true if the file is a valid compressed archive
75
+ */
76
+ fun isValidZipFile(filePath: String): Boolean {
77
+ for (strategy in strategies) {
78
+ if (strategy.isValid(filePath)) {
79
+ return true
80
+ }
81
+ }
82
+ CommonHelper.logPrint(CLASS_TAG, "No valid strategy found for file: $filePath")
83
+ return false
84
+ }
85
+ }
@@ -0,0 +1,24 @@
1
+ package com.instantpaycodepush
2
+
3
+ interface DecompressionStrategy {
4
+
5
+ /**
6
+ * Validates if a file can be decompressed by this strategy
7
+ * @param filePath Path to the file to validate
8
+ * @return true if the file is valid for this strategy
9
+ */
10
+ fun isValid(filePath: String): Boolean
11
+
12
+ /**
13
+ * Decompresses a file to the destination directory
14
+ * @param filePath Path to the compressed file
15
+ * @param destinationPath Path to the destination directory
16
+ * @param progressCallback Callback for progress updates (0.0 - 1.0)
17
+ * @return true if decompression was successful, false otherwise
18
+ */
19
+ fun decompress(
20
+ filePath: String,
21
+ destinationPath: String,
22
+ progressCallback: (Double) -> Unit,
23
+ ): Boolean
24
+ }
@@ -0,0 +1,105 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.content.Context
4
+ import java.io.File
5
+
6
+ /**
7
+ * Interface for file system operations
8
+ */
9
+ interface FileSystemService {
10
+ /**
11
+ * Checks if a file exists at the given path
12
+ */
13
+ fun fileExists(path: String): Boolean
14
+
15
+ /**
16
+ * Creates directory at the given path, including any necessary parent directories
17
+ */
18
+ fun createDirectory(path: String): Boolean
19
+
20
+ /**
21
+ * Removes a file or directory at the given path
22
+ */
23
+ fun removeItem(path: String): Boolean
24
+
25
+ /**
26
+ * Moves a file or directory from source path to destination path
27
+ */
28
+ fun moveItem(
29
+ sourcePath: String,
30
+ destinationPath: String,
31
+ ): Boolean
32
+
33
+ /**
34
+ * Copies a file or directory from source path to destination path
35
+ */
36
+ fun copyItem(
37
+ sourcePath: String,
38
+ destinationPath: String,
39
+ ): Boolean
40
+
41
+ /**
42
+ * Lists the contents of a directory
43
+ */
44
+ fun contentsOfDirectory(path: String): List<String>
45
+
46
+ /**
47
+ * Gets the external files directory for the application
48
+ */
49
+ fun getExternalFilesDir(): File?
50
+ }
51
+
52
+ /**
53
+ * Implementation of FileSystemService using standard File API
54
+ */
55
+ class FileManagerService(
56
+ private val context: Context,
57
+ ) : FileSystemService {
58
+
59
+ override fun fileExists(path: String): Boolean = File(path).exists()
60
+
61
+ override fun createDirectory(path: String): Boolean = File(path).mkdirs()
62
+
63
+ override fun removeItem(path: String): Boolean = File(path).deleteRecursively()
64
+
65
+ override fun moveItem(
66
+ sourcePath: String,
67
+ destinationPath: String,
68
+ ): Boolean {
69
+ val source = File(sourcePath)
70
+ val destination = File(destinationPath)
71
+
72
+ return try {
73
+ if (destination.exists()) {
74
+ destination.deleteRecursively()
75
+ }
76
+ source.renameTo(destination)
77
+ } catch (e: Exception) {
78
+ false
79
+ }
80
+ }
81
+
82
+ override fun copyItem(
83
+ sourcePath: String,
84
+ destinationPath: String,
85
+ ): Boolean {
86
+ val source = File(sourcePath)
87
+ val destination = File(destinationPath)
88
+
89
+ return try {
90
+ if (destination.exists()) {
91
+ destination.deleteRecursively()
92
+ }
93
+ source.copyRecursively(target = destination, overwrite = true)
94
+ } catch (e: Exception) {
95
+ false
96
+ }
97
+ }
98
+
99
+ override fun contentsOfDirectory(path: String): List<String> {
100
+ val directory = File(path)
101
+ return directory.listFiles()?.map { it.name } ?: listOf()
102
+ }
103
+
104
+ override fun getExternalFilesDir(): File? = context.getExternalFilesDir(null)
105
+ }
@@ -0,0 +1,50 @@
1
+ package com.instantpaycodepush
2
+
3
+ import android.util.Log
4
+ import java.io.File
5
+ import java.security.MessageDigest
6
+
7
+ /**
8
+ * Utility class for file hash operations
9
+ */
10
+ object HashUtils {
11
+
12
+ private const val CLASS_TAG = "*HashUtils"
13
+
14
+ /**
15
+ * Calculates SHA256 hash of a file
16
+ * @param file The file to hash
17
+ * @return Hex string of the hash (lowercase)
18
+ */
19
+ fun calculateSHA256(file: File): String {
20
+ val digest = MessageDigest.getInstance("SHA-256")
21
+ file.inputStream().use { input ->
22
+ val buffer = ByteArray(8192)
23
+ var bytesRead: Int
24
+ while (input.read(buffer).also { bytesRead = it } != -1) {
25
+ digest.update(buffer, 0, bytesRead)
26
+ }
27
+ }
28
+ return digest.digest().joinToString("") { "%02x".format(it) }
29
+ }
30
+
31
+ /**
32
+ * Verifies file hash
33
+ * @param file File to verify
34
+ * @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
35
+ * @return true if hash matches
36
+ */
37
+ fun verifyHash(
38
+ file: File,
39
+ expectedHash: String,
40
+ ): Boolean {
41
+ val actualHash = calculateSHA256(file)
42
+ val matches = actualHash.equals(expectedHash, ignoreCase = true)
43
+
44
+ if (!matches) {
45
+ CommonHelper.logPrint(CLASS_TAG, "Hash mismatch - Expected: $expectedHash, Actual: $actualHash")
46
+ }
47
+
48
+ return matches
49
+ }
50
+ }