@capgo/capacitor-updater 8.51.8 → 8.51.10

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.
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
146
146
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
147
147
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
148
148
 
149
- private final String pluginVersion = "8.51.8";
149
+ private final String pluginVersion = "8.51.10";
150
150
  private static final String DELAY_CONDITION_PREFERENCES = "";
151
151
 
152
152
  private SharedPreferences.Editor editor;
@@ -449,7 +449,7 @@ public class CapgoUpdater {
449
449
  continue;
450
450
  }
451
451
  try {
452
- copyFile(file, cacheFile);
452
+ copyFileAtomically(file, cacheFile);
453
453
  } catch (IOException e) {
454
454
  logger.debug("Delta cache copy failed: " + file.getPath());
455
455
  }
@@ -475,7 +475,7 @@ public class CapgoUpdater {
475
475
 
476
476
  private void copyFile(final File source, final File dest) throws IOException {
477
477
  try (final FileInputStream input = new FileInputStream(source); final FileOutputStream output = new FileOutputStream(dest)) {
478
- final byte[] buffer = new byte[1024 * 1024];
478
+ final byte[] buffer = new byte[CryptoCipher.copyBufferBytes()];
479
479
  int length;
480
480
  while ((length = input.read(buffer)) != -1) {
481
481
  output.write(buffer, 0, length);
@@ -515,28 +515,74 @@ public class CapgoUpdater {
515
515
  return false;
516
516
  }
517
517
 
518
- final File builtinFile = new File(this.activity.getFilesDir(), "public/" + fileName);
519
- if (verifyChecksum(builtinFile, fileHash)) {
518
+ if (DownloadService.builtinAssetMatches(this.activity.getAssets(), fileName, fileHash)) {
520
519
  return true;
521
520
  }
522
521
 
522
+ try {
523
+ final File builtinFile = DownloadService.resolveManifestBuiltinFile(new File(this.activity.getFilesDir(), "public"), fileName);
524
+ if (verifyChecksum(builtinFile, fileHash)) {
525
+ return true;
526
+ }
527
+ } catch (IOException ignored) {
528
+ // Invalid path; fall through to cache lookup.
529
+ }
530
+
523
531
  final boolean isBrotli = fileName.endsWith(".br");
524
532
  final String fileNameWithoutPath = new File(fileName).getName();
525
533
  final String cacheBaseName = isBrotli ? fileNameWithoutPath.substring(0, fileNameWithoutPath.length() - 3) : fileNameWithoutPath;
526
- final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
527
- final File cacheFile = new File(cacheFolder, fileHash + "_" + cacheBaseName);
528
- if (verifyChecksum(cacheFile, fileHash)) {
529
- return true;
530
- }
534
+ if (isSafeCacheHash(fileHash)) {
535
+ final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
536
+ final File cacheFile = new File(cacheFolder, fileHash + "_" + cacheBaseName);
537
+ // Cache files are named `{hash}_{filename}` and were checksum-verified
538
+ // when written. Re-hashing every hit re-reads the whole bundle and
539
+ // OOMs/janks low-RAM devices during getMissing / delta apply.
540
+ if (isReusableCacheFile(cacheFile, fileHash)) {
541
+ return true;
542
+ }
531
543
 
532
- if (isBrotli) {
533
- final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
534
- return verifyChecksum(legacyCacheFile, fileHash);
544
+ if (isBrotli) {
545
+ final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
546
+ return isReusableCacheFile(legacyCacheFile, fileHash);
547
+ }
535
548
  }
536
549
 
537
550
  return false;
538
551
  }
539
552
 
553
+ static final String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
554
+
555
+ static boolean isSafeCacheHash(final String hash) {
556
+ if (hash == null) {
557
+ return false;
558
+ }
559
+ final int len = hash.length();
560
+ if (len != 64 && len != 8) {
561
+ return false;
562
+ }
563
+ for (int i = 0; i < len; i++) {
564
+ final char c = hash.charAt(i);
565
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
566
+ return false;
567
+ }
568
+ }
569
+ return true;
570
+ }
571
+
572
+ // SHA-256 hash-named cache files were verified when written. Existence is
573
+ // enough for non-empty files; empty files are reused only for the empty SHA-256.
574
+ // CRC32 (8 hex) is too collision-prone to trust without a re-read.
575
+ static boolean isReusableCacheFile(final File file, final String expectedHash) {
576
+ if (file == null || !file.isFile() || !isSafeCacheHash(expectedHash) || expectedHash.length() != 64) {
577
+ return false;
578
+ }
579
+ final long length = file.length();
580
+ if (length > 0) {
581
+ return true;
582
+ }
583
+ return length == 0 && EMPTY_SHA256.equalsIgnoreCase(expectedHash);
584
+ }
585
+
540
586
  public JSONArray getMissingBundleFiles(final JSONArray manifest, final String sessionKey) throws JSONException {
541
587
  final JSONArray missing = new JSONArray();
542
588
  for (int i = 0; i < manifest.length(); i++) {
@@ -1171,19 +1217,22 @@ public class CapgoUpdater {
1171
1217
  throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
1172
1218
  }
1173
1219
 
1174
- final File tempFile = new File(parent, dest.getName() + ".capgo_tmp");
1175
- try (final FileInputStream input = new FileInputStream(source); final FileOutputStream output = new FileOutputStream(tempFile)) {
1176
- final byte[] buffer = new byte[1024 * 1024];
1177
- int length;
1178
- while ((length = input.read(buffer)) != -1) {
1179
- output.write(buffer, 0, length);
1220
+ final File tempFile = File.createTempFile("capgo-", ".tmp", parent);
1221
+ try {
1222
+ try (
1223
+ final FileInputStream input = new FileInputStream(source);
1224
+ final FileOutputStream output = new FileOutputStream(tempFile)
1225
+ ) {
1226
+ final byte[] buffer = new byte[CryptoCipher.copyBufferBytes()];
1227
+ int length;
1228
+ while ((length = input.read(buffer)) != -1) {
1229
+ output.write(buffer, 0, length);
1230
+ }
1180
1231
  }
1181
- }
1182
-
1183
- if (!tempFile.renameTo(dest)) {
1184
- if (!dest.delete() || !tempFile.renameTo(dest)) {
1232
+ CryptoCipher.replaceFile(tempFile, dest);
1233
+ } finally {
1234
+ if (tempFile.exists()) {
1185
1235
  tempFile.delete();
1186
- throw new IOException("Failed to replace file: " + dest.getAbsolutePath());
1187
1236
  }
1188
1237
  }
1189
1238
  }
@@ -12,12 +12,13 @@ package ee.forgr.capacitor_updater;
12
12
  * references: http://stackoverflow.com/questions/12471999/rsa-encryption-decryption-in-android
13
13
  */
14
14
  import android.util.Base64;
15
- import java.io.BufferedInputStream;
16
- import java.io.DataInputStream;
15
+ import java.io.BufferedReader;
17
16
  import java.io.File;
18
17
  import java.io.FileInputStream;
19
18
  import java.io.FileOutputStream;
19
+ import java.io.FileReader;
20
20
  import java.io.IOException;
21
+ import java.io.InputStream;
21
22
  import java.security.GeneralSecurityException;
22
23
  import java.security.InvalidAlgorithmParameterException;
23
24
  import java.security.InvalidKeyException;
@@ -157,26 +158,52 @@ public class CryptoCipher {
157
158
  byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
158
159
 
159
160
  SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
160
- byte[] content = new byte[(int) file.length()];
161
-
162
- try (
163
- final FileInputStream fis = new FileInputStream(file);
164
- final BufferedInputStream bis = new BufferedInputStream(fis);
165
- final DataInputStream dis = new DataInputStream(bis)
166
- ) {
167
- dis.readFully(content);
168
- dis.close();
169
- byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
170
- // write the decrypted string to the file
171
- try (final FileOutputStream fos = new FileOutputStream(file.getAbsolutePath())) {
172
- fos.write(decrypted);
173
- }
174
- }
161
+ decryptAesFile(file, sKey, iv);
175
162
  } catch (GeneralSecurityException e) {
176
163
  logger.info("decryptFile fail");
177
- e.printStackTrace();
178
- throw new IOException("GeneralSecurityException");
164
+ throw new IOException("GeneralSecurityException", e);
165
+ }
166
+ }
167
+
168
+ static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException, GeneralSecurityException {
169
+ if (file.length() == 0) {
170
+ throw new IOException("Empty encrypted data");
171
+ }
172
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
173
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
174
+ File tempFile = File.createTempFile("capgo-aes-", ".tmp", file.getParentFile());
175
+ try {
176
+ byte[] inBuf = new byte[ioBufferBytes()];
177
+ // Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk
178
+ // and 64 workers * 5MB was why AES barely won on heap in local benches.
179
+ byte[] outBuf = new byte[inBuf.length + 16];
180
+ try (FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(tempFile)) {
181
+ int n;
182
+ while ((n = fis.read(inBuf)) != -1) {
183
+ int outLen = cipher.update(inBuf, 0, n, outBuf, 0);
184
+ if (outLen > 0) {
185
+ fos.write(outBuf, 0, outLen);
186
+ }
187
+ }
188
+ int last = cipher.doFinal(outBuf, 0);
189
+ if (last > 0) {
190
+ fos.write(outBuf, 0, last);
191
+ }
192
+ }
193
+ replaceFile(tempFile, file);
194
+ tempFile = null;
195
+ } finally {
196
+ if (tempFile != null && tempFile.exists()) {
197
+ tempFile.delete();
198
+ }
199
+ }
200
+ }
201
+
202
+ static void replaceFile(File from, File to) throws IOException {
203
+ if (from.renameTo(to)) {
204
+ return;
179
205
  }
206
+ throw new IOException("Failed to replace file: " + to.getAbsolutePath());
180
207
  }
181
208
 
182
209
  private static byte[] hexStringToByteArray(String s) {
@@ -303,8 +330,87 @@ public class CryptoCipher {
303
330
  }
304
331
  }
305
332
 
333
+ private static final long TWO_GIB = 2L * 1024 * 1024 * 1024;
334
+ private static final long THREE_GIB = 3L * 1024 * 1024 * 1024;
335
+ private static final long FOUR_GIB = 4L * 1024 * 1024 * 1024;
336
+ private static final long EIGHT_GIB = 8L * 1024 * 1024 * 1024;
337
+ private static final int FLAGSHIP_IO_BUFFER_BYTES = 5 * 1024 * 1024;
338
+ private static volatile long cachedPhysicalRamBytes = -1;
339
+
340
+ // Checksum and copy share one ladder. 64-wide peak RAM = 64 * buffer.
341
+ // <2GB: 64KB. <3GB: 256KB. <4GB: 512KB. <8GB: 1MB. Else 5MB (flagship / unknown).
342
+ static int ioBufferBytes(long physicalRamBytes) {
343
+ if (physicalRamBytes <= 0) {
344
+ return FLAGSHIP_IO_BUFFER_BYTES;
345
+ }
346
+ if (physicalRamBytes < TWO_GIB) {
347
+ return 64 * 1024;
348
+ }
349
+ if (physicalRamBytes < THREE_GIB) {
350
+ return 256 * 1024;
351
+ }
352
+ if (physicalRamBytes < FOUR_GIB) {
353
+ return 512 * 1024;
354
+ }
355
+ if (physicalRamBytes < EIGHT_GIB) {
356
+ return 1024 * 1024;
357
+ }
358
+ return FLAGSHIP_IO_BUFFER_BYTES;
359
+ }
360
+
361
+ static int checksumBufferBytes(long physicalRamBytes) {
362
+ return ioBufferBytes(physicalRamBytes);
363
+ }
364
+
365
+ static int copyBufferBytes(long physicalRamBytes) {
366
+ return ioBufferBytes(physicalRamBytes);
367
+ }
368
+
369
+ static int ioBufferBytes() {
370
+ return ioBufferBytes(physicalRamBytes());
371
+ }
372
+
373
+ static int checksumBufferBytes() {
374
+ return ioBufferBytes(physicalRamBytes());
375
+ }
376
+
377
+ static int copyBufferBytes() {
378
+ return ioBufferBytes(physicalRamBytes());
379
+ }
380
+
381
+ static long physicalRamBytes() {
382
+ long cached = cachedPhysicalRamBytes;
383
+ if (cached >= 0) {
384
+ return cached;
385
+ }
386
+ long parsed = 0;
387
+ try (BufferedReader reader = new BufferedReader(new FileReader("/proc/meminfo"))) {
388
+ String line = reader.readLine();
389
+ if (line != null && line.startsWith("MemTotal:")) {
390
+ String[] parts = line.split("\\s+");
391
+ if (parts.length >= 2) {
392
+ parsed = Long.parseLong(parts[1]) * 1024L;
393
+ }
394
+ }
395
+ } catch (Exception ignored) {
396
+ parsed = 0;
397
+ }
398
+ cachedPhysicalRamBytes = parsed;
399
+ return parsed;
400
+ }
401
+
306
402
  public static String calcChecksum(File file) {
307
- final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
403
+ try (FileInputStream fis = new FileInputStream(file)) {
404
+ return calcChecksum(fis);
405
+ } catch (IOException e) {
406
+ logger.error("Cannot calculate checksum");
407
+ logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
408
+ return "";
409
+ }
410
+ }
411
+
412
+ public static String calcChecksum(InputStream inputStream) {
413
+ final int BUFFER_SIZE = checksumBufferBytes();
308
414
  MessageDigest digest;
309
415
  try {
310
416
  digest = MessageDigest.getInstance("SHA-256");
@@ -313,10 +419,10 @@ public class CryptoCipher {
313
419
  return "";
314
420
  }
315
421
 
316
- try (FileInputStream fis = new FileInputStream(file)) {
422
+ try {
317
423
  byte[] buffer = new byte[BUFFER_SIZE];
318
424
  int length;
319
- while ((length = fis.read(buffer)) != -1) {
425
+ while ((length = inputStream.read(buffer)) != -1) {
320
426
  digest.update(buffer, 0, length);
321
427
  }
322
428
  byte[] hash = digest.digest();
@@ -329,7 +435,7 @@ public class CryptoCipher {
329
435
  return hexString.toString();
330
436
  } catch (IOException e) {
331
437
  logger.error("Cannot calculate checksum");
332
- logger.debug("Path: " + file.getPath() + ", Error: " + e.getMessage());
438
+ logger.debug("Error: " + e.getMessage());
333
439
  return "";
334
440
  }
335
441
  }