Why the file Command Gets the Type Wrong: "Zip archive data" for JAR, APK and DOCX
On this page
Short answer: file reads a few fixed offsets near the start of a file. That is enough for ELF, Mach-O, PE, WebAssembly, SQLite, PNG and PDF. It breaks down for formats defined by what is inside a ZIP (APK, IPA, a JAR not made by jar, a repacked DOCX), for signed blobs such as iOS provisioning profiles, and for damaged files, where it reports the header and cannot know the rest is missing. When xxd shows PK\x03\x04, list the entries; for anything that has to be right, open the file with a parser for its format.
The signature table is in the magic bytes guide. This page covers where the answer was wrong or vague, on 25 sample files, comparing file 5.41 with this site’s file type identifier.
The test set
All 25 files were made on 2026-10-20 on macOS 27.0.1 or taken from this site’s own test fixtures, then copied to s01 … s25 so no extension gives anything away:
| Sample | What it is | How it was made |
|---|---|---|
| s01 | plain ZIP, one text file | zip 3.0 |
| s02 | JAR | javac --release 11 + jar cfe (Temurin 17.0.18) |
| s03 | APK | this site’s APK test fixture (test/apk/fixture.apk) |
| s04 | DOCX | textutil -convert docx |
| s05 | IPA | a synthetic iOS app zipped with zip -r (from the provisioning profile guide) |
| s06 | gzip, 54 bytes | gzip -9 of a 25-byte text file |
| s07 | ELF x86-64 | site fixture |
| s08 | Mach-O arm64 | clang -O2 |
| s09 | PE32+ x64 | site fixture |
| s10 | universal Mach-O | site fixture |
| s11 | Java class | the Hello.class from s02 |
| s12 | WebAssembly | site fixture (clang.wasm) |
| s13 | SQLite | sqlite3, one table |
| s14 | PNG 32 × 32 | site sample |
| s15 | cupsfilter of a text file |
|
| s16 | iOS provisioning profile, DER | openssl cms -sign -nodetach (synthetic) |
| s17 | the same, BER indefinite length | openssl cms -sign -stream |
| s18 | first half of s02 | head -c 373 |
| s19 | first 200 bytes of s14 | head -c 200 |
| s20 | s06 minus its last 8 bytes | head -c 46 |
| s21 | s02 unzipped and re-zipped with zip -r |
|
| s22 | s04 unzipped and re-zipped with zip -r . |
|
| s23 | valid gzip of hi\n, 23 bytes |
gzip -n |
| s24 | first half of a 43,771-byte gzip | seq 1 20000 | gzip -9, then head -c |
| s25 | s05 re-zipped with zip -X |
Versions: file-5.41 with /usr/share/file/magic as shipped with macOS, xxd from macOS, OpenSSL 4.0.3. The site tool’s identify() function, the same code the page’s Web Worker runs, ran under Node.js 22.22.3.
Treat a file you don’t recognize as untrusted: identify it, don’t run it.
Where both tools agreed
ELF, Mach-O (thin and universal), PE32+, Java class, WebAssembly, SQLite, PNG, PDF, gzip and the plain ZIP got the same answer from both, with the same details: x86-64, arm64, PE32+ ... x86-64, version 55.0 (Java SE 11), 32 x 32, version 1.3. The CA FE BA BE collision between universal Mach-O and Java class was handled correctly by both; the next four bytes are 00 00 00 02 (two architectures) in s10 and 00 00 00 37 (class version 55) in s11.
file --mime-type is less consistent than the description line, though. It returned application/octet-stream for the WebAssembly module (whose description it got right) and application/x-java-applet for a plain class file with a main method. If you route uploads by MIME type, check what file --mime-type returns for each format you care about.
Where file was wrong or vague
APK and IPA come out as plain ZIP
$ file s03 s05
s03: Zip archive data, at least v0.0 to extract, compression method=deflate
s05: Zip archive data, at least v1.0 to extract, compression method=store
We then ran file over all 19 APK and AAB fixtures in this site’s repository: every one came back as Zip archive data. Its magic database has no APK rule. It does have an iOS rule, which we found by reading /usr/share/file/magic/archive:
# iOS App
>(26.s+30) leshort !0xcafe
>>26 string !\x8\0\0\0mimetype
>>>30 string Payload/
>>>>38 search/64 .app/ iOS App
The first entry’s name must be Payload/, and .app/ must appear within 64 bytes after offset 38. Our IPA’s first entry is Payload/, but Info-ZIP’s zip adds 28 bytes of extra fields (UT timestamps and ux owner IDs) to every local header, which pushes the second entry’s Payload/Demo.app/ beyond that window. Re-zipped with zip -X (no extra fields), the same app was recognized:
$ file s25
s25: iOS App Zip archive data, at least v1.0 to extract, compression method=store
$ file --mime-type s25
s25: application/x-ios-app
So whether file 5.41 calls an IPA an IPA depends on the zip tool that packed it.
A JAR that wasn’t made by jar
$ file s02 s21
s02: Java archive data (JAR)
s21: Zip archive data, at least v1.0 to extract, compression method=store
Both contain the same META-INF/MANIFEST.MF and Hello.class, and java -jar s21 printed hi. The JAR rule, >(26.s+30) leshort 0xcafe, looks for a 0xCAFE extra field ID on the first entry. The jar tool writes one there:
$ xxd -l 48 hello.jar
00000000: 504b 0304 1400 0808 0800 268f 545d 0000 PK........&.T]..
00000010: 0000 0000 0000 0000 0000 0900 0400 4d45 ..............ME
00000020: 5441 2d49 4e46 2ffe ca00 0003 0050 4b07 TA-INF/......PK.
fe ca 00 00 right after META-INF/ is that marker. Gradle, Maven and zip do not have to write it, so file often gives no answer for a valid JAR.
A repacked DOCX loses its name and its MIME type
$ file s04 s22
s04: Microsoft Word 2007+
s22: Microsoft OOXML
$ file --mime-type s04 s22
s04: application/vnd.openxmlformats-officedocument.wordprocessingml.document
s22: application/octet-stream
The rule in /usr/share/file/magic/msooxml checks that the first entry is [Content_Types].xml, _rels/.rels or docProps, then looks only at the names of the third and fourth entries for word/, xl/ or ppt/. textutil writes [Content_Types].xml, _rels/.rels, then word/_rels/document.xml.rels, so the third entry matches. Our zip -r . copy also starts with [Content_Types].xml, but it adds directory entries and puts docProps/ next; word/ is entry number 8. The description falls back to “Microsoft OOXML”, and the MIME type to application/octet-stream.
A signed iOS profile is “data”
$ file s16 s17
s16: data
s17: data
The bytes are 30 82 0e af 06 09 2a 86 48 86 f7 0d 01 07 02: a DER sequence followed by the signedData object identifier. file 5.41 has no rule for a bare CMS structure. The provisioning profile guide shows how to decode it.
Half a JAR is still a JAR
$ file s18
s18: Java archive data (JAR)
$ unzip -l s18
End-of-central-directory signature not found. Either this file is not
a zipfile, ...
file reads the first local header, and that header survived. The central directory that every unzip tool needs is in the half that is gone. Same with the PNG cut to 200 bytes: file s19 printed PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced, identical to the full image, because the dimensions are in the first chunk.
“truncated” on a valid gzip, nothing on a broken one
Two gzip files:
$ gzip -t s23 && echo OK
OK
$ gzip -t s24
gzip: s24: unexpected end of file
$ file s23 s24
s23: gzip compressed data, from Unix, truncated
s24: gzip compressed data, last modified: Tue Oct 20 10:02:02 2026, max compression, from Unix, original size modulo 2^32 3994873712
The valid file is reported as truncated. The broken one is not, and file printed an “original size” of almost 4 GB for what was a 108,894-byte input. The rule in /usr/share/file/magic/compress explains both:
>>-0 offset >48
>>>-4 ulelong x \b, original size modulo 2^32 %u
>>-0 offset <48 \b, truncated
“truncated” means the file is shorter than 48 bytes, and the size is the last four bytes of the file, whatever they are. Our 23-byte gzip of hi\n is complete but short. The cut file’s last four bytes are compressed data. s20, cut to 46 bytes, also got “truncated”, correct only by accident. Use gzip -t (or xz -t, zstd -t) to test a compressed file.
Reading the ZIP central directory instead
The site’s file type identifier reads the central directory at the end of the file and names the format from the entry list (AndroidManifest.xml with classes.dex or resources.arsc for an APK, Payload/*.app/ for an IPA, META-INF/MANIFEST.MF or .class files for a JAR, word/ for DOCX). That is why it named s03, s05, s21, s22 and s25 correctly where file did not, and called all 14 APK fixtures apk at high confidence. It also gives the specific MIME type (application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/x-ios-app) rather than application/zip.
For the signed profiles it parses the CMS structure in both DER and BER form, and if the signed content is a plist with ProvisionedDevices, ProvisionsAllDevices, TeamIdentifier or Entitlements keys, calls it a profile and links to the iOS profile viewer:
s16: iOS provisioning profile (CMS-signed plist) high encoding DER, 1 certificate
s17: iOS provisioning profile (CMS-signed plist) high encoding BER, indefinite length, 1 certificate
A CMS signature over plain text comes back as CMS / PKCS#7 SignedData with content type 1.2.840.113549.1.7.1.
For damaged files it looks at the end as well as the start. The half JAR came back as ZIP archive, medium, with a note that the central directory could not be read. A PNG has to end with its IEND chunk, and a gzip file of up to 8 MiB is inflated once in the worker (output discarded) to see whether the stream reaches its end, so the cut PNG and the cut gzip files drop to medium with the note “does not end the way this format ends”. The same note appears when data is appended after the end, which some files do on purpose, so read it as “check this” rather than “broken”.
What to do with an unknown file
-
Look at the first 16 bytes with
xxd -l 16. That gives the container, and for executables, images and databases usually the final answer. -
For
PK\x03\x04, list the entries withunzip -lorzipinfo -1, or drop the file on the identifier, which does the same from the central directory. Don’t trustfileto tell APK, IPA, JAR or Office apart; its answer depends on how the archive was packed. For an APK or AAB, see unpacking an APK or AAB. -
For
30 82or30 80, think signed or encoded data: a certificate, a PKCS#12 file, a CMS signature, a provisioning profile.openssl asn1parsenames the outer object identifier, whichfiledid not do for s16:$ openssl asn1parse -inform DER -in s16 | head -3 0:d=0 hl=4 l=3759 cons: SEQUENCE 4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-signedData 15:d=1 hl=4 l=3744 cons: cont [ 0 ] -
Before trusting a compressed or archived file, test it:
gzip -t,unzip -t,xz -t.filedoes not check integrity. The site identifier inflates gzip files up to 8 MiB to the end, which also checks the stored CRC-32, and checks that a PNG ends withIEND, without checking the CRC of each PNG chunk. Other formats get no integrity check. -
Open it with a parser for its format. A header says what the file is supposed to be; a parser fails when the rest is missing.
Limits of this test
25 files is a small set chosen to probe ZIP subtypes and damage. We tested file 5.41, the version macOS ships; newer releases of file may have APK or CMS rules we did not see. And our IPA and profiles are synthetic: real ones exported by Xcode may be packed differently, which, as s05 and s25 showed, is exactly what changes file’s answer.