Skip to content

Commit eeaa1aa

Browse files
committed
fix: harden android helper manifest and traversal
1 parent e420318 commit eeaa1aa

4 files changed

Lines changed: 58 additions & 6 deletions

File tree

android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,14 @@ private static int appendInteractiveWindowRoots(
156156
if (root == null) {
157157
continue;
158158
}
159-
appendNode(xml, root, windowCount, 0, maxDepth, maxNodes, stats);
159+
StringBuilder windowXml = new StringBuilder();
160+
CaptureStats windowStats = stats.copy();
161+
appendNode(windowXml, root, windowCount, 0, maxDepth, maxNodes, windowStats);
162+
xml.append(windowXml);
163+
stats.copyFrom(windowStats);
160164
windowCount += 1;
165+
} catch (RuntimeException ignored) {
166+
// Accessibility windows can disappear while traversing; keep the rest of the snapshot.
161167
} finally {
162168
if (root != null) {
163169
root.recycle();
@@ -249,8 +255,11 @@ private static void appendNode(
249255
if (child == null) {
250256
continue;
251257
}
252-
appendNode(xml, child, index, depth + 1, maxDepth, maxNodes, stats);
253-
child.recycle();
258+
try {
259+
appendNode(xml, child, index, depth + 1, maxDepth, maxNodes, stats);
260+
} finally {
261+
child.recycle();
262+
}
254263
}
255264
xml.append("</node>");
256265
}
@@ -332,6 +341,18 @@ private static int readIntArgument(Bundle arguments, String name, int fallback)
332341
private static final class CaptureStats {
333342
int nodeCount;
334343
boolean truncated;
344+
345+
CaptureStats copy() {
346+
CaptureStats next = new CaptureStats();
347+
next.nodeCount = nodeCount;
348+
next.truncated = truncated;
349+
return next;
350+
}
351+
352+
void copyFrom(CaptureStats next) {
353+
nodeCount = next.nodeCount;
354+
truncated = next.truncated;
355+
}
335356
}
336357

337358
private static final class CaptureResult {

src/platforms/android/__tests__/snapshot-helper.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const manifest: AndroidSnapshotHelperManifest = {
2121
name: 'android-snapshot-helper',
2222
version: '0.13.3',
2323
apkUrl: null,
24-
sha256: 'abc123',
24+
sha256: 'a'.repeat(64),
2525
packageName: 'com.callstack.agentdevice.snapshothelper',
2626
versionCode: 13003,
2727
instrumentationRunner: 'com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation',
@@ -435,6 +435,16 @@ test('parseAndroidSnapshotHelperManifest validates manifest shape', () => {
435435
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, installArgs: ['shell'] }), {
436436
message: 'Android snapshot helper manifest installArgs must start with "install".',
437437
});
438+
assert.throws(() => parseAndroidSnapshotHelperManifest({ ...manifest, sha256: 'not-a-sha' }), {
439+
message: 'Android snapshot helper manifest sha256 must be a 64-character hex string.',
440+
});
441+
assert.equal(
442+
parseAndroidSnapshotHelperManifest({
443+
...manifest,
444+
sha256: ` ${sha256Text('helper-apk').toUpperCase()} `,
445+
}).sha256,
446+
sha256Text('helper-apk'),
447+
);
438448
});
439449

440450
function helperOutput(options: { chunks: string[]; result: Record<string, string> }): string {

src/platforms/android/__tests__/snapshot.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const helperManifest: AndroidSnapshotHelperManifest = {
4141
name: 'android-snapshot-helper',
4242
version: '0.13.3',
4343
apkUrl: null,
44-
sha256: 'abc123',
44+
sha256: 'a'.repeat(64),
4545
packageName: 'com.callstack.agentdevice.snapshothelper',
4646
versionCode: 13003,
4747
instrumentationRunner: 'com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation',

src/platforms/android/snapshot-helper.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ export function parseAndroidSnapshotHelperManifest(value: unknown): AndroidSnaps
492492
releaseTag: readOptionalString(record.releaseTag),
493493
assetName: readOptionalString(record.assetName),
494494
apkUrl: readOptionalNullableString(record.apkUrl, 'apkUrl'),
495-
sha256: readString(record.sha256, 'sha256').toLowerCase(),
495+
sha256: readSha256(record.sha256),
496496
checksumName: readOptionalString(record.checksumName),
497497
packageName: readString(record.packageName, 'packageName'),
498498
versionCode: readNumber(record.versionCode, 'versionCode'),
@@ -531,6 +531,17 @@ function readAndroidSnapshotHelperManifestInstallArgs(value: unknown): string[]
531531
return installArgs;
532532
}
533533

534+
function readSha256(value: unknown): string {
535+
const sha256 = readString(value, 'sha256').trim().toLowerCase();
536+
if (sha256.length !== 64 || !isLowerHex(sha256)) {
537+
throw new AppError(
538+
'INVALID_ARGS',
539+
'Android snapshot helper manifest sha256 must be a 64-character hex string.',
540+
);
541+
}
542+
return sha256;
543+
}
544+
534545
async function readInstalledVersionCode(
535546
adb: AndroidAdbExecutor,
536547
packageName: string,
@@ -770,3 +781,13 @@ function readStringArray(value: unknown, field: string): string[] {
770781
}
771782
return value;
772783
}
784+
785+
function isLowerHex(value: string): boolean {
786+
for (const char of value) {
787+
const code = char.charCodeAt(0);
788+
const isDigit = code >= 48 && code <= 57;
789+
const isLowerHexLetter = code >= 97 && code <= 102;
790+
if (!isDigit && !isLowerHexLetter) return false;
791+
}
792+
return true;
793+
}

0 commit comments

Comments
 (0)