@capgo/capacitor-updater 8.51.7 → 8.51.9

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.7";
149
+ private final String pluginVersion = "8.51.9";
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);
@@ -523,20 +523,58 @@ public class CapgoUpdater {
523
523
  final boolean isBrotli = fileName.endsWith(".br");
524
524
  final String fileNameWithoutPath = new File(fileName).getName();
525
525
  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
- }
526
+ if (isSafeCacheHash(fileHash)) {
527
+ final File cacheFolder = new File(this.activity.getCacheDir(), "capgo_downloads");
528
+ final File cacheFile = new File(cacheFolder, fileHash + "_" + cacheBaseName);
529
+ // Cache files are named `{hash}_{filename}` and were checksum-verified
530
+ // when written. Re-hashing every hit re-reads the whole bundle and
531
+ // OOMs/janks low-RAM devices during getMissing / delta apply.
532
+ if (isReusableCacheFile(cacheFile, fileHash)) {
533
+ return true;
534
+ }
531
535
 
532
- if (isBrotli) {
533
- final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
534
- return verifyChecksum(legacyCacheFile, fileHash);
536
+ if (isBrotli) {
537
+ final File legacyCacheFile = new File(cacheFolder, fileHash + "_" + fileNameWithoutPath);
538
+ return isReusableCacheFile(legacyCacheFile, fileHash);
539
+ }
535
540
  }
536
541
 
537
542
  return false;
538
543
  }
539
544
 
545
+ static final String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
546
+
547
+ static boolean isSafeCacheHash(final String hash) {
548
+ if (hash == null) {
549
+ return false;
550
+ }
551
+ final int len = hash.length();
552
+ if (len != 64 && len != 8) {
553
+ return false;
554
+ }
555
+ for (int i = 0; i < len; i++) {
556
+ final char c = hash.charAt(i);
557
+ if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
558
+ return false;
559
+ }
560
+ }
561
+ return true;
562
+ }
563
+
564
+ // SHA-256 hash-named cache files were verified when written. Existence is
565
+ // enough for non-empty files; empty files are reused only for the empty SHA-256.
566
+ // CRC32 (8 hex) is too collision-prone to trust without a re-read.
567
+ static boolean isReusableCacheFile(final File file, final String expectedHash) {
568
+ if (file == null || !file.isFile() || !isSafeCacheHash(expectedHash) || expectedHash.length() != 64) {
569
+ return false;
570
+ }
571
+ final long length = file.length();
572
+ if (length > 0) {
573
+ return true;
574
+ }
575
+ return length == 0 && EMPTY_SHA256.equalsIgnoreCase(expectedHash);
576
+ }
577
+
540
578
  public JSONArray getMissingBundleFiles(final JSONArray manifest, final String sessionKey) throws JSONException {
541
579
  final JSONArray missing = new JSONArray();
542
580
  for (int i = 0; i < manifest.length(); i++) {
@@ -1171,19 +1209,22 @@ public class CapgoUpdater {
1171
1209
  throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
1172
1210
  }
1173
1211
 
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);
1212
+ final File tempFile = File.createTempFile("capgo-", ".tmp", parent);
1213
+ try {
1214
+ try (
1215
+ final FileInputStream input = new FileInputStream(source);
1216
+ final FileOutputStream output = new FileOutputStream(tempFile)
1217
+ ) {
1218
+ final byte[] buffer = new byte[CryptoCipher.copyBufferBytes()];
1219
+ int length;
1220
+ while ((length = input.read(buffer)) != -1) {
1221
+ output.write(buffer, 0, length);
1222
+ }
1180
1223
  }
1181
- }
1182
-
1183
- if (!tempFile.renameTo(dest)) {
1184
- if (!dest.delete() || !tempFile.renameTo(dest)) {
1224
+ CryptoCipher.replaceFile(tempFile, dest);
1225
+ } finally {
1226
+ if (tempFile.exists()) {
1185
1227
  tempFile.delete();
1186
- throw new IOException("Failed to replace file: " + dest.getAbsolutePath());
1187
1228
  }
1188
1229
  }
1189
1230
  }
@@ -12,11 +12,11 @@ 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
21
  import java.security.GeneralSecurityException;
22
22
  import java.security.InvalidAlgorithmParameterException;
@@ -157,26 +157,52 @@ public class CryptoCipher {
157
157
  byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
158
158
 
159
159
  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
- }
160
+ decryptAesFile(file, sKey, iv);
175
161
  } catch (GeneralSecurityException e) {
176
162
  logger.info("decryptFile fail");
177
- e.printStackTrace();
178
- throw new IOException("GeneralSecurityException");
163
+ throw new IOException("GeneralSecurityException", e);
164
+ }
165
+ }
166
+
167
+ static void decryptAesFile(File file, SecretKey key, byte[] iv) throws IOException, GeneralSecurityException {
168
+ if (file.length() == 0) {
169
+ throw new IOException("Empty encrypted data");
170
+ }
171
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
172
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
173
+ File tempFile = File.createTempFile("capgo-aes-", ".tmp", file.getParentFile());
174
+ try {
175
+ byte[] inBuf = new byte[ioBufferBytes()];
176
+ // Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk
177
+ // and 64 workers * 5MB was why AES barely won on heap in local benches.
178
+ byte[] outBuf = new byte[inBuf.length + 16];
179
+ try (FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(tempFile)) {
180
+ int n;
181
+ while ((n = fis.read(inBuf)) != -1) {
182
+ int outLen = cipher.update(inBuf, 0, n, outBuf, 0);
183
+ if (outLen > 0) {
184
+ fos.write(outBuf, 0, outLen);
185
+ }
186
+ }
187
+ int last = cipher.doFinal(outBuf, 0);
188
+ if (last > 0) {
189
+ fos.write(outBuf, 0, last);
190
+ }
191
+ }
192
+ replaceFile(tempFile, file);
193
+ tempFile = null;
194
+ } finally {
195
+ if (tempFile != null && tempFile.exists()) {
196
+ tempFile.delete();
197
+ }
198
+ }
199
+ }
200
+
201
+ static void replaceFile(File from, File to) throws IOException {
202
+ if (from.renameTo(to)) {
203
+ return;
179
204
  }
205
+ throw new IOException("Failed to replace file: " + to.getAbsolutePath());
180
206
  }
