Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,16 @@ than string literals.
The format registry contains:

- ZIP, TAR, native PHAR, ar, gzip, bzip2, xz, zstd, PDF, CFBF, PNG, JPEG, and GIF
- ELF, Mach-O (thin and universal), PE/COFF, and WebAssembly
- ELF, Mach-O (thin and universal), PE/COFF, WebAssembly, and Java class files
- plain text, Git LFS pointers, JSON, HTML, XML, and SVG

Detection uses bytes only. ZIP-based package types such as JAR, wheel, and
NuGet remain `zip`, and compressed payloads are not opened. A `CA FE BA BE`
prefix is reported as Mach-O only when the following architecture count is
plausible, so Java class files fall through unclassified. PE requires the
`PE\0\0` signature to be reachable within the first 512 bytes. A caller can
combine the result with filename or domain rules when it needs a semantic
type.
plausible. Java class detection checks the major version and a nonzero constant
pool count in the ten-byte header. PE requires the `PE\0\0` signature to be
reachable within the first 512 bytes. A caller can combine the result with
filename or domain rules when it needs a semantic type.

Text accepts valid UTF-8 and BOM-marked UTF-16 or UTF-32. Tab, line feed, form feed,
carriage return, and escape are the permitted C0 controls. Other C0 controls
Expand Down
46 changes: 24 additions & 22 deletions magic.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,36 @@ const (
FormatGIF = "gif"
FormatELF = "elf"
FormatMachO = "mach-o"
FormatJavaClass = "java-class"
FormatPE = "pe"
FormatWASM = "wasm"
FormatAR = "ar"
)

const (
mimeText = "text/plain"
mimeHTML = "text/html"
mimeXML = "text/xml"
mimeSVG = "image/svg+xml"
mimeJSON = "application/json"
mimeZIP = "application/zip"
mimeTAR = "application/x-tar"
mimePHAR = "application/x-phar"
mimeGZIP = "application/gzip"
mimeBZIP2 = "application/x-bzip2"
mimeXZ = "application/x-xz"
mimePDF = "application/pdf"
mimeCFBF = "application/x-ole-storage"
mimePNG = "image/png"
mimeJPEG = "image/jpeg"
mimeGIF = "image/gif"
mimeZstd = "application/zstd"
mimeELF = "application/x-elf"
mimeMachO = "application/x-mach-binary"
mimePE = "application/vnd.microsoft.portable-executable"
mimeWASM = "application/wasm"
mimeAR = "application/x-archive"
mimeText = "text/plain"
mimeHTML = "text/html"
mimeXML = "text/xml"
mimeSVG = "image/svg+xml"
mimeJSON = "application/json"
mimeZIP = "application/zip"
mimeTAR = "application/x-tar"
mimePHAR = "application/x-phar"
mimeGZIP = "application/gzip"
mimeBZIP2 = "application/x-bzip2"
mimeXZ = "application/x-xz"
mimePDF = "application/pdf"
mimeCFBF = "application/x-ole-storage"
mimePNG = "image/png"
mimeJPEG = "image/jpeg"
mimeGIF = "image/gif"
mimeZstd = "application/zstd"
mimeELF = "application/x-elf"
mimeMachO = "application/x-mach-binary"
mimeJavaClass = "application/java-vm"
mimePE = "application/vnd.microsoft.portable-executable"
mimeWASM = "application/wasm"
mimeAR = "application/x-archive"
)

// Encoding values reported in Result.Encoding.
Expand Down
12 changes: 12 additions & 0 deletions signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
// architectures.
machOFatHeaderLen = 8
machOFatArchLimit = 40
classHeaderLen = 10
classMinMajor = 45
)

var htmlSignatures = [...]string{
Expand Down Expand Up @@ -94,6 +96,8 @@ func binaryFormatState(data []byte) (format, mime string, needMore bool) {
return FormatMachO, mimeMachO, false
case machOFatHeader(data):
return FormatMachO, mimeMachO, false
case javaClassHeader(data):
return FormatJavaClass, mimeJavaClass, false
case hasPrefix(data, "\x00asm"):
return FormatWASM, mimeWASM, false
case hasPrefix(data, "!<arch>\n"):
Expand All @@ -114,6 +118,14 @@ func binaryFormatState(data []byte) (format, mime string, needMore bool) {
}
}

func javaClassHeader(data []byte) bool {
if len(data) < classHeaderLen || !hasPrefix(data, "\xca\xfe\xba\xbe") {
return false
}
return binary.BigEndian.Uint16(data[6:8]) >= classMinMajor &&
binary.BigEndian.Uint16(data[8:classHeaderLen]) > 0
}

func machOFatHeader(data []byte) bool {
if len(data) < machOFatHeaderLen {
return false
Expand Down
40 changes: 40 additions & 0 deletions signatures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"fmt"
"os"
"testing"
)

Expand Down Expand Up @@ -60,6 +61,45 @@ func TestBinaryFormatRegistry(t *testing.T) {
}
}

func TestJavaClassDetection(t *testing.T) {
t.Parallel()
class, err := os.ReadFile("testdata/InventoryExample.class")
if err != nil {
t.Fatal(err)
}
for _, detect := range []func([]byte) Result{Detect, DetectPrefix} {
got := detect(class)
if got.Format != FormatJavaClass || got.MIME != mimeJavaClass || got.Kind != KindBinary || got.Reason != ReasonNone {
t.Fatalf("compiled class: %#v", got)
}
}
for length := 0; length < classHeaderLen; length++ {
got := DetectPrefix(class[:length])
if got.Format == FormatJavaClass || got.Reason != ReasonNeedMore {
t.Fatalf("prefix length %d: %#v", length, got)
}
}
for _, data := range [][]byte{
[]byte("\xca\xfe\xba\xbe\x00\x00\x00\x02\x00\x01"),
[]byte("\xca\xfe\xba\xbf\x00\x00\x00\x02\x00\x01"),
[]byte("\xca\xfe\xba\xbe\x00\x00\x00\x34\x00\x00"),
[]byte("\xca\xfe\xba\xbe\x00\x00\x00\x2c\x00\x01"),
} {
if got := Detect(data); got.Format == FormatJavaClass {
t.Fatalf("non-class header: %#v", got)
}
}
for _, data := range [][]byte{
[]byte("\xca\xfe\xba\xbe\x00\x00\x00\x2d\x00\x01"),
[]byte("\xca\xfe\xba\xbe\x00\x03\x00\x2d\x00\x02"),
[]byte("\xca\xfe\xba\xbe\xff\xff\x00\x41\x00\x02"),
} {
if got := Detect(data); got.Format != FormatJavaClass {
t.Fatalf("class version: %#v", got)
}
}
}

func TestTruncatedBinarySignaturesDoNotMatch(t *testing.T) {
t.Parallel()

Expand Down
Binary file added testdata/InventoryExample.class
Binary file not shown.
5 changes: 5 additions & 0 deletions testdata/InventoryExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public class InventoryExample {
public static String message() {
return "inventory fixture";
}
}
Loading