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

163 lines
3.6 KiB
Go

package analysis
import (
"encoding/binary"
"fmt"
"math"
"time"
"unicode/utf16"
"unicode/utf8"
)
// Inspect interprets bytes starting at `buf[0]` as many candidate
// types. It is resilient: types that don't have enough bytes simply
// return empty strings.
func Inspect(buf []byte, offset int64) map[string]any {
out := map[string]any{
"offset": offset,
"length": len(buf),
}
// 8-bit
if len(buf) >= 1 {
out["u8"] = int(buf[0])
out["i8"] = int(int8(buf[0]))
out["char"] = printable(buf[0])
out["bin8"] = fmt.Sprintf("%08b", buf[0])
}
// 16-bit
if len(buf) >= 2 {
le16 := binary.LittleEndian.Uint16(buf)
be16 := binary.BigEndian.Uint16(buf)
out["u16_le"] = int(le16)
out["u16_be"] = int(be16)
out["i16_le"] = int(int16(le16))
out["i16_be"] = int(int16(be16))
}
// 32-bit
if len(buf) >= 4 {
le32 := binary.LittleEndian.Uint32(buf)
be32 := binary.BigEndian.Uint32(buf)
out["u32_le"] = uint(le32)
out["u32_be"] = uint(be32)
out["i32_le"] = int(int32(le32))
out["i32_be"] = int(int32(be32))
out["f32_le"] = float64(math.Float32frombits(le32))
out["f32_be"] = float64(math.Float32frombits(be32))
}
// 64-bit
if len(buf) >= 8 {
le64 := binary.LittleEndian.Uint64(buf)
be64 := binary.BigEndian.Uint64(buf)
out["u64_le"] = float64(le64)
out["u64_be"] = float64(be64)
out["i64_le"] = float64(int64(le64))
out["i64_be"] = float64(int64(be64))
out["f64_le"] = math.Float64frombits(le64)
out["f64_be"] = math.Float64frombits(be64)
}
// Timestamp guesses
if len(buf) >= 4 {
le := binary.LittleEndian.Uint32(buf)
out["unix_le"] = formatUnix(int64(le))
}
if len(buf) >= 8 {
le := binary.LittleEndian.Uint64(buf)
out["filetime_le"] = formatFiletime(le)
out["webkit_le"] = formatWebkit(le)
out["oletime_le"] = formatOLEDate(math.Float64frombits(le))
}
// ASCII preview (up to 32 bytes)
n := len(buf)
if n > 32 {
n = 32
}
ascii := make([]byte, n)
for i := 0; i < n; i++ {
if buf[i] >= 0x20 && buf[i] <= 0x7e {
ascii[i] = buf[i]
} else {
ascii[i] = '.'
}
}
out["ascii"] = string(ascii)
// UTF-8 attempt
if utf8.Valid(buf[:n]) {
out["utf8"] = string(buf[:n])
}
// UTF-16 LE/BE preview
if len(buf) >= 2 {
lu := n / 2
if lu > 0 {
u := make([]uint16, lu)
for i := 0; i < lu; i++ {
u[i] = uint16(buf[i*2]) | uint16(buf[i*2+1])<<8
}
out["utf16le"] = string(utf16.Decode(u))
for i := 0; i < lu; i++ {
u[i] = uint16(buf[i*2])<<8 | uint16(buf[i*2+1])
}
out["utf16be"] = string(utf16.Decode(u))
}
}
return out
}
func printable(b byte) string {
if b >= 0x20 && b <= 0x7e {
return string([]byte{b})
}
return "."
}
func formatUnix(sec int64) string {
if sec < 0 || sec > 4102444800 { // past 2100
return ""
}
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}
// Windows FILETIME epoch 1601-01-01 in 100ns ticks.
const filetimeEpochDelta = 11644473600
func formatFiletime(v uint64) string {
if v == 0 {
return ""
}
sec := int64(v/10_000_000) - filetimeEpochDelta
if sec < 0 || sec > 4102444800 {
return ""
}
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}
// WebKit/Chrome epoch 1601-01-01 in microseconds.
func formatWebkit(v uint64) string {
if v == 0 {
return ""
}
sec := int64(v/1_000_000) - filetimeEpochDelta
if sec < 0 || sec > 4102444800 {
return ""
}
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
}
// OLE Automation date: days since 1899-12-30, as float64.
func formatOLEDate(days float64) string {
if days < 2 || days > 75000 {
return ""
}
base := time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
sec := int64(days * 86400)
return base.Add(time.Duration(sec) * time.Second).Format(time.RFC3339)
}