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

877 lines
25 KiB
Go

package analysis
import (
"bytes"
"compress/gzip"
"context"
"encoding/binary"
"io"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// Signature describes a carveable object. `validate` is run against
// the candidate start and returns the object length if it is a real
// object, or 0 to reject. `trailer`, if present, must appear before
// the object ends (for simple trailer-based terminations).
type Signature struct {
Kind string
Magic []byte
Extension string
// Anchor is how many bytes into the object the magic appears. It is 0 for
// almost every format (magic at offset 0), but a few — notably ISO-BMFF
// (MP4/MOV), whose `ftyp` magic follows a 4-byte box size — carry it.
Anchor int64
Validate func(d *document.Document, start, max int64) (length int64, confidence float64)
}
// carveSignatures is the phase-1 signature set. We stay conservative
// and only declare a carve when we can determine length reliably.
var carveSignatures = []Signature{
{Kind: "png", Magic: []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, Extension: "png", Validate: validatePNG},
{Kind: "jpeg", Magic: []byte{0xff, 0xd8, 0xff}, Extension: "jpg", Validate: validateJPEG},
{Kind: "gif87a", Magic: []byte("GIF87a"), Extension: "gif", Validate: validateGIF},
{Kind: "gif89a", Magic: []byte("GIF89a"), Extension: "gif", Validate: validateGIF},
{Kind: "zip", Magic: []byte{'P', 'K', 0x03, 0x04}, Extension: "zip", Validate: validateZIP},
{Kind: "pdf", Magic: []byte("%PDF-"), Extension: "pdf", Validate: validatePDF},
{Kind: "elf", Magic: []byte{0x7f, 'E', 'L', 'F'}, Extension: "elf", Validate: validateELF},
{Kind: "mz", Magic: []byte{'M', 'Z'}, Extension: "exe", Validate: validatePE},
{Kind: "gzip", Magic: []byte{0x1f, 0x8b, 0x08}, Extension: "gz", Validate: validateGZIP},
{Kind: "bzip2", Magic: []byte{'B', 'Z', 'h'}, Extension: "bz2", Validate: validateBzip2},
{Kind: "sevenz", Magic: []byte{'7', 'z', 0xbc, 0xaf, 0x27, 0x1c}, Extension: "7z", Validate: validate7z},
{Kind: "rar4", Magic: []byte{'R', 'a', 'r', '!', 0x1a, 0x07, 0x00}, Extension: "rar", Validate: validateRAR},
{Kind: "rar5", Magic: []byte{'R', 'a', 'r', '!', 0x1a, 0x07, 0x01, 0x00}, Extension: "rar", Validate: validateRAR},
{Kind: "wav", Magic: []byte("RIFF"), Extension: "wav", Validate: validateRIFF},
{Kind: "mach-o-le32", Magic: []byte{0xce, 0xfa, 0xed, 0xfe}, Extension: "macho", Validate: validateMachO},
{Kind: "mach-o-le64", Magic: []byte{0xcf, 0xfa, 0xed, 0xfe}, Extension: "macho", Validate: validateMachO},
{Kind: "mach-o-be32", Magic: []byte{0xfe, 0xed, 0xfa, 0xce}, Extension: "macho", Validate: validateMachO},
{Kind: "mach-o-be64", Magic: []byte{0xfe, 0xed, 0xfa, 0xcf}, Extension: "macho", Validate: validateMachO},
{Kind: "sqlite", Magic: []byte("SQLite format 3\x00"), Extension: "sqlite", Validate: validateSQLite},
{Kind: "mp3", Magic: []byte("ID3"), Extension: "mp3", Validate: validateID3},
{Kind: "ogg", Magic: []byte("OggS"), Extension: "ogg", Validate: validateOgg},
{Kind: "matroska", Magic: []byte{0x1a, 0x45, 0xdf, 0xa3}, Extension: "mkv", Validate: validateMatroska},
{Kind: "flac", Magic: []byte("fLaC"), Extension: "flac", Validate: validateFLAC},
{Kind: "bmp", Magic: []byte{'B', 'M'}, Extension: "bmp", Validate: validateBMP},
{Kind: "mp4", Magic: []byte("ftyp"), Anchor: 4, Extension: "mp4", Validate: validateMP4},
}
// Carve scans the document for known signatures and returns a list of
// discovered objects with offset, length, kind, and confidence.
func Carve(d *document.Document, start, length int64, maxHits int) ([]map[string]any, error) {
return CarveContext(context.Background(), d, start, length, maxHits)
}
func CarveContext(ctx context.Context, d *document.Document, start, length int64, maxHits int) ([]map[string]any, error) {
if length < 0 {
length = d.Size() - start
}
if maxHits <= 0 {
maxHits = 10_000
}
results := make([]map[string]any, 0, 64)
// Build a flat list of (magicBytes, sigIndex) for a simple first-pass scan.
// Phase 1 uses per-signature naive scan chunks.
for idx, sig := range carveSignatures {
if err := ctx.Err(); err != nil {
return nil, err
}
hits, err := SearchBytesContext(ctx, d, start, length, sig.Magic, maxHits)
if err != nil {
return nil, err
}
for _, h := range hits {
if err := ctx.Err(); err != nil {
return nil, err
}
// The object begins Anchor bytes before the magic (0 for most formats).
objStart := h.Offset - sig.Anchor
if objStart < start {
continue
}
ln, conf := int64(0), 0.4
if sig.Validate != nil {
ln, conf = sig.Validate(d, objStart, start+length)
}
if ln <= 0 {
// record low-confidence magic hit only for kinds where length is not determinable
if sig.Kind == "mz" || sig.Kind == "elf" || sig.Kind == "mach-o-le32" ||
sig.Kind == "mach-o-le64" || sig.Kind == "mach-o-be32" || sig.Kind == "mach-o-be64" ||
sig.Kind == "sqlite" || sig.Kind == "mp3" || sig.Kind == "matroska" ||
sig.Kind == "flac" || sig.Kind == "bzip2" || sig.Kind == "rar4" || sig.Kind == "rar5" {
results = append(results, map[string]any{
"kind": sig.Kind,
"extension": sig.Extension,
"offset": objStart,
"length": int64(0),
"confidence": 0.3,
"status": "magic-only",
"sigIndex": idx,
})
}
continue
}
results = append(results, map[string]any{
"kind": sig.Kind,
"extension": sig.Extension,
"offset": objStart,
"length": ln,
"confidence": conf,
"status": "valid",
"sigIndex": idx,
})
if len(results) >= maxHits {
return results, nil
}
}
}
return results, nil
}
// --- validators ---
func validatePNG(d *document.Document, start, max int64) (int64, float64) {
// Walk chunks until IEND
off := start + 8
for off < max {
hdr, err := d.Range(off, 8)
if err != nil || len(hdr) < 8 {
return 0, 0
}
size := int64(binary.BigEndian.Uint32(hdr[0:4]))
typ := string(hdr[4:8])
// Skip payload + CRC (4 bytes)
off += 8 + size + 4
if typ == "IEND" {
return off - start, 0.95
}
if off > max {
return 0, 0
}
}
return 0, 0
}
func validateJPEG(d *document.Document, start, max int64) (int64, float64) {
// Walk segments until EOI (0xFFD9)
off := start + 2
for off < max-1 {
buf, err := d.Range(off, 2)
if err != nil || len(buf) < 2 {
return 0, 0
}
if buf[0] != 0xff {
return 0, 0
}
marker := buf[1]
off += 2
// No-length markers
if marker == 0xd9 { // EOI
return off - start, 0.95
}
if marker == 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker == 0x00 {
continue
}
// Read length
lb, err := d.Range(off, 2)
if err != nil || len(lb) < 2 {
return 0, 0
}
ln := int64(binary.BigEndian.Uint16(lb))
off += ln
if marker == 0xda { // SOS, scan until next 0xff!=0
for off < max {
b, err := d.Range(off, 2)
if err != nil || len(b) < 2 {
return 0, 0
}
if b[0] == 0xff && b[1] != 0x00 && !(b[1] >= 0xd0 && b[1] <= 0xd7) {
break
}
off++
}
}
}
return 0, 0
}
func validateGIF(d *document.Document, start, max int64) (int64, float64) {
// Minimal: locate trailer 0x3B
buf, err := d.Range(start, min64(max-start, 1<<20))
if err != nil {
return 0, 0
}
idx := bytes.IndexByte(buf[13:], 0x3b)
if idx < 0 {
return 0, 0
}
return int64(idx + 13 + 1), 0.7
}
func validateZIP(d *document.Document, start, max int64) (int64, float64) {
// Search forward for End of Central Directory record 0x06054b50.
const sig = "\x50\x4b\x05\x06"
limit := min64(max-start, 64<<20)
buf, err := d.Range(start, limit)
if err != nil {
return 0, 0
}
idx := bytes.LastIndex(buf, []byte(sig))
if idx < 0 {
return 0, 0
}
// EOCD record is at least 22 bytes; read comment length.
if idx+22 > len(buf) {
return 0, 0
}
commentLen := int(binary.LittleEndian.Uint16(buf[idx+20 : idx+22]))
end := int64(idx + 22 + commentLen)
return end, 0.9
}
func validatePDF(d *document.Document, start, max int64) (int64, float64) {
limit := min64(max-start, 64<<20)
buf, err := d.Range(start, limit)
if err != nil {
return 0, 0
}
idx := bytes.LastIndex(buf, []byte("%%EOF"))
if idx < 0 {
return 0, 0
}
return int64(idx + 5), 0.85
}
func validateELF(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 64)
if err != nil || len(hdr) < 52 {
return 0, 0
}
if hdr[4] != 1 && hdr[4] != 2 {
return 0, 0
}
is64 := hdr[4] == 2
little := hdr[5] == 1
var end int64
if is64 {
if len(hdr) < 64 {
return 0, 0
}
// e_shoff at 40, e_shentsize at 58, e_shnum at 60
off := binary.LittleEndian.Uint64(hdr[40:48])
entSz := uint64(binary.LittleEndian.Uint16(hdr[58:60]))
num := uint64(binary.LittleEndian.Uint16(hdr[60:62]))
if !little {
off = binary.BigEndian.Uint64(hdr[40:48])
entSz = uint64(binary.BigEndian.Uint16(hdr[58:60]))
num = uint64(binary.BigEndian.Uint16(hdr[60:62]))
}
end = int64(off + entSz*num)
} else {
off := binary.LittleEndian.Uint32(hdr[32:36])
entSz := uint32(binary.LittleEndian.Uint16(hdr[46:48]))
num := uint32(binary.LittleEndian.Uint16(hdr[48:50]))
if !little {
off = binary.BigEndian.Uint32(hdr[32:36])
entSz = uint32(binary.BigEndian.Uint16(hdr[46:48]))
num = uint32(binary.BigEndian.Uint16(hdr[48:50]))
}
end = int64(off) + int64(entSz)*int64(num)
}
if end <= 0 || start+end > max {
return 0, 0.5
}
return end, 0.9
}
func validatePE(d *document.Document, start, max int64) (int64, float64) {
// e_lfanew at offset 0x3C points to PE header
hdr, err := d.Range(start, 64)
if err != nil || len(hdr) < 64 {
return 0, 0
}
peOff := int64(binary.LittleEndian.Uint32(hdr[0x3c:0x40]))
if peOff < 0x40 || start+peOff+4 > max {
return 0, 0
}
sig, err := d.Range(start+peOff, 4)
if err != nil || string(sig) != "PE\x00\x00" {
return 0, 0.3
}
coff, err := d.Range(start+peOff+4, 20)
if err != nil || len(coff) < 20 {
return 0, 0.4
}
numSections := int(binary.LittleEndian.Uint16(coff[2:4]))
sizeOpt := int64(binary.LittleEndian.Uint16(coff[16:18]))
if numSections <= 0 || numSections > 96 || sizeOpt < 0 || sizeOpt > 4096 {
return 0, 0.5
}
sectOff := start + peOff + 24 + sizeOpt
end := peOff + 24 + sizeOpt + int64(numSections*40)
for i := 0; i < numSections; i++ {
sh, err := d.Range(sectOff+int64(i*40), 40)
if err != nil || len(sh) < 40 {
return 0, 0.5
}
rawSize := int64(binary.LittleEndian.Uint32(sh[16:20]))
rawPtr := int64(binary.LittleEndian.Uint32(sh[20:24]))
if rawSize < 0 || rawPtr < 0 {
return 0, 0.5
}
if rawSize > 0 && rawPtr+rawSize > end {
end = rawPtr + rawSize
}
}
if end <= peOff || start+end > max {
return 0, 0.5
}
return end, 0.85
}
func validateGZIP(d *document.Document, start, max int64) (int64, float64) {
limit := min64(max-start, 64<<20)
buf, err := d.Range(start, limit)
if err != nil || len(buf) < 18 {
return 0, 0
}
br := bytes.NewReader(buf)
zr, err := gzip.NewReader(br)
if err != nil {
return 0, 0
}
zr.Multistream(false)
if _, err := io.Copy(io.Discard, io.LimitReader(zr, 512<<20)); err != nil {
_ = zr.Close()
return 0, 0
}
if err := zr.Close(); err != nil {
return 0, 0
}
consumed := int64(len(buf)) - int64(br.Len())
if consumed <= 10 || start+consumed > max {
return 0, 0
}
return consumed, 0.9
}
// validateBzip2 locates the bit-aligned end-of-stream marker (0x177245385090,
// the "sqrt(pi)" magic) and returns the stream length including the trailing
// 32-bit combined CRC. bzip2 carries no length in its header, so this is the
// only reliable way to bound the object.
func validateBzip2(d *document.Document, start, max int64) (int64, float64) {
if start+4 > max {
return 0, 0.4
}
hdr, err := d.Range(start, 4)
if err != nil || len(hdr) < 4 || hdr[0] != 'B' || hdr[1] != 'Z' || hdr[2] != 'h' || hdr[3] < '1' || hdr[3] > '9' {
return 0, 0
}
limit := min64(max-start, 64<<20)
buf, err := d.Range(start, limit)
if err != nil || len(buf) < 14 {
return 0, 0.4
}
const eos uint64 = 0x177245385090
const mask48 uint64 = (1 << 48) - 1
var w uint64
for i := 0; i < len(buf); i++ {
w = (w << 8) | uint64(buf[i])
// Check all eight bit alignments ending at or before this byte.
for s := 0; s < 8; s++ {
if (w>>uint(s))&mask48 == eos {
endBit := (int64(i)+1)*8 - int64(s) + 32 // include stream CRC
endByte := (endBit + 7) / 8
if endByte > 14 && start+endByte <= max {
return endByte, 0.85
}
}
}
}
return 0, 0.4
}
func validate7z(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 32)
if err != nil || len(hdr) < 32 {
return 0, 0
}
nextHdrOff := binary.LittleEndian.Uint64(hdr[12:20])
nextHdrSz := binary.LittleEndian.Uint64(hdr[20:28])
end := int64(32 + nextHdrOff + nextHdrSz)
if end <= 0 || start+end > max {
return 0, 0.5
}
return end, 0.85
}
// validateRAR walks the block headers of a RAR4 or RAR5 archive to the
// end-of-archive marker. The two formats differ structurally, so the version
// is detected from the signature byte at offset 6.
func validateRAR(d *document.Document, start, max int64) (int64, float64) {
sig, err := d.Range(start, 8)
if err != nil || len(sig) < 8 {
return 0, 0
}
if sig[6] == 0x01 {
return validateRAR5(d, start, max)
}
return validateRAR4(d, start, max)
}
func validateRAR4(d *document.Document, start, max int64) (int64, float64) {
pos := start + 7 // 7-byte signature
for n := 0; n < 100000; n++ {
h, err := d.Range(pos, 11)
if err != nil || len(h) < 7 {
return 0, 0.4
}
flags := binary.LittleEndian.Uint16(h[3:5])
headSize := int64(binary.LittleEndian.Uint16(h[5:7]))
if headSize < 7 {
return 0, 0.4
}
blockSize := headSize
if flags&0x8000 != 0 { // ADD_SIZE present
if len(h) < 11 {
return 0, 0.4
}
blockSize += int64(binary.LittleEndian.Uint32(h[7:11]))
}
pos += blockSize
if h[2] == 0x7b { // terminator block
if pos > start && pos <= max {
return pos - start, 0.85
}
return 0, 0.4
}
if pos > max {
return 0, 0.4
}
}
return 0, 0.4
}
func validateRAR5(d *document.Document, start, max int64) (int64, float64) {
pos := start + 8 // 8-byte signature
for n := 0; n < 100000; n++ {
// Each block: CRC32(4) + vint(header_size) + header_size bytes [+ data].
h, err := d.Range(pos, 4+11)
if err != nil || len(h) < 5 {
return 0, 0.4
}
headerSize, vlen := rar5Vint(h, 4)
if vlen == 0 || headerSize == 0 {
return 0, 0.4
}
hdrStart := pos + 4 + int64(vlen)
body, err := d.Range(hdrStart, int64(headerSize))
if err != nil || int64(len(body)) < int64(headerSize) {
return 0, 0.4
}
htype, t1 := rar5Vint(body, 0)
hflags, t2 := rar5Vint(body, t1)
p := t1 + t2
var dataSize uint64
if hflags&0x0001 != 0 { // extra area present
_, te := rar5Vint(body, p)
p += te
}
if hflags&0x0002 != 0 { // data area present
dataSize, _ = rar5Vint(body, p)
}
pos = hdrStart + int64(headerSize) + int64(dataSize)
if htype == 5 { // end-of-archive header
if pos > start && pos <= max {
return pos - start, 0.85
}
return 0, 0.4
}
if pos > max {
return 0, 0.4
}
}
return 0, 0.4
}
// rar5Vint decodes a RAR5 little-endian base-128 variable integer.
func rar5Vint(buf []byte, pos int) (uint64, int) {
var v uint64
shift := uint(0)
for n := 0; pos+n < len(buf) && n < 10; n++ {
b := buf[pos+n]
v |= uint64(b&0x7f) << shift
if b&0x80 == 0 {
return v, n + 1
}
shift += 7
}
return 0, 0
}
func validateRIFF(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 12)
if err != nil || len(hdr) < 12 {
return 0, 0
}
size := int64(binary.LittleEndian.Uint32(hdr[4:8]))
return size + 8, 0.9
}
func validateMachO(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 32)
if err != nil || len(hdr) < 28 {
return 0, 0
}
var (
bo binary.ByteOrder
is64 bool
hdrSize int64 = 28
)
switch binary.BigEndian.Uint32(hdr[0:4]) {
case 0xfeedface:
bo = binary.BigEndian
case 0xfeedfacf:
bo = binary.BigEndian
is64 = true
hdrSize = 32
case 0xcefaedfe:
bo = binary.LittleEndian
case 0xcffaedfe:
bo = binary.LittleEndian
is64 = true
hdrSize = 32
default:
return 0, 0
}
if is64 && len(hdr) < 32 {
return 0, 0
}
ncmds := bo.Uint32(hdr[16:20])
sizeofcmds := bo.Uint32(hdr[20:24])
if ncmds == 0 || ncmds > 10000 || sizeofcmds > 128<<20 {
return 0, 0
}
cmdBytes, err := d.Range(start+hdrSize, int64(sizeofcmds))
if err != nil || len(cmdBytes) < int(sizeofcmds) {
return 0, 0
}
end := hdrSize + int64(sizeofcmds)
off := 0
for i := uint32(0); i < ncmds; i++ {
if off+8 > len(cmdBytes) {
return 0, 0
}
cmd := bo.Uint32(cmdBytes[off : off+4])
cmdSize := int(bo.Uint32(cmdBytes[off+4 : off+8]))
if cmdSize < 8 || off+cmdSize > len(cmdBytes) {
return 0, 0
}
switch cmd {
case 0x1: // LC_SEGMENT
if cmdSize >= 56 {
fileOff := int64(bo.Uint32(cmdBytes[off+32 : off+36]))
fileSize := int64(bo.Uint32(cmdBytes[off+36 : off+40]))
if fileOff >= 0 && fileSize >= 0 && fileOff+fileSize > end {
end = fileOff + fileSize
}
}
case 0x19: // LC_SEGMENT_64
if cmdSize >= 72 {
fileOff := int64(bo.Uint64(cmdBytes[off+40 : off+48]))
fileSize := int64(bo.Uint64(cmdBytes[off+48 : off+56]))
if fileOff >= 0 && fileSize >= 0 && fileOff+fileSize > end {
end = fileOff + fileSize
}
}
}
off += cmdSize
}
if end <= hdrSize || start+end > max {
return 0, 0
}
return end, 0.85
}
func validateID3(d *document.Document, start, max int64) (int64, float64) {
// ID3v2 header: "ID3" + ver(2) + flags(1) + size(4, synchsafe).
hdr, err := d.Range(start, 10)
if err != nil || len(hdr) < 10 {
return 0, 0.3
}
if hdr[3] == 0xff || hdr[4] == 0xff {
return 0, 0.3
}
// Synchsafe 28-bit size (7 bits per byte).
sz := (int64(hdr[6]&0x7f) << 21) | (int64(hdr[7]&0x7f) << 14) |
(int64(hdr[8]&0x7f) << 7) | int64(hdr[9]&0x7f)
if sz < 0 || sz > 32*1024*1024 {
return 0, 0.4
}
// Skip the ID3v2 tag (header + synchsafe body, plus a 10-byte footer if the
// footer-present flag is set), then walk MPEG audio frames to the end.
audioStart := start + 10 + sz
if hdr[5]&0x10 != 0 {
audioStart += 10
}
if end := walkMP3Frames(d, audioStart, max); end > audioStart {
return end - start, 0.85
}
return 0, 0.7
}
// walkMP3Frames follows MPEG-1/2/2.5 Layer I/II/III frame headers from start
// until a byte that is not a valid frame sync, returning the offset just past
// the last frame. APE/Lyrics3 trailers are not stripped — the audio extent is
// what matters for carving.
func walkMP3Frames(d *document.Document, start, max int64) int64 {
pos := start
frames := 0
for pos+4 <= max && frames < 5_000_000 {
h, err := d.Range(pos, 4)
if err != nil || len(h) < 4 {
break
}
ln := mp3FrameLen(h)
if ln <= 0 {
break
}
pos += ln
frames++
}
if frames == 0 {
return 0
}
if pos > max {
return max
}
return pos
}
var mp3BitrateV1 = [16]int{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}
var mp3BitrateV2 = [16]int{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}
var mp3SampleRate = map[int][3]int{ // [version]{rate index 0,1,2}
3: {44100, 48000, 32000}, // MPEG1
2: {22050, 24000, 16000}, // MPEG2
0: {11025, 12000, 8000}, // MPEG2.5
}
// mp3FrameLen returns the byte length of the MPEG audio frame whose 4-byte
// header is h, or 0 if h is not a valid frame header.
func mp3FrameLen(h []byte) int64 {
if h[0] != 0xff || h[1]&0xe0 != 0xe0 {
return 0
}
version := int(h[1] >> 3 & 0x03) // 3=MPEG1, 2=MPEG2, 0=MPEG2.5
layer := int(h[1] >> 1 & 0x03) // 3=I, 2=II, 1=III
if version == 1 || layer == 0 {
return 0 // reserved
}
brIndex := int(h[2] >> 4 & 0x0f)
srIndex := int(h[2] >> 2 & 0x03)
padding := int(h[2] >> 1 & 0x01)
if brIndex == 0 || brIndex == 15 || srIndex == 3 {
return 0
}
var bitrate int
if version == 3 {
bitrate = mp3BitrateV1[brIndex]
} else {
bitrate = mp3BitrateV2[brIndex]
}
rates, ok := mp3SampleRate[version]
if !ok || bitrate == 0 {
return 0
}
sampleRate := rates[srIndex]
if sampleRate == 0 {
return 0
}
bps := bitrate * 1000
if layer == 3 { // Layer I
return int64(((12 * bps / sampleRate) + padding) * 4)
}
// Layer II & III. MPEG2/2.5 Layer III uses 72 samples-per-byte factor.
coef := 144
if version != 3 && layer == 1 {
coef = 72
}
return int64(coef*bps/sampleRate + padding)
}
func validateOgg(d *document.Document, start, max int64) (int64, float64) {
off := start
first := true
for off+27 <= max {
hdr, err := d.Range(off, 27)
if err != nil || len(hdr) < 27 || string(hdr[:4]) != "OggS" || hdr[4] != 0 {
return 0, 0
}
headerType := hdr[5]
if first && headerType&0x02 == 0 {
return 0, 0.4
}
segCount := int(hdr[26])
lacing, err := d.Range(off+27, int64(segCount))
if err != nil || len(lacing) < segCount {
return 0, 0
}
payload := int64(0)
for _, v := range lacing {
payload += int64(v)
}
pageLen := int64(27 + segCount)
if payload < 0 || off+pageLen+payload > max {
return 0, 0
}
off += pageLen + payload
if headerType&0x04 != 0 {
return off - start, 0.9
}
first = false
}
return 0, 0.6
}
// validateMatroska reads the EBML header element and the following Segment
// element's size to bound a Matroska/WebM stream. A Segment with "unknown size"
// (all size bits set, as live-streamed files use) cannot be bounded this way.
func validateMatroska(d *document.Document, start, max int64) (int64, float64) {
buf, err := d.Range(start, min64(max-start, 4096))
if err != nil || len(buf) < 8 {
return 0, 0
}
// EBML header: ID 0x1A45DFA3, then a vint size.
if !(buf[0] == 0x1a && buf[1] == 0x45 && buf[2] == 0xdf && buf[3] == 0xa3) {
return 0, 0
}
hsz, n, unknown := ebmlSize(buf, 4)
if n == 0 || unknown {
return 0, 0.6
}
pos := 4 + n + int(hsz) // start of the Segment element
if pos+4 > len(buf) {
return 0, 0.6
}
// Segment: ID 0x18538067, then a vint size.
if !(buf[pos] == 0x18 && buf[pos+1] == 0x53 && buf[pos+2] == 0x80 && buf[pos+3] == 0x67) {
return 0, 0.6
}
segSz, sn, segUnknown := ebmlSize(buf, pos+4)
if sn == 0 || segUnknown {
return 0, 0.6 // live/streamed segment without a known size
}
end := int64(pos+4+sn) + segSz
if end <= 0 || start+end > max {
return 0, 0.6
}
return end, 0.9
}
// ebmlSize decodes an EBML variable-length integer (data size form). The length
// is the number of leading zero bits in the first byte plus one; the value is
// the remaining bits. unknown is true when every value bit is set.
func ebmlSize(buf []byte, pos int) (val int64, n int, unknown bool) {
if pos >= len(buf) || buf[pos] == 0 {
return 0, 0, false
}
first := buf[pos]
mask := byte(0x80)
length := 1
for first&mask == 0 {
mask >>= 1
length++
}
if length > 8 || pos+length > len(buf) {
return 0, 0, false
}
low := mask - 1
v := int64(first & low)
allOnes := (first & low) == low
for i := 1; i < length; i++ {
b := buf[pos+i]
v = (v << 8) | int64(b)
if b != 0xff {
allOnes = false
}
}
return v, length, allOnes
}
func validateFLAC(d *document.Document, start, max int64) (int64, float64) {
return 0, 0.8
}
// validateBMP reads the BITMAPFILEHEADER size field (bfSize), sanity-checking
// the embedded pixel-data offset against the declared length.
func validateBMP(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 14)
if err != nil || len(hdr) < 14 {
return 0, 0
}
size := int64(binary.LittleEndian.Uint32(hdr[2:6]))
dataOff := int64(binary.LittleEndian.Uint32(hdr[10:14]))
if size < 26 || dataOff < 26 || dataOff > size || start+size > max {
return 0, 0
}
return size, 0.8
}
// validateMP4 walks the top-level boxes of an ISO base-media file (MP4/MOV/M4A)
// from the file start (the ftyp box). 64-bit box sizes are honoured.
func validateMP4(d *document.Document, start, max int64) (int64, float64) {
pos := start
boxes := 0
for pos+8 <= max && boxes < 100000 {
h, err := d.Range(pos, 16)
if err != nil || len(h) < 8 {
break
}
size := int64(binary.BigEndian.Uint32(h[0:4]))
switch {
case size == 1: // 64-bit largesize follows the type
if len(h) < 16 {
return 0, 0.4
}
size = int64(binary.BigEndian.Uint64(h[8:16]))
case size == 0: // box extends to end of file
return max - start, 0.8
}
if size < 8 {
break
}
pos += size
boxes++
if pos > max {
return 0, 0.4
}
}
if pos > start && pos <= max && boxes > 0 {
return pos - start, 0.85
}
return 0, 0.4
}
func validateSQLite(d *document.Document, start, max int64) (int64, float64) {
hdr, err := d.Range(start, 32)
if err != nil || len(hdr) < 32 {
return 0, 0
}
pageSize := int64(binary.BigEndian.Uint16(hdr[16:18]))
if pageSize == 1 {
pageSize = 65536
}
pages := int64(binary.BigEndian.Uint32(hdr[28:32]))
if pages == 0 || pageSize == 0 {
return 0, 0.5
}
return pageSize * pages, 0.9
}
func min64(a, b int64) int64 {
if a < b {
return a
}
return b
}