fix(parser,exporter): fix large tar.gz truncation and empty device_class defaulting
- archive.go: extractTarGzFromReader truncated decompressed tar.gz content at a hard 50MB byte boundary before checking whether it was a tar archive, corrupting the tar structure mid-entry for any legitimately large archive (e.g. a 200MB decompressed NVIDIA bug-report bundle) and causing the whole file to fail with "tar read: unexpected EOF" instead of extracting what's there. Now peeks the first 512-byte tar block to detect tar vs. single gzipped file without consuming the stream, and streams tar entries with a cumulative (not raw-byte) size limit that only ever stops at an entry boundary. The byte-level cap still applies to the single-gzipped-file case, where it's safe since there's no container structure to corrupt. - reanimator_converter.go: normalizeLegacyPCIeDeviceClass mapped an empty device_class to "NetworkController" by accident (grouped into the same case as "network"/"ethernet" aliases). Sources that never populate a class at all (e.g. Dell's DCIM_PCIDeviceView) got every such device — including NVMe drives and SATA controllers — mislabeled as network controllers. Empty now stays empty instead of being guessed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4fa16c78a5
commit
8665f79fd6
@@ -2545,7 +2545,14 @@ func normalizePCIeDeviceClass(d models.HardwareDevice) string {
|
|||||||
|
|
||||||
func normalizeLegacyPCIeDeviceClass(deviceClass string) string {
|
func normalizeLegacyPCIeDeviceClass(deviceClass string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(deviceClass)) {
|
switch strings.ToLower(strings.TrimSpace(deviceClass)) {
|
||||||
case "", "network", "network controller", "networkcontroller", "ethernet", "ethernet controller", "ethernetcontroller":
|
case "":
|
||||||
|
// Unknown class must stay unknown — it must not be guessed as
|
||||||
|
// "NetworkController" just because that happened to be the first
|
||||||
|
// case in this switch. An empty class is common for device
|
||||||
|
// sources (e.g. Dell DCIM_PCIDeviceView) that never carry a class
|
||||||
|
// at all, including storage/GPU/other non-network devices.
|
||||||
|
return ""
|
||||||
|
case "network", "network controller", "networkcontroller", "ethernet", "ethernet controller", "ethernetcontroller":
|
||||||
return "NetworkController"
|
return "NetworkController"
|
||||||
case "fibre channel", "fibre channel controller", "fibrechannelcontroller", "fc":
|
case "fibre channel", "fibre channel controller", "fibrechannelcontroller", "fc":
|
||||||
return "FibreChannelController"
|
return "FibreChannelController"
|
||||||
|
|||||||
+49
-29
@@ -3,6 +3,7 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -172,9 +173,28 @@ func extractTarGzFromReader(r io.Reader, filename string) ([]ExtractedFile, erro
|
|||||||
}
|
}
|
||||||
defer gzr.Close()
|
defer gzr.Close()
|
||||||
|
|
||||||
// Read decompressed content with a hard cap.
|
// Peek the first tar block (512 bytes) to decide whether this is a tar
|
||||||
// When the payload exceeds the cap, keep the first chunk and mark it as truncated.
|
// archive or a single gzipped file, without consuming/truncating the
|
||||||
decompressed, err := io.ReadAll(io.LimitReader(gzr, maxGzipDecompressedSize+1))
|
// rest of the stream. A raw byte-level cap on the whole decompressed
|
||||||
|
// payload would otherwise cut a tar archive mid-entry and corrupt its
|
||||||
|
// structure, turning "large but valid archive" into "unexpected EOF"
|
||||||
|
// for the entire file instead of extracting what's actually there.
|
||||||
|
buf := bufio.NewReaderSize(gzr, 64*1024)
|
||||||
|
head, peekErr := buf.Peek(512)
|
||||||
|
looksLikeTar := peekErr == nil || (peekErr == io.EOF && len(head) > 0)
|
||||||
|
if looksLikeTar {
|
||||||
|
if _, err := tar.NewReader(bytes.NewReader(head)).Next(); err != nil {
|
||||||
|
looksLikeTar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if looksLikeTar {
|
||||||
|
return extractTarEntriesWithLimit(buf, maxSingleFileSizeLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a tar archive - treat as a single gzipped file. A hard byte cap
|
||||||
|
// is safe here since there's no container structure to corrupt.
|
||||||
|
decompressed, err := io.ReadAll(io.LimitReader(buf, maxGzipDecompressedSize+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read gzip content: %w", err)
|
return nil, fmt.Errorf("read gzip content: %w", err)
|
||||||
}
|
}
|
||||||
@@ -183,15 +203,6 @@ func extractTarGzFromReader(r io.Reader, filename string) ([]ExtractedFile, erro
|
|||||||
decompressed = decompressed[:maxGzipDecompressedSize]
|
decompressed = decompressed[:maxGzipDecompressedSize]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read as tar archive
|
|
||||||
tr := tar.NewReader(bytes.NewReader(decompressed))
|
|
||||||
var files []ExtractedFile
|
|
||||||
|
|
||||||
header, err := tr.Next()
|
|
||||||
if err != nil {
|
|
||||||
// Not a tar archive - treat as a single gzipped file
|
|
||||||
if strings.Contains(err.Error(), "invalid tar header") || err == io.EOF {
|
|
||||||
// Get base filename without .gz extension
|
|
||||||
baseName := strings.TrimSuffix(filename, ".gz")
|
baseName := strings.TrimSuffix(filename, ".gz")
|
||||||
if gzr.Name != "" {
|
if gzr.Name != "" {
|
||||||
baseName = gzr.Name
|
baseName = gzr.Name
|
||||||
@@ -212,15 +223,34 @@ func extractTarGzFromReader(r io.Reader, filename string) ([]ExtractedFile, erro
|
|||||||
|
|
||||||
return []ExtractedFile{file}, nil
|
return []ExtractedFile{file}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractTarEntriesWithLimit streams tar entries from r, skipping individual
|
||||||
|
// files over 10MB, and stops once the cumulative extracted content reaches
|
||||||
|
// totalLimit — always at an entry boundary, never mid-file.
|
||||||
|
func extractTarEntriesWithLimit(r io.Reader, totalLimit int64) ([]ExtractedFile, error) {
|
||||||
|
tr := tar.NewReader(r)
|
||||||
|
var files []ExtractedFile
|
||||||
|
var totalExtracted int64
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("tar read: %w", err)
|
return nil, fmt.Errorf("tar read: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's a valid tar archive, process it
|
if header.Typeflag == tar.TypeDir {
|
||||||
for {
|
continue
|
||||||
// Skip directories
|
}
|
||||||
if header.Typeflag != tar.TypeDir {
|
if header.Size > 10*1024*1024 {
|
||||||
// Skip large files (>10MB)
|
continue
|
||||||
if header.Size <= 10*1024*1024 {
|
}
|
||||||
|
if totalExtracted+header.Size > totalLimit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
content, err := io.ReadAll(tr)
|
content, err := io.ReadAll(tr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read file %s: %w", header.Name, err)
|
return nil, fmt.Errorf("read file %s: %w", header.Name, err)
|
||||||
@@ -231,17 +261,7 @@ func extractTarGzFromReader(r io.Reader, filename string) ([]ExtractedFile, erro
|
|||||||
Content: content,
|
Content: content,
|
||||||
ModTime: header.ModTime,
|
ModTime: header.ModTime,
|
||||||
})
|
})
|
||||||
}
|
totalExtracted += int64(len(content))
|
||||||
}
|
|
||||||
|
|
||||||
// Read next header
|
|
||||||
header, err = tr.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("tar read: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return files, nil
|
return files, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user