Rez Moss e7f1a803e7
fixed dotnet cataloger can't find packages from deps.json in linux el… (#4517)
* fixed dotnet cataloger can't find packages from deps.json in linux elf, fixed #4514

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* split bundle and PE concerns

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* limit resource usage of readall call

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* removed duplicat

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* make sure the first 4 bytes in elf arent lostt

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* revert readelfbundle func, check size of readdeps json

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* revert readelfbundle func, check size of readdeps json, fixed #4514

Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* move dotnet net8 linux fixture to testdata convention

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* address malformed elf size claims + add tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* dont key off of cataloger name in testing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-06-29 13:52:55 -04:00

177 lines
4.9 KiB
Go

package bundle
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
var dotNetBundleSignature = []byte{
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
}
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
type dotNetBundleHeader struct {
MajorVersion uint32
MinorVersion uint32
NumEmbeddedFiles int32
}
// dotNetBundleHeaderV2 represents additional fields in V2+ bundles (.NET 5+)
type dotNetBundleHeaderV2 struct {
DepsJSONOffset int64
DepsJSONSize int64
RuntimeConfigJSONOffset int64
RuntimeConfigJSONSize int64
Flags uint64
}
// dotNetFileType represents the type of bundled file in the manifest
type dotNetFileType uint8
const (
dotNetFileTypeUnknown dotNetFileType = iota
dotNetFileTypeAssembly
dotNetFileTypeNativeBinary
dotNetFileTypeDepsJSON
dotNetFileTypeRuntimeConfigJSON
dotNetFileTypeSymbols
)
// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content.
func ReadDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) {
if _, err := r.Seek(headerOffset, io.SeekStart); err != nil {
return "", err
}
var header dotNetBundleHeader
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return "", err
}
// skip bundle ID (7-bit length-prefixed string)
if err := skipDotNetString(r); err != nil {
return "", err
}
// for V2+ bundles (.NET 5+), read deps.json location directly from header
if header.MajorVersion >= 2 {
var headerV2 dotNetBundleHeaderV2
if err := binary.Read(r, binary.LittleEndian, &headerV2); err != nil {
return "", err
}
if headerV2.DepsJSONSize > 0 && headerV2.DepsJSONOffset > 0 {
return readDepsJSONAtOffset(r, headerV2.DepsJSONOffset, headerV2.DepsJSONSize)
}
}
// for V1 bundles (.NET Core 3.x) or if V2 header doesn't have deps.json, parse manifest
return findDepsJSONInManifest(r, header.NumEmbeddedFiles, header.MajorVersion)
}
// skipDotNetString skips a 7-bit length-prefixed string (.NET BinaryWriter format)
func skipDotNetString(r io.ReadSeeker) error {
length, err := read7BitEncodedInt(r)
if err != nil {
return err
}
_, err = r.Seek(int64(length), io.SeekCurrent)
return err
}
// read7BitEncodedInt reads a .NET 7-bit encoded integer (variable-length encoding used by BinaryWriter)
func read7BitEncodedInt(r io.Reader) (int, error) {
result := 0
shift := 0
for {
var b [1]byte
if _, err := r.Read(b[:]); err != nil {
return 0, err
}
result |= int(b[0]&0x7F) << shift
if b[0]&0x80 == 0 {
break
}
shift += 7
if shift >= 35 { // prevent overflow
return 0, errors.New("invalid 7-bit encoded int")
}
}
return result, nil
}
// readDepsJSONAtOffset reads deps.json content at a specific offset using seeks (avoiding loading entire file)
func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) {
if size <= 0 || size > 3*1024*1024 { // 3MB max deps.json size in dotnet bundle
return "", nil
}
if _, err := r.Seek(offset, io.SeekStart); err != nil {
return "", fmt.Errorf("failed to seek to deps.json at offset %d: %w", offset, err)
}
data := make([]byte, size)
if _, err := io.ReadFull(r, data); err != nil {
return "", fmt.Errorf("failed to read deps.json (%d bytes): %w", size, err)
}
return string(data), nil
}
// findDepsJSONInManifest parses manifest entries to find deps.json (for V1 bundles or fallback)
func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32) (string, error) {
for i := int32(0); i < numFiles; i++ {
var offset, size int64
if err := binary.Read(r, binary.LittleEndian, &offset); err != nil {
return "", err
}
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
return "", err
}
// V6+ bundles (.NET 6+) have compressed size field
if majorVersion >= 6 {
var compressedSize int64
if err := binary.Read(r, binary.LittleEndian, &compressedSize); err != nil {
return "", err
}
}
var fileType dotNetFileType
if err := binary.Read(r, binary.LittleEndian, &fileType); err != nil {
return "", err
}
// skip relativePath string
if err := skipDotNetString(r); err != nil {
return "", err
}
if fileType == dotNetFileTypeDepsJSON && size > 0 {
// save current position to resume manifest parsing if needed
currentPos, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return "", err
}
// read deps.json content
content, err := readDepsJSONAtOffset(r, offset, size)
if err != nil {
return "", err
}
// restore position (in case caller needs to continue)
if _, err := r.Seek(currentPos, io.SeekStart); err != nil {
return "", err
}
return content, nil
}
}
return "", nil
}