@hot-updater/react-native 1.0.0-rc.14 → 1.0.0-rc.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/proguard-rules.pro +2 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/lib/commonjs/checkForUpdate.js +7 -10
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +151 -39
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +247 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +15 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +101 -0
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +7 -8
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +21 -16
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.js +4 -1
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/uuidv7.js +41 -0
- package/lib/commonjs/uuidv7.js.map +1 -0
- package/lib/commonjs/uuidv7.spec.js +30 -0
- package/lib/commonjs/uuidv7.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +1 -3
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +7 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +7 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +153 -41
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +248 -6
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +15 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +102 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +7 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +21 -16
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.js +5 -2
- package/lib/module/notifyAppReadyInsights.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.spec.js +59 -0
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/uuidv7.js +36 -0
- package/lib/module/uuidv7.js.map +1 -0
- package/lib/module/uuidv7.spec.js +30 -0
- package/lib/module/uuidv7.spec.js.map +1 -0
- package/lib/module/wrap.js +1 -3
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +7 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +0 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
- package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +0 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/uuidv7.d.ts +10 -0
- package/lib/typescript/module/uuidv7.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +7 -4
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
- package/src/checkForUpdate.ts +5 -11
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +289 -8
- package/src/httpClient.ts +224 -67
- package/src/index.spec.ts +70 -1
- package/src/index.ts +18 -7
- package/src/native.spec.ts +23 -16
- package/src/native.ts +12 -24
- package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
- package/src/notifyAppReadyInsights.spec.ts +60 -0
- package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
- package/src/notifyAppReadyInsights.ts +13 -1
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/uuidv7.spec.ts +25 -0
- package/src/uuidv7.ts +38 -0
- package/src/wrap.spec.ts +8 -0
- package/src/wrap.tsx +0 -2
- package/src/wrap.types.ts +9 -4
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
|
@@ -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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
package com.hotupdater
|
|
2
|
-
|
|
3
|
-
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
|
|
4
|
-
import org.junit.Assert.assertArrayEquals
|
|
5
|
-
import org.junit.Assert.assertEquals
|
|
6
|
-
import org.junit.Assert.assertNull
|
|
7
|
-
import org.junit.Assert.assertThrows
|
|
8
|
-
import org.junit.Test
|
|
9
|
-
import java.io.ByteArrayInputStream
|
|
10
|
-
import java.io.ByteArrayOutputStream
|
|
11
|
-
import org.apache.commons.compress.archivers.tar.TarArchiveEntry as CommonsTarArchiveEntry
|
|
12
|
-
|
|
13
|
-
class TarArchiveInputStreamTest {
|
|
14
|
-
@Test
|
|
15
|
-
fun `reads a long basename from a POSIX PAX header`() {
|
|
16
|
-
val path = "raw/${"long-name-".repeat(14)}asset.bmp"
|
|
17
|
-
val contents = "pax asset".toByteArray()
|
|
18
|
-
val archive = createPaxArchive(path, contents)
|
|
19
|
-
|
|
20
|
-
TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
|
|
21
|
-
val entry = tar.getNextEntry()
|
|
22
|
-
|
|
23
|
-
assertEquals(path, entry?.name)
|
|
24
|
-
assertArrayEquals(contents, tar.readBytes())
|
|
25
|
-
assertNull(tar.getNextEntry())
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
@Test
|
|
30
|
-
fun `rejects path traversal from a POSIX PAX header`() {
|
|
31
|
-
val path = "../${"long-name-".repeat(14)}asset.bmp"
|
|
32
|
-
val archive =
|
|
33
|
-
createPaxArchive(
|
|
34
|
-
path = path,
|
|
35
|
-
contents = "malicious".toByteArray(),
|
|
36
|
-
headerPath = "safe.bmp",
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
val error =
|
|
40
|
-
assertThrows(SecurityException::class.java) {
|
|
41
|
-
TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
|
|
42
|
-
tar.getNextEntry()
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
assertEquals("Path traversal detected: $path", error.message)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private fun createPaxArchive(
|
|
50
|
-
path: String,
|
|
51
|
-
contents: ByteArray,
|
|
52
|
-
headerPath: String = path,
|
|
53
|
-
): ByteArray {
|
|
54
|
-
val output = ByteArrayOutputStream()
|
|
55
|
-
TarArchiveOutputStream(output).use { tar ->
|
|
56
|
-
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX)
|
|
57
|
-
val entry =
|
|
58
|
-
CommonsTarArchiveEntry(headerPath).apply {
|
|
59
|
-
size = contents.size.toLong()
|
|
60
|
-
if (path != headerPath) addPaxHeader("path", path)
|
|
61
|
-
}
|
|
62
|
-
tar.putArchiveEntry(entry)
|
|
63
|
-
tar.write(contents)
|
|
64
|
-
tar.closeArchiveEntry()
|
|
65
|
-
}
|
|
66
|
-
return output.toByteArray()
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
enum ArchiveExtractionUtilities {
|
|
4
|
-
static let bufferSize = 64 * 1024
|
|
5
|
-
|
|
6
|
-
static func readUpToCount(from handle: FileHandle, count: Int) throws -> Data? {
|
|
7
|
-
guard count >= 0 else {
|
|
8
|
-
throw NSError(
|
|
9
|
-
domain: "ArchiveExtractionUtilities",
|
|
10
|
-
code: 5,
|
|
11
|
-
userInfo: [NSLocalizedDescriptionKey: "Invalid read size: \(count)"]
|
|
12
|
-
)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if count == 0 {
|
|
16
|
-
return Data()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
20
|
-
return try handle.read(upToCount: count)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return handle.readData(ofLength: count)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
static func readExactly(from handle: FileHandle, count: Int) throws -> Data {
|
|
27
|
-
guard count >= 0 else {
|
|
28
|
-
throw NSError(
|
|
29
|
-
domain: "ArchiveExtractionUtilities",
|
|
30
|
-
code: 1,
|
|
31
|
-
userInfo: [NSLocalizedDescriptionKey: "Invalid read size: \(count)"]
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if count == 0 {
|
|
36
|
-
return Data()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
guard let data = try readUpToCount(from: handle, count: count), data.count == count else {
|
|
40
|
-
throw NSError(
|
|
41
|
-
domain: "ArchiveExtractionUtilities",
|
|
42
|
-
code: 2,
|
|
43
|
-
userInfo: [NSLocalizedDescriptionKey: "Unexpected end of archive while reading \(count) bytes"]
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return data
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static func currentOffset(for handle: FileHandle) -> UInt64 {
|
|
51
|
-
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
52
|
-
return (try? handle.offset()) ?? 0
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return handle.offsetInFile
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static func seek(_ handle: FileHandle, to offset: UInt64) throws {
|
|
59
|
-
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
60
|
-
try handle.seek(toOffset: offset)
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
handle.seek(toFileOffset: offset)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static func skipBytes(_ byteCount: UInt64, in handle: FileHandle) throws {
|
|
68
|
-
guard byteCount > 0 else {
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let offset = currentOffset(for: handle)
|
|
73
|
-
let (targetOffset, overflowed) = offset.addingReportingOverflow(byteCount)
|
|
74
|
-
|
|
75
|
-
guard !overflowed else {
|
|
76
|
-
throw NSError(
|
|
77
|
-
domain: "ArchiveExtractionUtilities",
|
|
78
|
-
code: 6,
|
|
79
|
-
userInfo: [NSLocalizedDescriptionKey: "Archive offset overflow while skipping \(byteCount) bytes from offset \(offset)"]
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try seek(handle, to: targetOffset)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
static func normalizedRelativePath(from rawPath: String) -> String? {
|
|
87
|
-
let candidate = rawPath.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
88
|
-
guard !candidate.isEmpty,
|
|
89
|
-
!candidate.contains("\0"),
|
|
90
|
-
!candidate.contains("\\"),
|
|
91
|
-
!candidate.hasPrefix("/"),
|
|
92
|
-
candidate.range(of: #"^[A-Za-z]:"#, options: .regularExpression) == nil
|
|
93
|
-
else {
|
|
94
|
-
return nil
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let components = candidate
|
|
98
|
-
.split(separator: "/")
|
|
99
|
-
.map(String.init)
|
|
100
|
-
|
|
101
|
-
guard !components.isEmpty else {
|
|
102
|
-
return nil
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
guard components.count == candidate.split(separator: "/", omittingEmptySubsequences: false).count,
|
|
106
|
-
!components.contains(".."),
|
|
107
|
-
!components.contains(".") else {
|
|
108
|
-
return nil
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return components.joined(separator: "/")
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
static func extractionURL(for relativePath: String, destinationRoot: String) throws -> URL {
|
|
115
|
-
let rootURL = URL(fileURLWithPath: destinationRoot, isDirectory: true)
|
|
116
|
-
let targetURL = rootURL.appendingPathComponent(relativePath)
|
|
117
|
-
let standardizedRoot = rootURL.standardizedFileURL.path
|
|
118
|
-
let standardizedTarget = targetURL.standardizedFileURL.path
|
|
119
|
-
|
|
120
|
-
guard standardizedTarget == standardizedRoot ||
|
|
121
|
-
standardizedTarget.hasPrefix(standardizedRoot + "/") else {
|
|
122
|
-
throw NSError(
|
|
123
|
-
domain: "ArchiveExtractionUtilities",
|
|
124
|
-
code: 3,
|
|
125
|
-
userInfo: [NSLocalizedDescriptionKey: "Path traversal attempt detected: \(relativePath)"]
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return targetURL
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
static func ensureDirectory(at url: URL, fileManager: FileManager = .default) throws {
|
|
133
|
-
var isDirectory = ObjCBool(false)
|
|
134
|
-
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
|
135
|
-
if isDirectory.boolValue {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try fileManager.removeItem(at: url)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
static func createOutputFile(at url: URL, fileManager: FileManager = .default) throws -> FileHandle {
|
|
146
|
-
try ensureDirectory(at: url.deletingLastPathComponent(), fileManager: fileManager)
|
|
147
|
-
|
|
148
|
-
if fileManager.fileExists(atPath: url.path) {
|
|
149
|
-
try fileManager.removeItem(at: url)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
guard fileManager.createFile(atPath: url.path, contents: nil) else {
|
|
153
|
-
throw NSError(
|
|
154
|
-
domain: "ArchiveExtractionUtilities",
|
|
155
|
-
code: 4,
|
|
156
|
-
userInfo: [NSLocalizedDescriptionKey: "Failed to create output file at \(url.path)"]
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return try FileHandle(forWritingTo: url)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
extension Data {
|
|
165
|
-
func archiveUInt16LE(at offset: Int) -> UInt16 {
|
|
166
|
-
let byte0 = UInt16(self[offset])
|
|
167
|
-
let byte1 = UInt16(self[offset + 1]) << 8
|
|
168
|
-
return byte0 | byte1
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
func archiveUInt32LE(at offset: Int) -> UInt32 {
|
|
172
|
-
let byte0 = UInt32(self[offset])
|
|
173
|
-
let byte1 = UInt32(self[offset + 1]) << 8
|
|
174
|
-
let byte2 = UInt32(self[offset + 2]) << 16
|
|
175
|
-
let byte3 = UInt32(self[offset + 3]) << 24
|
|
176
|
-
return byte0 | byte1 | byte2 | byte3
|
|
177
|
-
}
|
|
178
|
-
}
|