Tencent/tinker · error · ZipException
Local file header offset is after central directory
Error message
Local file header offset is after central directory
What it means
While reading the central directory in the constructor, TinkerZipFile sanity-checks each entry's localHeaderRelOffset against centralDirOffset: a local file header must live before the central directory in a well-formed zip. An offset >= centralDirOffset means the archive is internally inconsistent (offsets were rewritten without fixing the central directory, or the CD was moved), so it throws ZipException('Local file header offset is after central directory') instead of reading garbage later.
Source
Thrown at third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java:484
if (commentLength > 0) {
byte[] commentBytes = new byte[commentLength];
raf.readFully(commentBytes);
comment = new String(commentBytes, 0, commentBytes.length, StandardCharsets.UTF_8);
}
// Seek to the first CDE and read all entries.
// We have to do this now (from the constructor) rather than lazily because the
// public API doesn't allow us to throw IOException except from the constructor
// or from getInputStream.
RAFStream rafStream = new RAFStream(raf, centralDirOffset);
BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096);
byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.
for (int i = 0; i < numEntries; ++i) {
TinkerZipEntry newEntry = new TinkerZipEntry(hdrBuf, bufferedStream, StandardCharsets.UTF_8,
(false) /* isZip64 */);
if (newEntry.localHeaderRelOffset >= centralDirOffset) {
throw new ZipException("Local file header offset is after central directory");
}
String entryName = newEntry.getName();
if (entries.put(entryName, newEntry) != null) {
throw new ZipException("Duplicate entry name: " + entryName);
}
}
}
// private final CloseGuard guard = CloseGuard.get();
static class EocdRecord {
final long numEntries;
final long centralDirOffset;
final int commentLength;
EocdRecord(long numEntries, long centralDirOffset, int commentLength) {
this.numEntries = numEntries;
this.centralDirOffset = centralDirOffset;
this.commentLength = commentLength;View on GitHub (pinned to 1b7ea02c23)
Solutions
- Rebuild the archive from its contents with a trusted tool (unzip to a temp dir, re-zip) so all offsets are regenerated consistently.
- Identify which upstream step rewrote the file (repackager, obfuscator, signer) and fix or update it.
- Run 'unzip -t' / 'zip -T' on inputs before processing to catch inconsistent archives early.
Example fix
// before TinkerZipFile zf = new TinkerZipFile(editedApk); // offsets corrupted by a repack tool // after File rebuilt = rebuildArchive(editedApk); // unzip to tmp, re-zip with a standard tool TinkerZipFile zf = new TinkerZipFile(rebuilt);
Defensive patterns
Strategy: try-catch
Validate before calling
// full pre-validation is costly; rely on independent verification instead:
// unzip -t returns nonzero on inconsistent archives — run it on untrusted inputs
Process p = Runtime.getRuntime().exec(new String[]{"unzip", "-t", file.getPath()});
if (p.waitFor() != 0) {
throw new IOException("Archive fails integrity check: " + file);
} Try / catch
try {
new TinkerZipFile(file);
} catch (ZipException e) {
if (e.getMessage().contains("after central directory")) {
throw new IOException("Archive has inconsistent offsets (bad repack?): " + file, e);
}
throw e;
} Prevention
- Do not strip/insert zip entries with hand-rolled offset math — use a mature zip library.
- Run 'unzip -t' on archives produced by custom build tooling before shipping.
- After any repackaging step, regenerate the central directory from scratch.
When it happens
Trigger: Opening a zip whose central-directory entries carry stale/inflated local header offsets — typically after a tool deleted or inserted entries and rewrote only part of the metadata, after zip64->zip32 conversion bugs, or after naive binary splicing of two archives.
Common situations: APK/patch post-processing steps that strip entries (signature removal, dex pruning) but corrupt offsets; archives edited by buggy repackagers; hand-rolled zip merge tools.
Related errors
- file name:${filename}, file size${fileSize}, entry name:${en
- Not a zip archive
- Duplicate entry name: ${entryName}
- zipEntry is null when get from oldApk
- Expected ${DEX_IN_JAR_NAME} in ${file}
AI-assisted analysis of Tencent/tinker@1b7ea02c23 (2026-08-14).
Data as JSON: /api/errors/b18919f00c6eb3dd.
Report an issue: GitHub.