Files
rea/src/domain/zipPackageFormat.ts
T
3cb9d61ec1 fix(artifacts): describe ZIP non-entry coverage accurately (#2059)
* fix(artifacts): describe ZIP non-entry coverage accurately

* fix(process): report no remaining time after a provider deadline fires

Node timers can fire while performance.now() is still a fraction of a
millisecond short of the monotonic deadline. ProviderStartupDeadline then
reported interruption "timeout" and an aborted signal while remainingMs()
returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0
(Hopper and Ghidra startup) could run past an elapsed deadline, and the
real-timer test "aborts a bounded wait when its real monotonic deadline
expires" failed intermittently in CI (expected 1 to be 0).

Once the deadline's own timer has classified a timeout, report zero
remaining time. A deterministic regression test fires the timer with the
monotonic clock 0.4 ms short of the deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: format comparison and IDA sources

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:37:35 +08:00

64 lines
2.7 KiB
TypeScript

/** ZIP-backed application package families that share the hardened archive reader. */
export const ZIP_PACKAGE_FORMATS = [
"zip",
"ipa",
"apk",
"msix",
"appx",
"hap",
"hsp",
"app-pack",
] as const;
export type ZipPackageFormat = (typeof ZIP_PACKAGE_FORMATS)[number];
/** All ZIP package families share archive ownership and inventory coverage. */
export const isZipPackageFormat = (
format: string,
): format is ZipPackageFormat =>
ZIP_PACKAGE_FORMATS.some((candidate) => candidate === format);
/** Recognize local-header, empty-archive, or split-archive ZIP signatures, not archive validity. */
export const hasZipSignature = (bytes: Uint8Array): boolean =>
bytes.length >= 4 &&
bytes[0] === 0x50 &&
bytes[1] === 0x4b &&
((bytes[2] === 0x03 && bytes[3] === 0x04) ||
(bytes[2] === 0x05 && bytes[3] === 0x06) ||
(bytes[2] === 0x07 && bytes[3] === 0x08));
/**
* Classify a ZIP-backed package by its complete lower-cased path suffix.
*
* This is deliberately extension-only. Callers must separately verify ZIP
* magic before trusting the classification for a root input. HarmonyOS `.har`
* is intentionally absent: the suffix also names HTTP Archive JSON, so a
* Harmony shared archive is only established from verified ZIP bytes. The
* HarmonyOS App Pack suffix `.app` is also absent here: it applies only to
* root inputs (see `classifyAndHashRoot`), because a nested ZIP entry ending
* in `.app` inside another package, such as an IPA, is not an App Pack.
*/
export const zipPackageFormatForPath = (
path: string,
): ZipPackageFormat | undefined => {
const lower = path.toLowerCase();
if (lower.endsWith(".zip")) return "zip";
if (lower.endsWith(".ipa")) return "ipa";
// An Android App Bundle is a ZIP archive, not a standalone APK.
if (lower.endsWith(".apk")) return "apk";
if (lower.endsWith(".msix") || lower.endsWith(".msixbundle")) return "msix";
if (lower.endsWith(".appx") || lower.endsWith(".appxbundle")) return "appx";
if (lower.endsWith(".hap")) return "hap";
if (lower.endsWith(".hsp")) return "hsp";
return undefined;
};
/** Inventory lists entries, not all bytes surrounding the central directory. */
export const ZIP_NON_ENTRY_LIMITATION =
"ZIP inventory lists central-directory entries only. Non-entry bytes, including an APK Signing Block before the central directory or a ZIP comment after it, are not occurrences.";
/** Recognize the same inventory scope in current and previously retained Evidence. */
export const isZipInventoryScopeLimitation = (limitation: string): boolean =>
limitation === ZIP_NON_ENTRY_LIMITATION ||
limitation ===
"ZIP inventory lists central-directory entries only. Bytes after the central directory, including an APK Signing Block or a ZIP comment, are not occurrences.";