Skip to content

Commit a804821

Browse files
authored
Add executable and object file signatures (#2)
* Add executable and object file signatures Detect ELF, Mach-O (thin and universal, both byte orders), PE/COFF, WebAssembly, ar, and zstd. PE follows e_lfanew within the 512-byte sniff window. Universal Mach-O is gated on a plausible architecture count so Java class files sharing the CA FE BA BE prefix are not misclassified. Export the Format* constants so callers can switch on Result.Format without string literals. Closes #1 * Drop byte-swapped fat Mach-O signatures Per mach-o/fat.h the fat header is always big-endian on disk; FAT_CIGAM and FAT_CIGAM_64 are memory-order constants for LE readers, not alternative on-disk byte sequences. Keep FAT_MAGIC and FAT_MAGIC_64 gated on the big-endian nfat_arch check and add a negative test for the swapped prefix.
1 parent 49c6144 commit a804821

6 files changed

Lines changed: 255 additions & 84 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,20 @@ mutable package state.
4949

5050
`Kind` is `text`, `binary`, or `unknown`. `Format` and `MIME` describe the
5151
physical content. `Encoding` is set for accepted UTF-8, UTF-16LE, or UTF-16BE
52-
text and never appears as a MIME charset parameter.
52+
text and never appears as a MIME charset parameter. Compare `Format` against
53+
the exported `Format*` constants rather than string literals.
5354

54-
The first format registry contains:
55+
The format registry contains:
5556

56-
- ZIP, TAR, gzip, bzip2, xz, PDF, CFBF, PNG, JPEG, and GIF
57+
- ZIP, TAR, ar, gzip, bzip2, xz, zstd, PDF, CFBF, PNG, JPEG, and GIF
58+
- ELF, Mach-O (thin and universal), PE/COFF, and WebAssembly
5759
- plain text, HTML, XML, and SVG
5860

5961
Detection uses bytes only. ZIP-based package types such as JAR, wheel, and
60-
NuGet remain `zip`, and compressed payloads are not opened. A caller can
62+
NuGet remain `zip`, and compressed payloads are not opened. A `CA FE BA BE`
63+
prefix is reported as Mach-O only when the following architecture count is
64+
plausible, so Java class files fall through unclassified. PE requires the
65+
`PE\0\0` signature to be reachable within the first 512 bytes. A caller can
6166
combine the result with filename or domain rules when it needs a semantic
6267
type.
6368

‎magic.go‎

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,31 @@ type Result struct {
4040
NeedBytes int
4141
}
4242

43+
// Format values reported in Result.Format.
4344
const (
44-
formatText = "text"
45-
formatHTML = "html"
46-
formatXML = "xml"
47-
formatSVG = "svg"
48-
formatZIP = "zip"
49-
formatTAR = "tar"
50-
formatGZIP = "gzip"
51-
formatBZIP2 = "bzip2"
52-
formatXZ = "xz"
53-
formatPDF = "pdf"
54-
formatCFBF = "cfbf"
55-
formatPNG = "png"
56-
formatJPEG = "jpeg"
57-
formatGIF = "gif"
45+
FormatText = "text"
46+
FormatHTML = "html"
47+
FormatXML = "xml"
48+
FormatSVG = "svg"
49+
FormatZIP = "zip"
50+
FormatTAR = "tar"
51+
FormatGZIP = "gzip"
52+
FormatBZIP2 = "bzip2"
53+
FormatXZ = "xz"
54+
FormatZstd = "zstd"
55+
FormatPDF = "pdf"
56+
FormatCFBF = "cfbf"
57+
FormatPNG = "png"
58+
FormatJPEG = "jpeg"
59+
FormatGIF = "gif"
60+
FormatELF = "elf"
61+
FormatMachO = "mach-o"
62+
FormatPE = "pe"
63+
FormatWASM = "wasm"
64+
FormatAR = "ar"
65+
)
5866

67+
const (
5968
mimeText = "text/plain"
6069
mimeHTML = "text/html"
6170
mimeXML = "text/xml"
@@ -70,6 +79,12 @@ const (
7079
mimePNG = "image/png"
7180
mimeJPEG = "image/jpeg"
7281
mimeGIF = "image/gif"
82+
mimeZstd = "application/zstd"
83+
mimeELF = "application/x-elf"
84+
mimeMachO = "application/x-mach-binary"
85+
mimePE = "application/vnd.microsoft.portable-executable"
86+
mimeWASM = "application/wasm"
87+
mimeAR = "application/x-archive"
7388

7489
encodingUTF8 = "utf-8"
7590
encodingUTF16LE = "utf-16le"
@@ -107,7 +122,7 @@ func detect(data []byte, prefix bool) Result {
107122
result.Format = format
108123
result.MIME = mime
109124
} else if result.Kind == KindText {
110-
result.Format = formatText
125+
result.Format = FormatText
111126
result.MIME = mimeText
112127
}
113128

‎magic_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestDetectPrefix(t *testing.T) {
2424
expect: Result{
2525
Kind: KindText,
2626
MIME: mimeText,
27-
Format: formatText,
27+
Format: FormatText,
2828
Encoding: encodingUTF8,
2929
Reason: ReasonNeedMore,
3030
},
@@ -43,7 +43,7 @@ func TestDetectPrefix(t *testing.T) {
4343
expect: Result{
4444
Kind: KindBinary,
4545
MIME: mimePNG,
46-
Format: formatPNG,
46+
Format: FormatPNG,
4747
},
4848
},
4949
{
@@ -124,7 +124,7 @@ func TestDetectDoesNotRetainInput(t *testing.T) {
124124
assertResult(t, got, Result{
125125
Kind: KindText,
126126
MIME: mimeText,
127-
Format: formatText,
127+
Format: FormatText,
128128
Encoding: encodingUTF8,
129129
})
130130
}

‎signatures.go‎

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
package magic
99

10+
import "encoding/binary"
11+
1012
const (
1113
// sniffLength is the furthest byte inspected by any signature. A binary
1214
// rule that reads beyond it must also update prefixResultCanChange.
@@ -17,6 +19,20 @@ const (
1719
tarChecksumTo = 156
1820
xmlCloseLength = 2
1921
octalBase = 8
22+
23+
// peHeaderOffsetAt is the location of the uint32le e_lfanew field in the
24+
// DOS header, which holds the offset of the "PE\0\0" signature.
25+
peHeaderOffsetAt = 0x3c
26+
peSignatureLen = 4
27+
28+
// machOFatArchLimit separates a Mach-O universal binary from a Java
29+
// class file, which share the CA FE BA BE prefix. Bytes 4-7 are the
30+
// big-endian architecture count in a fat header and (minor||major)
31+
// version in a class file; the class-file major version has been at
32+
// least 45 since JDK 1.0.2 while no fat binary approaches that many
33+
// architectures.
34+
machOFatHeaderLen = 8
35+
machOFatArchLimit = 40
2036
)
2137

2238
var htmlSignatures = [...]string{
@@ -43,46 +59,91 @@ func binaryFormat(data []byte) (format, mime string) {
4359
case hasPrefix(data, "PK\x03\x04"),
4460
hasPrefix(data, "PK\x05\x06"),
4561
hasPrefix(data, "PK\x07\x08"):
46-
return formatZIP, mimeZIP
62+
return FormatZIP, mimeZIP
4763
case hasPrefix(data, "\x1f\x8b\x08"):
48-
return formatGZIP, mimeGZIP
64+
return FormatGZIP, mimeGZIP
4965
case len(data) >= 4 &&
5066
hasPrefix(data, "BZh") &&
5167
data[3] >= '1' && data[3] <= '9':
52-
return formatBZIP2, mimeBZIP2
68+
return FormatBZIP2, mimeBZIP2
5369
case hasPrefix(data, "\xfd7zXZ\x00"):
54-
return formatXZ, mimeXZ
70+
return FormatXZ, mimeXZ
5571
case hasPrefix(data, "%PDF-"):
56-
return formatPDF, mimePDF
72+
return FormatPDF, mimePDF
5773
case hasPrefix(data, "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"):
58-
return formatCFBF, mimeCFBF
74+
return FormatCFBF, mimeCFBF
5975
case hasPrefix(data, "\x89PNG\r\n\x1a\n"):
60-
return formatPNG, mimePNG
76+
return FormatPNG, mimePNG
6177
case hasPrefix(data, "\xff\xd8\xff"):
62-
return formatJPEG, mimeJPEG
78+
return FormatJPEG, mimeJPEG
6379
case hasPrefix(data, "GIF87a"), hasPrefix(data, "GIF89a"):
64-
return formatGIF, mimeGIF
80+
return FormatGIF, mimeGIF
81+
case hasPrefix(data, "\x28\xb5\x2f\xfd"):
82+
return FormatZstd, mimeZstd
83+
case hasPrefix(data, "\x7fELF"):
84+
return FormatELF, mimeELF
85+
case hasPrefix(data, "\xcf\xfa\xed\xfe"),
86+
hasPrefix(data, "\xce\xfa\xed\xfe"),
87+
hasPrefix(data, "\xfe\xed\xfa\xcf"),
88+
hasPrefix(data, "\xfe\xed\xfa\xce"):
89+
return FormatMachO, mimeMachO
90+
case machOFatHeader(data):
91+
return FormatMachO, mimeMachO
92+
case hasPrefix(data, "\x00asm"):
93+
return FormatWASM, mimeWASM
94+
case hasPrefix(data, "!<arch>\n"):
95+
return FormatAR, mimeAR
96+
case peHeader(data):
97+
return FormatPE, mimePE
6598
case validTARHeader(data):
66-
return formatTAR, mimeTAR
99+
return FormatTAR, mimeTAR
67100
default:
68101
return "", ""
69102
}
70103
}
71104

105+
func machOFatHeader(data []byte) bool {
106+
if len(data) < machOFatHeaderLen {
107+
return false
108+
}
109+
// Fat headers are always big-endian on disk per mach-o/fat.h; FAT_CIGAM
110+
// is a memory-order constant, not an alternative on-disk signature.
111+
if !hasPrefix(data, "\xca\xfe\xba\xbe") &&
112+
!hasPrefix(data, "\xca\xfe\xba\xbf") {
113+
return false
114+
}
115+
nfat := binary.BigEndian.Uint32(data[4:machOFatHeaderLen])
116+
return nfat > 0 && nfat < machOFatArchLimit
117+
}
118+
119+
func peHeader(data []byte) bool {
120+
if !hasPrefix(data, "MZ") || len(data) < peHeaderOffsetAt+4 {
121+
return false
122+
}
123+
offset := binary.LittleEndian.Uint32(data[peHeaderOffsetAt:])
124+
// Bound to sniffLength so prefixResultCanChange stays correct. PE files
125+
// with a DOS stub larger than the sniff window are not recognised.
126+
if offset < peHeaderOffsetAt+4 || offset > sniffLength-peSignatureLen ||
127+
int(offset)+peSignatureLen > len(data) {
128+
return false
129+
}
130+
return hasPrefix(data[offset:], "PE\x00\x00")
131+
}
132+
72133
func textFormat(data []byte) (format, mime string) {
73134
if len(data) > sniffLength {
74135
data = data[:sniffLength]
75136
}
76137

77138
first := skipWhitespace(data, 0)
78139
if isSVG(data, first) {
79-
return formatSVG, mimeSVG
140+
return FormatSVG, mimeSVG
80141
}
81142
if hasPrefix(data[first:], "<?xml") {
82-
return formatXML, mimeXML
143+
return FormatXML, mimeXML
83144
}
84145
if isHTML(data, first) {
85-
return formatHTML, mimeHTML
146+
return FormatHTML, mimeHTML
86147
}
87148
return "", ""
88149
}

0 commit comments

Comments
 (0)