Files
nexus/internal/leviathan/binsnoop/sig_archives.go
T
2026-06-16 11:51:07 +01:00

1144 lines
31 KiB
Go

package binsnoop
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"strconv"
"strings"
)
// ---------------------------------------------------------------------------
// gzip
// ---------------------------------------------------------------------------
const gzipDescription = "gzip compressed data"
func gzipMagic() [][]byte {
return [][]byte{
{0x1f, 0x8b, 0x08},
}
}
func gzipParser(data []byte, offset int) (*SignatureResult, error) {
const (
flagCRC = 0b0000_0010
flagExtra = 0b0000_0100
flagName = 0b0000_1000
flagComment = 0b0001_0000
flagReserved = 0b1110_0000
deflateCompression = 8
crcSize = 2
nullByteSize = 1
)
knownOSIDs := map[int]string{
0: "FAT filesystem (MS-DOS, OS/2, NT/Win32",
1: "Amiga",
2: "VMS (or OpenVMS)",
3: "Unix",
4: "VM/CMS",
5: "Atari TOS",
6: "HPFS filesystem (OS/2, NT)",
7: "Macintosh",
8: "Z-System",
9: "CP/M",
10: "TOPS-20",
11: "NTFS filesystem (NT)",
12: "QDOS",
13: "Acorn RISCOS",
255: "unknown",
}
gzipHeaderFields := []StructField{
{"magic", "u16"},
{"compression_method", "u8"},
{"flags", "u8"},
{"timestamp", "u32"},
{"extra_flags", "u8"},
{"osid", "u8"},
}
gzipExtraHeaderFields := []StructField{
{"id", "u16"},
{"extra_data_len", "u16"},
}
if offset >= len(data) {
return nil, fmt.Errorf("gzip: offset out of bounds")
}
headerData := data[offset:]
hdr, err := Parse(headerData, gzipHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("gzip: failed to parse header")
}
if hdr["flags"]&flagReserved != 0 {
return nil, fmt.Errorf("gzip: reserved flags set")
}
if hdr["compression_method"] != deflateCompression {
return nil, fmt.Errorf("gzip: invalid compression method %d", hdr["compression_method"])
}
osName, ok := knownOSIDs[hdr["osid"]]
if !ok {
return nil, fmt.Errorf("gzip: unknown OS ID %d", hdr["osid"])
}
headerSize := StructSize(gzipHeaderFields)
// Handle optional extra data
if hdr["flags"]&flagExtra != 0 {
extraHeaderSize := StructSize(gzipExtraHeaderFields)
extraStart := headerSize
extraEnd := extraStart + extraHeaderSize
if extraEnd > len(headerData) {
return nil, fmt.Errorf("gzip: not enough data for extra header")
}
extraHdr, err := Parse(headerData[extraStart:], gzipExtraHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("gzip: failed to parse extra header")
}
headerSize += extraHeaderSize + extraHdr["extra_data_len"]
}
// Handle optional original file name (NULL-terminated)
originalName := ""
if hdr["flags"]&flagName != 0 {
if headerSize >= len(headerData) {
return nil, fmt.Errorf("gzip: not enough data for original name")
}
originalName = GetCString(headerData[headerSize:])
headerSize += len(originalName) + nullByteSize
}
// Handle optional comment (NULL-terminated)
if hdr["flags"]&flagComment != 0 {
if headerSize >= len(headerData) {
return nil, fmt.Errorf("gzip: not enough data for comment")
}
comment := GetCString(headerData[headerSize:])
headerSize += len(comment) + nullByteSize
}
// Handle optional header CRC
if hdr["flags"]&flagCRC != 0 {
headerSize += crcSize
}
if len(headerData) < headerSize {
return nil, fmt.Errorf("gzip: data shorter than header")
}
originalFileNameText := ""
if originalName != "" {
originalFileNameText = fmt.Sprintf(" original file name: \"%s\",", originalName)
}
description := fmt.Sprintf("%s,%s operating system: %s, timestamp: %s",
gzipDescription,
originalFileNameText,
osName,
EpochToString(uint32(hdr["timestamp"])),
)
return &SignatureResult{
Offset: offset,
Size: 0, // size unknown without decompression
Description: description,
Confidence: ConfidenceMedium,
}, nil
}
// ---------------------------------------------------------------------------
// zip
// ---------------------------------------------------------------------------
const zipDescription = "ZIP archive"
func zipMagic() [][]byte {
return [][]byte{
{0x50, 0x4b, 0x03, 0x04}, // PK\x03\x04
}
}
func zipParser(data []byte, offset int) (*SignatureResult, error) {
return parseZipLike(data, offset, zipDescription)
}
// parseZipLike is shared between zip and dahua_zip parsers.
func parseZipLike(data []byte, offset int, descPrefix string) (*SignatureResult, error) {
const (
unusedFlagsMask = 0b11010111_10000000
compressionEncrypted = 99
)
zipLocalFileFields := []StructField{
{"magic", "u32"},
{"version", "u16"},
{"flags", "u16"},
{"compression", "u16"},
{"modification_time", "u16"},
{"modification_date", "u16"},
{"crc", "u32"},
{"compressed_size", "u32"},
{"uncompressed_size", "u32"},
{"file_name_len", "u16"},
{"extra_field_len", "u16"},
}
allowedCompressionMethods := map[int]bool{
0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true,
8: true, 9: true, 10: true, 12: true, 14: true, 18: true,
19: true, 20: true, 93: true, 94: true, 95: true, 96: true,
97: true, 98: true, compressionEncrypted: true,
}
if offset >= len(data) {
return nil, fmt.Errorf("zip: offset out of bounds")
}
zipData := data[offset:]
hdr, err := Parse(zipData, zipLocalFileFields, "little")
if err != nil {
return nil, fmt.Errorf("zip: failed to parse local file header")
}
if hdr["flags"]&unusedFlagsMask != 0 {
return nil, fmt.Errorf("zip: unused flags set")
}
if !allowedCompressionMethods[hdr["compression"]] {
return nil, fmt.Errorf("zip: unsupported compression method %d", hdr["compression"])
}
versionMajor := hdr["version"] / 10
versionMinor := hdr["version"] % 10
localHeaderSize := StructSize(zipLocalFileFields) + hdr["file_name_len"] + hdr["extra_field_len"]
dataSize := hdr["compressed_size"]
if dataSize == 0 {
dataSize = hdr["uncompressed_size"]
}
firstEntryTotalSize := localHeaderSize + dataSize
// Try to find the end-of-central-directory header
eocdMagic := []byte("PK\x05\x06\x00\x00\x00\x00")
eocdFields := []StructField{
{"magic", "u32"},
{"disk_number", "u16"},
{"central_directory_disk_number", "u16"},
{"central_directory_disk_entries", "u16"},
{"central_directory_total_entries", "u16"},
{"central_directory_size", "u32"},
{"central_directory_offset", "u32"},
{"comment_length", "u16"},
}
totalSize := 0
fileCount := 0
missingEOCD := false
// Search for EOCD in data after offset
searchData := data[offset:]
eocdIdx := -1
for i := 0; i+len(eocdMagic) <= len(searchData); i++ {
if bytes.Equal(searchData[i:i+len(eocdMagic)], eocdMagic) {
eocdData := searchData[i:]
eocdHdr, err := Parse(eocdData, eocdFields, "little")
if err != nil {
continue
}
if eocdHdr["central_directory_disk_entries"] == eocdHdr["central_directory_total_entries"] &&
eocdHdr["central_directory_total_entries"] > 0 {
eocdSize := StructSize(eocdFields) + eocdHdr["comment_length"]
eocdIdx = i
totalSize = eocdIdx + eocdSize
fileCount = eocdHdr["central_directory_total_entries"]
break
}
}
}
if eocdIdx < 0 {
// No EOCD found — walk local file entries
missingEOCD = true
availableData := len(data) - offset
var previousOffset *int
nextOffset := offset + firstEntryTotalSize
for IsOffsetSafe(availableData, nextOffset, previousOffset) {
entryHdr, err := Parse(data[nextOffset:], zipLocalFileFields, "little")
if err != nil {
totalSize = nextOffset - offset
break
}
if entryHdr["flags"]&unusedFlagsMask != 0 || !allowedCompressionMethods[entryHdr["compression"]] {
totalSize = nextOffset - offset
break
}
entryHeaderSize := StructSize(zipLocalFileFields) + entryHdr["file_name_len"] + entryHdr["extra_field_len"]
entryDataSize := entryHdr["compressed_size"]
if entryDataSize == 0 {
entryDataSize = entryHdr["uncompressed_size"]
}
prev := nextOffset
previousOffset = &prev
nextOffset += entryHeaderSize + entryDataSize
}
if totalSize == 0 {
totalSize = nextOffset - offset
}
}
if totalSize <= 0 {
return nil, fmt.Errorf("zip: could not determine archive size")
}
var description string
if missingEOCD {
description = fmt.Sprintf("%s, version: %d.%d, missing end-of-central-directory header, total size: %d bytes",
descPrefix, versionMajor, versionMinor, totalSize)
} else {
description = fmt.Sprintf("%s, version: %d.%d, file count: %d, total size: %d bytes",
descPrefix, versionMajor, versionMinor, fileCount, totalSize)
}
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: description,
Confidence: ConfidenceHigh,
}, nil
}
// ---------------------------------------------------------------------------
// tarball
// ---------------------------------------------------------------------------
const tarballDescription = "POSIX tar archive"
const (
tarballBlockSize = 512
tarballMagicOffset = 257
tarballMagicSize = 5
tarballSizeOffset = 124
tarballSizeLen = 11
tarballMinExpectedHdrs = 10
tarballChecksumStart = 148
tarballChecksumEnd = 156
)
var tarballUniversalMagic = []byte("ustar")
func tarballMagic() [][]byte {
return [][]byte{
{0x75, 0x73, 0x74, 0x61, 0x72, 0x00}, // ustar\x00
{0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00}, // ustar\x20\x20\x00
}
}
func tarballParser(data []byte, offset int) (*SignatureResult, error) {
// The magic bytes are at offset 257 within a tar header block
tarballStartOffset := offset - tarballMagicOffset
if tarballStartOffset < 0 || tarballStartOffset >= len(data) {
return nil, fmt.Errorf("tarball: invalid start offset")
}
var tarballTotalSize int
var validHeaderCount int
nextHeaderStart := tarballStartOffset
var previousHeaderStart *int
availableData := len(data)
for IsOffsetSafe(availableData, nextHeaderStart, previousHeaderStart) {
nextHeaderEnd := nextHeaderStart + tarballBlockSize
if nextHeaderEnd > len(data) {
break
}
headerBlock := data[nextHeaderStart:nextHeaderEnd]
if !tarballHeaderChecksumValid(headerBlock) {
break
}
validHeaderCount++
entrySize, err := tarballEntrySize(headerBlock)
if err != nil {
break
}
tarballTotalSize += entrySize
prev := nextHeaderStart
previousHeaderStart = &prev
nextHeaderStart += entrySize
}
if tarballTotalSize < tarballBlockSize {
return nil, fmt.Errorf("tarball: archive too small")
}
confidence := ConfidenceMedium
if validHeaderCount >= tarballMinExpectedHdrs {
confidence = ConfidenceHigh
}
return &SignatureResult{
Offset: tarballStartOffset,
Size: tarballTotalSize,
Description: fmt.Sprintf("%s, file count: %d", tarballDescription, validHeaderCount),
Confidence: confidence,
}, nil
}
func tarballHeaderChecksumValid(headerBlock []byte) bool {
checksumValueBytes := headerBlock[tarballChecksumStart:tarballChecksumEnd]
reportedChecksum := tarballOctal(checksumValueBytes)
var sum int
for i, b := range headerBlock {
if i >= tarballChecksumStart && i < tarballChecksumEnd {
sum += 0x20
} else {
sum += int(b)
}
}
return sum == reportedChecksum
}
func tarballEntrySize(entryData []byte) (int, error) {
entryMagic := entryData[tarballMagicOffset : tarballMagicOffset+tarballMagicSize]
if !bytes.Equal(entryMagic, tarballUniversalMagic) {
return 0, fmt.Errorf("tarball: invalid entry magic")
}
entrySizeBytes := entryData[tarballSizeOffset : tarballSizeOffset+tarballSizeLen]
reportedEntrySize := tarballOctal(entrySizeBytes)
blockCount := 1 + int(math.Ceil(float64(reportedEntrySize)/float64(tarballBlockSize)))
return blockCount * tarballBlockSize, nil
}
func tarballOctal(octalStr []byte) int {
var num int
for _, c := range octalStr {
if c < 0x30 || c > 0x39 {
break
}
num = num*8 + int(c) - 0x30
}
return num
}
// ---------------------------------------------------------------------------
// 7-zip (sevenzip)
// ---------------------------------------------------------------------------
const sevenzipDescription = "7-zip archive data"
func sevenzipMagic() [][]byte {
return [][]byte{
{0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c}, // 7z\xbc\xaf\x27\x1c
}
}
func sevenzipParser(data []byte, offset int) (*SignatureResult, error) {
const (
sevenzipCRCStart = 12
sevenzipHeaderSize = 32
)
sevenzipFields := []StructField{
{"magic_p1", "u16"},
{"magic_p2", "u32"},
{"major_version", "u8"},
{"minor_version", "u8"},
{"header_crc", "u32"},
{"next_header_offset", "u64"},
{"next_header_size", "u64"},
{"next_header_crc", "u32"},
}
if offset >= len(data) {
return nil, fmt.Errorf("7zip: offset out of bounds")
}
szData := data[offset:]
hdr, err := Parse(szData, sevenzipFields, "little")
if err != nil {
return nil, fmt.Errorf("7zip: failed to parse header")
}
// Validate the header CRC (over bytes 12..32)
if sevenzipHeaderSize > len(szData) {
return nil, fmt.Errorf("7zip: not enough data for header CRC")
}
crcData := szData[sevenzipCRCStart:sevenzipHeaderSize]
calculatedHeaderCRC := CRC32(crcData)
if calculatedHeaderCRC != uint32(hdr["header_crc"]) {
return nil, fmt.Errorf("7zip: header CRC mismatch")
}
// Validate the next_header CRC
nextCRCStart := offset + sevenzipHeaderSize + hdr["next_header_offset"]
nextCRCEnd := nextCRCStart + hdr["next_header_size"]
if nextCRCEnd > len(data) {
return nil, fmt.Errorf("7zip: not enough data for next header CRC")
}
nextCRCData := data[nextCRCStart:nextCRCEnd]
calculatedNextCRC := CRC32(nextCRCData)
if int(calculatedNextCRC) != hdr["next_header_crc"] {
return nil, fmt.Errorf("7zip: next header CRC mismatch")
}
totalSize := sevenzipHeaderSize + hdr["next_header_offset"] + hdr["next_header_size"]
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: fmt.Sprintf("%s, version %d.%d, total size: %d bytes", sevenzipDescription, hdr["major_version"], hdr["minor_version"], totalSize),
Confidence: ConfidenceHigh,
}, nil
}
// ---------------------------------------------------------------------------
// RAR
// ---------------------------------------------------------------------------
const rarDescription = "RAR archive"
func rarMagic() [][]byte {
return [][]byte{
{0x52, 0x61, 0x72, 0x21, 0x1a, 0x07}, // Rar!\x1A\x07
}
}
func rarParser(data []byte, offset int) (*SignatureResult, error) {
archiveHeaderFields := []StructField{
{"magic_p1", "u32"},
{"magic_p2", "u16"},
{"version", "u8"},
}
// version field 0 => RARv4, 1 => RARv5
versionMap := map[int]int{0: 4, 1: 5}
if offset >= len(data) {
return nil, fmt.Errorf("rar: offset out of bounds")
}
hdr, err := Parse(data[offset:], archiveHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("rar: failed to parse header")
}
version, ok := versionMap[hdr["version"]]
if !ok {
return nil, fmt.Errorf("rar: unknown version %d", hdr["version"])
}
// EOF markers for RARv4 and RARv5
eofMarkers := map[int][]byte{
4: {0xc4, 0x3d, 0x7b, 0x00, 0x40, 0x07, 0x00},
5: {0x1d, 0x77, 0x56, 0x51, 0x03, 0x05, 0x04, 0x00},
}
totalSize := 0
extraDesc := ""
if marker, ok := eofMarkers[version]; ok {
searchData := data[offset:]
idx := bytes.Index(searchData, marker)
if idx >= 0 {
totalSize = idx + len(marker)
} else {
extraDesc = " (failed to locate RAR EOF)"
}
}
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: fmt.Sprintf("%s, version: %d, total size: %d bytes%s", rarDescription, version, totalSize, extraDesc),
Confidence: ConfidenceMedium,
}, nil
}
// ---------------------------------------------------------------------------
// CAB (Microsoft Cabinet)
// ---------------------------------------------------------------------------
const cabDescription = "Microsoft Cabinet archive"
func cabMagic() [][]byte {
return [][]byte{
{0x4d, 0x53, 0x43, 0x46, 0x00, 0x00, 0x00, 0x00}, // MSCF\x00\x00\x00\x00
}
}
func cabParser(data []byte, offset int) (*SignatureResult, error) {
const (
majorVersion = 1
minorVersion = 3
cabStructSize = 40
cabExtraStructSize = 20
flagExtraDataPresent = 4
)
cabHeaderFields := []StructField{
{"magic", "u32"},
{"reserved1", "u32"},
{"size", "u32"},
{"reserved2", "u32"},
{"first_file_offset", "u32"},
{"reserved3", "u32"},
{"minor_version", "u8"},
{"major_version", "u8"},
{"folder_count", "u16"},
{"file_count", "u16"},
{"flags", "u16"},
{"id", "u16"},
{"set_number", "u16"},
{"extra_header_size", "u16"},
{"cbCFFolder", "u8"},
{"cbCFData", "u8"},
}
cabExtraHeaderFields := []StructField{
{"unknown1", "u32"},
{"data_offset", "u32"},
{"data_size", "u32"},
{"unknown2", "u32"},
{"unknown3", "u32"},
}
if offset >= len(data) {
return nil, fmt.Errorf("cab: offset out of bounds")
}
hdr, err := Parse(data[offset:], cabHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("cab: failed to parse header")
}
// All reserved fields must be 0
if hdr["reserved1"] != 0 || hdr["reserved2"] != 0 || hdr["reserved3"] != 0 {
return nil, fmt.Errorf("cab: reserved fields not zero")
}
// Version must be 1.3
if hdr["major_version"] != majorVersion || hdr["minor_version"] != minorVersion {
return nil, fmt.Errorf("cab: invalid version %d.%d", hdr["major_version"], hdr["minor_version"])
}
totalSize := hdr["size"]
headerSize := cabStructSize
// Handle extra data flag
if hdr["flags"]&flagExtraDataPresent != 0 && hdr["extra_header_size"] == cabExtraStructSize {
extraStart := cabStructSize
extraEnd := extraStart + cabExtraStructSize
if offset+extraEnd > len(data) {
return nil, fmt.Errorf("cab: not enough data for extra header")
}
extraHdr, err := Parse(data[offset+extraStart:], cabExtraHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("cab: failed to parse extra header")
}
if extraHdr["data_offset"] != hdr["size"] {
return nil, fmt.Errorf("cab: extra data offset mismatch")
}
totalSize += extraHdr["data_size"]
_ = headerSize
} else if hdr["flags"]&flagExtraDataPresent != 0 {
return nil, fmt.Errorf("cab: extra data flag set but wrong extra header size")
}
availableData := len(data) - offset
if totalSize > availableData {
return nil, fmt.Errorf("cab: reported size exceeds available data")
}
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: fmt.Sprintf("%s, file count: %d, folder count: %d, header size: %d, total size: %d bytes", cabDescription, hdr["file_count"], hdr["folder_count"], cabStructSize, totalSize),
Confidence: ConfidenceMedium,
}, nil
}
// ---------------------------------------------------------------------------
// Debian package (deb)
// ---------------------------------------------------------------------------
const debDescription = "Debian package file"
func debMagic() [][]byte {
return [][]byte{
// "!<arch>\ndebian-binary "
{0x21, 0x3c, 0x61, 0x72, 0x63, 0x68, 0x3e, 0x0a,
0x64, 0x65, 0x62, 0x69, 0x61, 0x6e, 0x2d, 0x62,
0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x20, 0x20},
}
}
func debParser(data []byte, offset int) (*SignatureResult, error) {
const (
endMarkerSize = 2
dataFileSizeLen = 10
dataFileSizeOffset = 48
controlFileSizeEnd = 130
controlFileSizeStart = 120
)
if offset >= len(data) {
return nil, fmt.Errorf("deb: offset out of bounds")
}
debData := data[offset:]
if len(debData) < controlFileSizeEnd {
return nil, fmt.Errorf("deb: not enough data for control file size")
}
// Read control file size as ASCII decimal
controlFileSizeBytes := debData[controlFileSizeStart:controlFileSizeEnd]
controlFileSizeStr := strings.TrimSpace(string(controlFileSizeBytes))
controlFileSize, err := strconv.Atoi(controlFileSizeStr)
if err != nil {
return nil, fmt.Errorf("deb: invalid control file size: %s", controlFileSizeStr)
}
// Calculate data file size location
dataFileSizeStart := controlFileSizeEnd + endMarkerSize + controlFileSize + dataFileSizeOffset
dataFileSizeEnd := dataFileSizeStart + dataFileSizeLen
if dataFileSizeEnd > len(debData) {
return nil, fmt.Errorf("deb: not enough data for data file size")
}
dataFileSizeBytes := debData[dataFileSizeStart:dataFileSizeEnd]
dataFileSizeStr := strings.TrimSpace(string(dataFileSizeBytes))
dataFileSize, err := strconv.Atoi(dataFileSizeStr)
if err != nil {
return nil, fmt.Errorf("deb: invalid data file size: %s", dataFileSizeStr)
}
totalSize := dataFileSizeEnd + endMarkerSize + dataFileSize
if totalSize > len(debData) {
return nil, fmt.Errorf("deb: reported size exceeds available data")
}
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: fmt.Sprintf("%s, total size: %d bytes", debDescription, totalSize),
Confidence: ConfidenceHigh,
}, nil
}
// ---------------------------------------------------------------------------
// CPIO
// ---------------------------------------------------------------------------
const cpioDescription = "CPIO ASCII archive"
const cpioHeaderSize = 110
func cpioMagic() [][]byte {
return [][]byte{
{0x30, 0x37, 0x30, 0x37, 0x30, 0x31}, // 070701
{0x30, 0x37, 0x30, 0x37, 0x30, 0x32}, // 070702
}
}
func cpioParser(data []byte, offset int) (*SignatureResult, error) {
const eofMarker = "TRAILER!!!"
if offset >= len(data) {
return nil, fmt.Errorf("cpio: offset out of bounds")
}
validMagics := cpioMagic()
var headerCount int
var totalSize int
nextHeaderOffset := offset
var previousHeaderOffset *int
availableData := len(data)
for IsOffsetSafe(availableData, nextHeaderOffset, previousHeaderOffset) {
if nextHeaderOffset >= len(data) {
break
}
cpioEntryData := data[nextHeaderOffset:]
entryHeader, err := parseCPIOEntryHeader(cpioEntryData)
if err != nil {
break
}
// Validate magic
magicValid := false
for _, m := range validMagics {
if bytes.Equal(entryHeader.magic, m) {
magicValid = true
break
}
}
if !magicValid {
break
}
headerCount++
totalSize += entryHeader.headerSize + entryHeader.dataSize
if entryHeader.fileName == eofMarker {
if headerCount > 1 {
return &SignatureResult{
Offset: offset,
Size: totalSize,
Description: fmt.Sprintf("%s, file count: %d", cpioDescription, headerCount-1),
Confidence: ConfidenceHigh,
}, nil
}
break
}
prev := nextHeaderOffset
previousHeaderOffset = &prev
nextHeaderOffset = offset + totalSize
}
return nil, fmt.Errorf("cpio: no valid archive found")
}
type cpioEntry struct {
magic []byte
fileName string
headerSize int
dataSize int
}
func parseCPIOEntryHeader(cpioData []byte) (*cpioEntry, error) {
const (
nullByteSize = 1
cpioMagicStart = 0
cpioMagicEnd = 6
fileSizeStart = 54
fileSizeEnd = 62
fileNameSizeStart = 94
fileNameSizeEnd = 102
)
if len(cpioData) <= cpioHeaderSize {
return nil, fmt.Errorf("cpio: not enough data")
}
headerMagic := make([]byte, cpioMagicEnd-cpioMagicStart)
copy(headerMagic, cpioData[cpioMagicStart:cpioMagicEnd])
// Get file data size (ASCII hex)
fileSizeStr := string(cpioData[fileSizeStart:fileSizeEnd])
fileDataSize, err := strconv.ParseInt(fileSizeStr, 16, 64)
if err != nil {
return nil, fmt.Errorf("cpio: invalid file data size")
}
// Get file name size (ASCII hex)
fileNameSizeStr := string(cpioData[fileNameSizeStart:fileNameSizeEnd])
fileNameSize, err := strconv.ParseInt(fileNameSizeStr, 16, 64)
if err != nil {
return nil, fmt.Errorf("cpio: invalid file name size")
}
fileNameStart := cpioHeaderSize
fileNameEnd := fileNameStart + int(fileNameSize) - nullByteSize
if fileNameEnd > len(cpioData) || fileNameEnd < fileNameStart {
return nil, fmt.Errorf("cpio: file name out of bounds")
}
fileName := string(cpioData[fileNameStart:fileNameEnd])
headerTotalSize := cpioHeaderSize + int(fileNameSize)
return &cpioEntry{
magic: headerMagic,
fileName: fileName,
headerSize: headerTotalSize + cpioBytePadding(headerTotalSize),
dataSize: int(fileDataSize) + cpioBytePadding(int(fileDataSize)),
}, nil
}
func cpioBytePadding(n int) int {
mod := n % 4
if mod == 0 {
return 0
}
return 4 - mod
}
// ---------------------------------------------------------------------------
// ARJ
// ---------------------------------------------------------------------------
const arjDescription = "ARJ archive data"
func arjMagic() [][]byte {
return [][]byte{
{0x60, 0xea},
}
}
func arjParser(data []byte, offset int) (*SignatureResult, error) {
arjHeaderFields := []StructField{
{"magic", "u16"},
{"basic_header_size", "u16"},
{"extra_header_size", "u8"},
{"archiver_version", "u8"},
{"min_version", "u8"},
{"host_os", "u8"},
{"internal_flags", "u8"},
{"compression_method", "u8"},
{"file_type", "u8"},
{"reserved1", "u8"},
{"datetime_file", "u32"},
{"compressed_filesize", "u32"},
{"original_filesize", "u32"},
}
if offset >= len(data) {
return nil, fmt.Errorf("arj: offset out of bounds")
}
hdr, err := Parse(data[offset:], arjHeaderFields, "little")
if err != nil {
return nil, fmt.Errorf("arj: failed to parse header")
}
// Version validation
if hdr["archiver_version"] < 1 || hdr["archiver_version"] > 16 ||
hdr["min_version"] < 1 || hdr["min_version"] > 16 ||
hdr["archiver_version"] < hdr["min_version"] {
return nil, fmt.Errorf("arj: invalid version info")
}
// Flags
flags := "no password"
if hdr["internal_flags"]&0x01 != 0 {
flags = "password"
}
if hdr["internal_flags"]&0x04 != 0 {
flags += "|multi-volume"
}
if hdr["internal_flags"]&0x10 != 0 {
flags += "|slash-switched"
}
if hdr["internal_flags"]&0x20 != 0 {
flags += "|backup"
}
// Host OS
hostOSMap := map[int]string{
0: "MS-DOS", 1: "PRIMOS", 2: "UNIX", 3: "AMIGA", 4: "MAX-OS",
5: "OS/2", 6: "APPLE GS", 7: "ATARI ST", 8: "NeXT", 9: "VAX VMS",
}
hostOS, ok := hostOSMap[hdr["host_os"]]
if !ok {
return nil, fmt.Errorf("arj: unknown host OS %d", hdr["host_os"])
}
// Compression method
compressionMethodMap := map[int]string{
0: "stored", 1: "compressed most", 2: "compressed",
3: "compressed faster", 4: "compressed fastest",
}
compressionMethod, ok := compressionMethodMap[hdr["compression_method"]]
if !ok {
return nil, fmt.Errorf("arj: unknown compression method %d", hdr["compression_method"])
}
// File type
fileTypeMap := map[int]string{
0: "binary", 1: "7-bit text", 2: "comment header",
3: "directory", 4: "volume label",
}
fileType, ok := fileTypeMap[hdr["file_type"]]
if !ok {
return nil, fmt.Errorf("arj: unknown file type %d", hdr["file_type"])
}
// Compressed/uncompressed sizes — check for negative as signed i32
compressedFileSize := int32(hdr["compressed_filesize"])
if compressedFileSize < 0 {
return nil, fmt.Errorf("arj: negative compressed file size")
}
uncompressedFileSize := int32(hdr["original_filesize"])
if uncompressedFileSize < 0 {
return nil, fmt.Errorf("arj: negative uncompressed file size")
}
// Original file name
headerSize := hdr["extra_header_size"]
originalName := ""
if headerSize+4 < len(data[offset:]) {
originalName = GetCString(data[offset+headerSize+4:])
}
originalFileDate := EpochToString(uint32(hdr["datetime_file"]))
availableData := len(data) - offset
if headerSize > availableData {
return nil, fmt.Errorf("arj: header size exceeds available data")
}
extractionDeclined := fileType != "comment header"
return &SignatureResult{
Offset: offset,
Size: headerSize,
Description: fmt.Sprintf(
"%s, header size: %d, version %d, minimum version to extract: %d, flags: %s, compression method: %s, file type: %s, original name: %s, original file date: %s, compressed file size: %d, uncompressed file size: %d, os: %s",
arjDescription,
headerSize,
hdr["archiver_version"],
hdr["min_version"],
flags,
compressionMethod,
fileType,
originalName,
originalFileDate,
compressedFileSize,
uncompressedFileSize,
hostOS,
),
Confidence: ConfidenceMedium,
ExtractionDeclined: extractionDeclined,
}, nil
}
// ---------------------------------------------------------------------------
// ISO 9660
// ---------------------------------------------------------------------------
const iso9660Description = "ISO9660 primary volume"
func iso9660Magic() [][]byte {
return [][]byte{
{0x01, 0x43, 0x44, 0x30, 0x30, 0x31, 0x01, 0x00}, // \x01CD001\x01\x00
}
}
func iso9660Parser(data []byte, offset int) (*SignatureResult, error) {
const (
isoMagicOffset = 32768
isoStructStart = 32840
)
isoFields := []StructField{
{"unused1", "u64"},
{"volume_size_lsb", "u32"},
{"volume_size_msb", "u32"},
{"unused2", "u64"},
{"unused3", "u64"},
{"unused4", "u64"},
{"unused5", "u64"},
{"set_size_lsb", "u16"},
{"set_size_msb", "u16"},
{"sequence_number_lsb", "u16"},
{"sequence_number_msb", "u16"},
{"block_size_lsb", "u16"},
{"block_size_msb", "u16"},
{"path_table_size_lsb", "u32"},
{"path_table_size_msb", "u32"},
}
if offset < isoMagicOffset {
return nil, fmt.Errorf("iso9660: not enough data before magic offset")
}
isoStart := offset - isoMagicOffset
if isoStart+isoStructStart >= len(data) {
return nil, fmt.Errorf("iso9660: not enough data for header")
}
isoHeaderData := data[isoStart+isoStructStart:]
hdr, err := Parse(isoHeaderData, isoFields, "little")
if err != nil {
return nil, fmt.Errorf("iso9660: failed to parse header")
}
// Unused fields must be 0
if hdr["unused1"] != 0 || hdr["unused2"] != 0 || hdr["unused3"] != 0 ||
hdr["unused4"] != 0 || hdr["unused5"] != 0 {
return nil, fmt.Errorf("iso9660: unused fields not zero")
}
// Validate byte-swapped field pairs
if hdr["set_size_lsb"] != int(swapU16(uint16(hdr["set_size_msb"]))) {
return nil, fmt.Errorf("iso9660: set_size mismatch")
}
if hdr["block_size_lsb"] != int(swapU16(uint16(hdr["block_size_msb"]))) {
return nil, fmt.Errorf("iso9660: block_size mismatch")
}
if hdr["volume_size_lsb"] != int(swapU32(uint32(hdr["volume_size_msb"]))) {
return nil, fmt.Errorf("iso9660: volume_size mismatch")
}
if hdr["sequence_number_lsb"] != int(swapU16(uint16(hdr["sequence_number_msb"]))) {
return nil, fmt.Errorf("iso9660: sequence_number mismatch")
}
if hdr["path_table_size_lsb"] != int(swapU32(uint32(hdr["path_table_size_msb"]))) {
return nil, fmt.Errorf("iso9660: path_table_size mismatch")
}
imageSize := hdr["volume_size_lsb"] * hdr["block_size_lsb"]
return &SignatureResult{
Offset: isoStart,
Size: imageSize,
Description: fmt.Sprintf("%s, total size: %d bytes", iso9660Description, imageSize),
Confidence: ConfidenceHigh,
}, nil
}
// swapU16 swaps bytes of a uint16 (big-endian to native / vice versa)
func swapU16(v uint16) uint16 {
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, v)
return binary.LittleEndian.Uint16(b)
}
// swapU32 swaps bytes of a uint32
func swapU32(v uint32) uint32 {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, v)
return binary.LittleEndian.Uint32(b)
}
// ---------------------------------------------------------------------------
// Dahua ZIP
// ---------------------------------------------------------------------------
const dahuaZipDescription = "Dahua ZIP archive"
func dahuaZipMagic() [][]byte {
return [][]byte{
{0x44, 0x48, 0x03, 0x04}, // DH\x03\x04
}
}
func dahuaZipParser(data []byte, offset int) (*SignatureResult, error) {
// Parse as a normal ZIP file, then replace the description prefix
result, err := parseZipLike(data, offset, zipDescription)
if err != nil {
return nil, fmt.Errorf("dahua_zip: %v", err)
}
result.Description = strings.Replace(result.Description, zipDescription, dahuaZipDescription, 1)
return result, nil
}