@hot-updater/react-native 0.21.4 → 0.21.6
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/build.gradle +0 -1
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +387 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +6 -6
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +6 -6
- package/package.json +5 -5
package/android/build.gradle
CHANGED
|
@@ -132,7 +132,6 @@ dependencies {
|
|
|
132
132
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
133
133
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
134
134
|
implementation "org.brotli:dec:0.1.2"
|
|
135
|
-
implementation "org.apache.commons:commons-compress:1.28.0"
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
if (isNewArchitectureEnabled()) {
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import java.io.EOFException
|
|
4
|
+
import java.io.IOException
|
|
5
|
+
import java.io.InputStream
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Secure TAR archive input stream for Android API 21+
|
|
9
|
+
* Replaces Apache Commons Compress to avoid java.nio.file dependencies
|
|
10
|
+
*/
|
|
11
|
+
class TarArchiveInputStream(
|
|
12
|
+
private val input: InputStream,
|
|
13
|
+
) : InputStream() {
|
|
14
|
+
private var currentEntry: TarArchiveEntry? = null
|
|
15
|
+
private var currentEntryBytesRead: Long = 0
|
|
16
|
+
private var longName: String? = null
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
private const val TAG = "TarInputStream"
|
|
20
|
+
private const val BLOCK_SIZE = 512
|
|
21
|
+
private const val NAME_OFFSET = 0
|
|
22
|
+
private const val NAME_LENGTH = 100
|
|
23
|
+
private const val MODE_OFFSET = 100
|
|
24
|
+
private const val SIZE_OFFSET = 124
|
|
25
|
+
private const val SIZE_LENGTH = 12
|
|
26
|
+
private const val CHECKSUM_OFFSET = 148
|
|
27
|
+
private const val CHECKSUM_LENGTH = 8
|
|
28
|
+
private const val TYPEFLAG_OFFSET = 156
|
|
29
|
+
private const val LINKNAME_OFFSET = 157
|
|
30
|
+
private const val LINKNAME_LENGTH = 100
|
|
31
|
+
private const val MAGIC_OFFSET = 257
|
|
32
|
+
private const val PREFIX_OFFSET = 345
|
|
33
|
+
private const val PREFIX_LENGTH = 155
|
|
34
|
+
|
|
35
|
+
// Maximum file size: 1GB per file
|
|
36
|
+
private const val MAX_FILE_SIZE = 1_073_741_824L
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the next TAR entry
|
|
41
|
+
*/
|
|
42
|
+
fun getNextEntry(): TarArchiveEntry? {
|
|
43
|
+
// Skip remaining bytes of current entry
|
|
44
|
+
if (currentEntry != null) {
|
|
45
|
+
val remaining = currentEntry!!.size - currentEntryBytesRead
|
|
46
|
+
if (remaining > 0) {
|
|
47
|
+
skipBytes(remaining)
|
|
48
|
+
}
|
|
49
|
+
skipPadding(currentEntry!!.size)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
currentEntryBytesRead = 0
|
|
53
|
+
|
|
54
|
+
while (true) {
|
|
55
|
+
val headerBytes = readBlock() ?: return null
|
|
56
|
+
|
|
57
|
+
// Check for end of archive (all zeros)
|
|
58
|
+
if (isAllZeros(headerBytes)) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Verify header
|
|
63
|
+
if (!isValidHeader(headerBytes)) {
|
|
64
|
+
throw IOException("Invalid TAR header")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!verifyChecksum(headerBytes)) {
|
|
68
|
+
throw IOException("TAR header checksum verification failed")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse header
|
|
72
|
+
val entry = parseHeader(headerBytes)
|
|
73
|
+
|
|
74
|
+
// Handle GNU long filename extension
|
|
75
|
+
if (entry.typeFlag == 'L') {
|
|
76
|
+
longName = readLongName(entry.size)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Apply long name if present
|
|
81
|
+
if (longName != null) {
|
|
82
|
+
entry.name = longName!!
|
|
83
|
+
longName = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate entry
|
|
87
|
+
validateEntry(entry)
|
|
88
|
+
|
|
89
|
+
currentEntry = entry
|
|
90
|
+
return entry
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override fun read(): Int {
|
|
95
|
+
val b = ByteArray(1)
|
|
96
|
+
val n = read(b, 0, 1)
|
|
97
|
+
return if (n <= 0) -1 else b[0].toInt() and 0xFF
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun read(
|
|
101
|
+
b: ByteArray,
|
|
102
|
+
off: Int,
|
|
103
|
+
len: Int,
|
|
104
|
+
): Int {
|
|
105
|
+
if (currentEntry == null) {
|
|
106
|
+
throw IllegalStateException("No current entry")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
val remaining = currentEntry!!.size - currentEntryBytesRead
|
|
110
|
+
if (remaining <= 0) {
|
|
111
|
+
return -1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
val toRead = minOf(len.toLong(), remaining).toInt()
|
|
115
|
+
val bytesRead = input.read(b, off, toRead)
|
|
116
|
+
|
|
117
|
+
if (bytesRead > 0) {
|
|
118
|
+
currentEntryBytesRead += bytesRead
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return bytesRead
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override fun close() {
|
|
125
|
+
input.close()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read a 512-byte block from input
|
|
130
|
+
*/
|
|
131
|
+
private fun readBlock(): ByteArray? {
|
|
132
|
+
val block = ByteArray(BLOCK_SIZE)
|
|
133
|
+
var offset = 0
|
|
134
|
+
|
|
135
|
+
while (offset < BLOCK_SIZE) {
|
|
136
|
+
val n = input.read(block, offset, BLOCK_SIZE - offset)
|
|
137
|
+
if (n < 0) {
|
|
138
|
+
return if (offset == 0) null else throw EOFException("Unexpected end of TAR archive")
|
|
139
|
+
}
|
|
140
|
+
offset += n
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return block
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if block is all zeros
|
|
148
|
+
*/
|
|
149
|
+
private fun isAllZeros(block: ByteArray): Boolean = block.all { it == 0.toByte() }
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verify TAR header has valid magic number
|
|
153
|
+
*/
|
|
154
|
+
private fun isValidHeader(header: ByteArray): Boolean {
|
|
155
|
+
// Check for "ustar" magic (may have \0 or space after)
|
|
156
|
+
val magic = String(header, MAGIC_OFFSET, 5, Charsets.US_ASCII)
|
|
157
|
+
return magic == "ustar"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verify header checksum
|
|
162
|
+
*/
|
|
163
|
+
private fun verifyChecksum(header: ByteArray): Boolean {
|
|
164
|
+
val storedChecksum = parseOctal(header, CHECKSUM_OFFSET, CHECKSUM_LENGTH).toInt()
|
|
165
|
+
|
|
166
|
+
// Calculate checksums (both signed and unsigned for compatibility)
|
|
167
|
+
var unsignedSum = 0
|
|
168
|
+
var signedSum = 0
|
|
169
|
+
|
|
170
|
+
for (i in 0 until BLOCK_SIZE) {
|
|
171
|
+
val value =
|
|
172
|
+
if (i in CHECKSUM_OFFSET until CHECKSUM_OFFSET + CHECKSUM_LENGTH) {
|
|
173
|
+
32 // Space character
|
|
174
|
+
} else {
|
|
175
|
+
header[i].toInt()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
unsignedSum += value and 0xFF
|
|
179
|
+
signedSum += value.toByte().toInt()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return storedChecksum == unsignedSum || storedChecksum == signedSum
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse TAR header into TarArchiveEntry
|
|
187
|
+
*/
|
|
188
|
+
private fun parseHeader(header: ByteArray): TarArchiveEntry {
|
|
189
|
+
val name = parseString(header, NAME_OFFSET, NAME_LENGTH)
|
|
190
|
+
val mode = parseOctal(header, MODE_OFFSET, 8).toInt()
|
|
191
|
+
val size = parseNumeric(header, SIZE_OFFSET, SIZE_LENGTH)
|
|
192
|
+
val typeFlag = header[TYPEFLAG_OFFSET].toInt().toChar()
|
|
193
|
+
val linkName = parseString(header, LINKNAME_OFFSET, LINKNAME_LENGTH)
|
|
194
|
+
val prefix = parseString(header, PREFIX_OFFSET, PREFIX_LENGTH)
|
|
195
|
+
|
|
196
|
+
// Combine prefix and name
|
|
197
|
+
val fullName = if (prefix.isNotEmpty()) "$prefix/$name" else name
|
|
198
|
+
|
|
199
|
+
return TarArchiveEntry(
|
|
200
|
+
name = fullName,
|
|
201
|
+
mode = mode,
|
|
202
|
+
size = size,
|
|
203
|
+
typeFlag = typeFlag,
|
|
204
|
+
linkName = linkName,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse string field from header
|
|
210
|
+
*/
|
|
211
|
+
private fun parseString(
|
|
212
|
+
bytes: ByteArray,
|
|
213
|
+
offset: Int,
|
|
214
|
+
length: Int,
|
|
215
|
+
): String {
|
|
216
|
+
var end = offset
|
|
217
|
+
while (end < offset + length && bytes[end] != 0.toByte()) {
|
|
218
|
+
end++
|
|
219
|
+
}
|
|
220
|
+
return String(bytes, offset, end - offset, Charsets.UTF_8).trim()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parse octal number from header field
|
|
225
|
+
*/
|
|
226
|
+
private fun parseOctal(
|
|
227
|
+
bytes: ByteArray,
|
|
228
|
+
offset: Int,
|
|
229
|
+
length: Int,
|
|
230
|
+
): Long {
|
|
231
|
+
var result = 0L
|
|
232
|
+
var i = offset
|
|
233
|
+
val end = offset + length
|
|
234
|
+
|
|
235
|
+
// Skip leading spaces
|
|
236
|
+
while (i < end && bytes[i] == ' '.code.toByte()) i++
|
|
237
|
+
|
|
238
|
+
// Parse octal digits
|
|
239
|
+
while (i < end) {
|
|
240
|
+
val b = bytes[i]
|
|
241
|
+
if (b == 0.toByte() || b == ' '.code.toByte()) break
|
|
242
|
+
if (b < '0'.code.toByte() || b > '7'.code.toByte()) {
|
|
243
|
+
throw IOException("Invalid octal digit: ${b.toInt()}")
|
|
244
|
+
}
|
|
245
|
+
result = result * 8 + (b - '0'.code.toByte())
|
|
246
|
+
i++
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parse numeric field (supports both octal and base-256 encoding)
|
|
254
|
+
*/
|
|
255
|
+
private fun parseNumeric(
|
|
256
|
+
bytes: ByteArray,
|
|
257
|
+
offset: Int,
|
|
258
|
+
length: Int,
|
|
259
|
+
): Long {
|
|
260
|
+
// Check for base-256 encoding (high bit set)
|
|
261
|
+
if ((bytes[offset].toInt() and 0x80) != 0) {
|
|
262
|
+
return parseBase256(bytes, offset, length)
|
|
263
|
+
}
|
|
264
|
+
return parseOctal(bytes, offset, length)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse base-256 encoded number (for files > 8GB)
|
|
269
|
+
*/
|
|
270
|
+
private fun parseBase256(
|
|
271
|
+
bytes: ByteArray,
|
|
272
|
+
offset: Int,
|
|
273
|
+
length: Int,
|
|
274
|
+
): Long {
|
|
275
|
+
var result = 0L
|
|
276
|
+
|
|
277
|
+
// Skip first byte (marker) and read big-endian
|
|
278
|
+
for (i in 1 until length) {
|
|
279
|
+
result = (result shl 8) or (bytes[offset + i].toInt() and 0xFF).toLong()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return result
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Read GNU long filename extension
|
|
287
|
+
*/
|
|
288
|
+
private fun readLongName(size: Long): String {
|
|
289
|
+
val nameBytes = ByteArray(size.toInt())
|
|
290
|
+
var offset = 0
|
|
291
|
+
|
|
292
|
+
while (offset < size) {
|
|
293
|
+
val n = input.read(nameBytes, offset, size.toInt() - offset)
|
|
294
|
+
if (n < 0) throw EOFException("Unexpected end reading long name")
|
|
295
|
+
offset += n
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
skipPadding(size)
|
|
299
|
+
|
|
300
|
+
// Remove trailing NUL
|
|
301
|
+
val nameLength =
|
|
302
|
+
nameBytes
|
|
303
|
+
.indexOfFirst { it == 0.toByte() }
|
|
304
|
+
.takeIf { it >= 0 } ?: nameBytes.size
|
|
305
|
+
|
|
306
|
+
return String(nameBytes, 0, nameLength, Charsets.UTF_8)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Skip padding to 512-byte boundary
|
|
311
|
+
*/
|
|
312
|
+
private fun skipPadding(size: Long) {
|
|
313
|
+
val remainder = size % BLOCK_SIZE
|
|
314
|
+
if (remainder != 0L) {
|
|
315
|
+
skipBytes(BLOCK_SIZE - remainder)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Skip specified number of bytes
|
|
321
|
+
*/
|
|
322
|
+
private fun skipBytes(n: Long) {
|
|
323
|
+
var remaining = n
|
|
324
|
+
val buffer = ByteArray(8192)
|
|
325
|
+
|
|
326
|
+
while (remaining > 0) {
|
|
327
|
+
val toSkip = minOf(buffer.size.toLong(), remaining).toInt()
|
|
328
|
+
val skipped = input.read(buffer, 0, toSkip)
|
|
329
|
+
if (skipped < 0) throw EOFException("Unexpected end of stream")
|
|
330
|
+
remaining -= skipped
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Validate entry for security issues
|
|
336
|
+
*/
|
|
337
|
+
private fun validateEntry(entry: TarArchiveEntry) {
|
|
338
|
+
// Check for negative or excessive file size
|
|
339
|
+
if (entry.size < 0) {
|
|
340
|
+
throw SecurityException("Negative file size: ${entry.size}")
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (entry.size > MAX_FILE_SIZE) {
|
|
344
|
+
throw SecurityException("File size ${entry.size} exceeds maximum $MAX_FILE_SIZE")
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check for absolute paths
|
|
348
|
+
if (entry.name.startsWith("/")) {
|
|
349
|
+
throw SecurityException("Absolute path not allowed: ${entry.name}")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for path traversal
|
|
353
|
+
val normalized = entry.name.replace('\\', '/')
|
|
354
|
+
if (normalized.contains("../") ||
|
|
355
|
+
normalized.contains("/..") ||
|
|
356
|
+
normalized == ".." ||
|
|
357
|
+
normalized.startsWith("../")
|
|
358
|
+
) {
|
|
359
|
+
throw SecurityException("Path traversal detected: ${entry.name}")
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check for null bytes in filename
|
|
363
|
+
if (entry.name.contains('\u0000')) {
|
|
364
|
+
throw SecurityException("Null byte in filename: ${entry.name}")
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* TAR archive entry
|
|
371
|
+
*/
|
|
372
|
+
data class TarArchiveEntry(
|
|
373
|
+
var name: String,
|
|
374
|
+
val mode: Int,
|
|
375
|
+
val size: Long,
|
|
376
|
+
val typeFlag: Char,
|
|
377
|
+
val linkName: String,
|
|
378
|
+
) {
|
|
379
|
+
val isDirectory: Boolean
|
|
380
|
+
get() = typeFlag == '5' || name.endsWith('/')
|
|
381
|
+
|
|
382
|
+
val isFile: Boolean
|
|
383
|
+
get() = typeFlag == '0' || typeFlag == '\u0000'
|
|
384
|
+
|
|
385
|
+
val isSymbolicLink: Boolean
|
|
386
|
+
get() = typeFlag == '2'
|
|
387
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import org.
|
|
5
|
-
import org.apache.commons.compress.compressors.brotli.BrotliCompressorInputStream
|
|
4
|
+
import org.brotli.dec.BrotliInputStream
|
|
6
5
|
import java.io.BufferedInputStream
|
|
7
6
|
import java.io.File
|
|
8
7
|
import java.io.FileInputStream
|
|
@@ -10,6 +9,7 @@ import java.io.FileOutputStream
|
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Strategy for handling TAR+Brotli compressed files
|
|
12
|
+
* Uses native Brotli decoder and custom TAR parser
|
|
13
13
|
*/
|
|
14
14
|
class TarBrDecompressionStrategy : DecompressionStrategy {
|
|
15
15
|
companion object {
|
|
@@ -55,16 +55,16 @@ class TarBrDecompressionStrategy : DecompressionStrategy {
|
|
|
55
55
|
|
|
56
56
|
FileInputStream(filePath).use { fileInputStream ->
|
|
57
57
|
BufferedInputStream(fileInputStream).use { bufferedInputStream ->
|
|
58
|
-
|
|
58
|
+
BrotliInputStream(bufferedInputStream).use { brotliInputStream ->
|
|
59
59
|
TarArchiveInputStream(brotliInputStream).use { tarInputStream ->
|
|
60
|
-
var entry = tarInputStream.
|
|
60
|
+
var entry = tarInputStream.getNextEntry()
|
|
61
61
|
|
|
62
62
|
while (entry != null) {
|
|
63
63
|
val file = File(destinationPath, entry.name)
|
|
64
64
|
|
|
65
65
|
if (!file.canonicalPath.startsWith(destinationDir.canonicalPath)) {
|
|
66
66
|
Log.w(TAG, "Skipping potentially malicious tar entry: ${entry.name}")
|
|
67
|
-
entry = tarInputStream.
|
|
67
|
+
entry = tarInputStream.getNextEntry()
|
|
68
68
|
continue
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -87,7 +87,7 @@ class TarBrDecompressionStrategy : DecompressionStrategy {
|
|
|
87
87
|
val progress = processedBytes.toDouble() / (totalSize * 2.0)
|
|
88
88
|
progressCallback.invoke(progress.coerceIn(0.0, 1.0))
|
|
89
89
|
|
|
90
|
-
entry = tarInputStream.
|
|
90
|
+
entry = tarInputStream.getNextEntry()
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
package com.hotupdater
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
|
|
5
|
-
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
|
|
6
4
|
import java.io.BufferedInputStream
|
|
7
5
|
import java.io.File
|
|
8
6
|
import java.io.FileInputStream
|
|
9
7
|
import java.io.FileOutputStream
|
|
8
|
+
import java.util.zip.GZIPInputStream
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Strategy for handling TAR+GZIP compressed files
|
|
12
|
+
* Uses native GZIP decoder and custom TAR parser
|
|
13
13
|
*/
|
|
14
14
|
class TarGzDecompressionStrategy : DecompressionStrategy {
|
|
15
15
|
companion object {
|
|
@@ -68,16 +68,16 @@ class TarGzDecompressionStrategy : DecompressionStrategy {
|
|
|
68
68
|
|
|
69
69
|
FileInputStream(filePath).use { fileInputStream ->
|
|
70
70
|
BufferedInputStream(fileInputStream).use { bufferedInputStream ->
|
|
71
|
-
|
|
71
|
+
GZIPInputStream(bufferedInputStream).use { gzipInputStream ->
|
|
72
72
|
TarArchiveInputStream(gzipInputStream).use { tarInputStream ->
|
|
73
|
-
var entry = tarInputStream.
|
|
73
|
+
var entry = tarInputStream.getNextEntry()
|
|
74
74
|
|
|
75
75
|
while (entry != null) {
|
|
76
76
|
val file = File(destinationPath, entry.name)
|
|
77
77
|
|
|
78
78
|
if (!file.canonicalPath.startsWith(destinationDir.canonicalPath)) {
|
|
79
79
|
Log.w(TAG, "Skipping potentially malicious tar entry: ${entry.name}")
|
|
80
|
-
entry = tarInputStream.
|
|
80
|
+
entry = tarInputStream.getNextEntry()
|
|
81
81
|
continue
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -100,7 +100,7 @@ class TarGzDecompressionStrategy : DecompressionStrategy {
|
|
|
100
100
|
val progress = processedBytes.toDouble() / (totalSize * 2.0)
|
|
101
101
|
progressCallback.invoke(progress.coerceIn(0.0, 1.0))
|
|
102
102
|
|
|
103
|
-
entry = tarInputStream.
|
|
103
|
+
entry = tarInputStream.getNextEntry()
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.6",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -119,13 +119,13 @@
|
|
|
119
119
|
"react-native": "0.79.1",
|
|
120
120
|
"react-native-builder-bob": "^0.40.10",
|
|
121
121
|
"typescript": "^5.8.3",
|
|
122
|
-
"hot-updater": "0.21.
|
|
122
|
+
"hot-updater": "0.21.6"
|
|
123
123
|
},
|
|
124
124
|
"dependencies": {
|
|
125
125
|
"use-sync-external-store": "1.5.0",
|
|
126
|
-
"@hot-updater/core": "0.21.
|
|
127
|
-
"@hot-updater/
|
|
128
|
-
"@hot-updater/
|
|
126
|
+
"@hot-updater/core": "0.21.6",
|
|
127
|
+
"@hot-updater/plugin-core": "0.21.6",
|
|
128
|
+
"@hot-updater/js": "0.21.6"
|
|
129
129
|
},
|
|
130
130
|
"scripts": {
|
|
131
131
|
"build": "bob build && tsc -p plugin/tsconfig.json",
|