181
207
 
182
208
  private static byte[] hexStringToByteArray(String s) {
@@ -303,8 +329,77 @@ public class CryptoCipher {
303
329
  }
304
330
  }
305
331
 
332
+ private static final long TWO_GIB = 2L * 1024 * 1024 * 1024;
333
+ private static final long THREE_GIB = 3L * 1024 * 1024 * 1024;
334
+ private static final long FOUR_GIB = 4L * 1024 * 1024 * 1024;
335
+ private static final long EIGHT_GIB = 8L * 1024 * 1024 * 1024;
336
+ private static final int FLAGSHIP_IO_BUFFER_BYTES = 5 * 1024 * 1024;
337
+ private static volatile long cachedPhysicalRamBytes = -1;
338
+
339
+ // Checksum and copy share one ladder. 64-wide peak RAM = 64 * buffer.
340
+ // <2GB: 64KB. <3GB: 256KB. <4GB: 512KB. <8GB: 1MB. Else 5MB (flagship / unknown).
341
+ static int ioBufferBytes(long physicalRamBytes) {
342
+ if (physicalRamBytes <= 0) {
343
+ return FLAGSHIP_IO_BUFFER_BYTES;
344
+ }
345
+ if (physicalRamBytes < TWO_GIB) {
346
+ return 64 * 1024;
347
+ }
348
+ if (physicalRamBytes < THREE_GIB) {
349
+ return 256 * 1024;
350
+ }
351
+ if (physicalRamBytes < FOUR_GIB) {
352
+ return 512 * 1024;
353
+ }
354
+ if (physicalRamBytes < EIGHT_GIB) {
355
+ return 1024 * 1024;
356
+ }
357
+ return FLAGSHIP_IO_BUFFER_BYTES;
358
+ }
359
+
360
+ static int checksumBufferBytes(long physicalRamBytes) {
361
+ return ioBufferBytes(physicalRamBytes);
362
+ }
363
+
364
+ static int copyBufferBytes(long physicalRamBytes) {
365
+ return ioBufferBytes(physicalRamBytes);
366
+ }
367
+
368
+ static int ioBufferBytes() {
369
+ return ioBufferBytes(physicalRamBytes());
370
+ }
371
+
372
+ static int checksumBufferBytes() {
373
+ return ioBufferBytes(physicalRamBytes());
374
+ }
375
+
376
+ static int copyBufferBytes() {
377
+ return ioBufferBytes(physicalRamBytes());
378
+ }
379
+
380
+ static long physicalRamBytes() {
381
+ long cached = cachedPhysicalRamBytes;
382
+ if (cached >= 0) {
383
+ return cached;
384
+ }
385
+ long parsed = 0;
386
+ try (BufferedReader reader = new BufferedReader(new FileReader("/proc/meminfo"))) {
387
+ String line = reader.readLine();
388
+ if (line != null && line.startsWith("MemTotal:")) {
389
+ String[] parts = line.split("\\s+");
390
+ if (parts.length >= 2) {
391
+ parsed = Long.parseLong(parts[1]) * 1024L;
392
+ }
393
+ }
394
+ } catch (Exception ignored) {
395
+ parsed = 0;
396
+ }
397
+ cachedPhysicalRamBytes = parsed;
398
+ return parsed;
399
+ }
400
+
306
401
  public static String calcChecksum(File file) {
307
- final int BUFFER_SIZE = 1024 * 1024 * 5; // 5 MB buffer size
402
+ final int BUFFER_SIZE = checksumBufferBytes();
308
403
  MessageDigest digest;
309
404
  try {
310
405
  digest = MessageDigest.getInstance("SHA-256");