[Aider] Phase 0
This commit is contained in:
87
internal/utils/tar.go
Normal file
87
internal/utils/tar.go
Normal file
@ -0,0 +1,87 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const maxQuadletFileSize = 1 * 1024 * 1024 // 1MB limit per file in tarball
|
||||
const maxTotalQuadletSize = 5 * 1024 * 1024 // 5MB limit for total uncompressed size
|
||||
const maxQuadletFiles = 20 // Max number of files in a quadlet bundle
|
||||
|
||||
// UntarQuadlets unpacks a tar.gz stream in memory and returns a map of fileName -> fileContent.
|
||||
// It performs basic validation on file names and sizes.
|
||||
func UntarQuadlets(reader io.Reader) (map[string][]byte, error) {
|
||||
gzr, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
files := make(map[string][]byte)
|
||||
var totalSize int64
|
||||
fileCount := 0
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read tar header: %w", err)
|
||||
}
|
||||
|
||||
// Basic security checks
|
||||
if strings.Contains(header.Name, "..") {
|
||||
return nil, fmt.Errorf("invalid file path in tar: %s (contains '..')", header.Name)
|
||||
}
|
||||
// Ensure files are *.kat and are not in subdirectories within the tarball
|
||||
// The Quadlet concept implies a flat directory of *.kat files.
|
||||
if filepath.Dir(header.Name) != "." && filepath.Dir(header.Name) != "" {
|
||||
return nil, fmt.Errorf("invalid file path in tar: %s (subdirectories are not allowed for Quadlet files)", header.Name)
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(header.Name), ".kat") {
|
||||
return nil, fmt.Errorf("invalid file type in tar: %s (only .kat files are allowed)", header.Name)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeReg: // Regular file
|
||||
fileCount++
|
||||
if fileCount > maxQuadletFiles {
|
||||
return nil, fmt.Errorf("too many files in quadlet bundle; limit %d", maxQuadletFiles)
|
||||
}
|
||||
|
||||
if header.Size > maxQuadletFileSize {
|
||||
return nil, fmt.Errorf("file %s in tar is too large: %d bytes (max %d)", header.Name, header.Size, maxQuadletFileSize)
|
||||
}
|
||||
totalSize += header.Size
|
||||
if totalSize > maxTotalQuadletSize {
|
||||
return nil, fmt.Errorf("total size of files in tar is too large (max %d MB)", maxTotalQuadletSize/(1024*1024))
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file content for %s from tar: %w", header.Name, err)
|
||||
}
|
||||
if int64(len(content)) != header.Size {
|
||||
return nil, fmt.Errorf("file %s in tar has inconsistent size: header %d, read %d", header.Name, header.Size, len(content))
|
||||
}
|
||||
files[header.Name] = content
|
||||
case tar.TypeDir: // Directory
|
||||
// Directories are ignored; we expect a flat structure of .kat files.
|
||||
continue
|
||||
default:
|
||||
// Symlinks, char devices, etc. are not allowed.
|
||||
return nil, fmt.Errorf("unsupported file type in tar for %s: typeflag %c", header.Name, header.Typeflag)
|
||||
}
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf("no .kat files found in the provided archive")
|
||||
}
|
||||
return files, nil
|
||||
}
|
@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -132,7 +131,7 @@ func TestUntarQuadlets_FileTooLarge(t *testing.T) {
|
||||
func TestUntarQuadlets_TotalSizeTooLarge(t *testing.T) {
|
||||
numFiles := (maxTotalQuadletSize / maxQuadletFileSize) + 2
|
||||
fileSize := maxQuadletFileSize / 2
|
||||
|
||||
|
||||
inputFiles := make(map[string]string)
|
||||
content := strings.Repeat("a", int(fileSize))
|
||||
for i := 0; i < int(numFiles); i++ {
|
||||
@ -150,18 +149,18 @@ func TestUntarQuadlets_TotalSizeTooLarge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUntarQuadlets_TooManyFiles(t *testing.T) {
|
||||
inputFiles := make(map[string]string)
|
||||
for i := 0; i <= maxQuadletFiles; i++ {
|
||||
inputFiles[filepath.Join(".", "file"+string(rune(i+'a'))+".kat")] = "content"
|
||||
}
|
||||
reader := createTestTarGz(t, inputFiles, nil)
|
||||
_, err := UntarQuadlets(reader)
|
||||
if err == nil {
|
||||
t.Fatal("UntarQuadlets() with too many files did not return an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too many files in quadlet bundle") {
|
||||
t.Errorf("Expected 'too many files' error, got: %v", err)
|
||||
}
|
||||
inputFiles := make(map[string]string)
|
||||
for i := 0; i <= maxQuadletFiles; i++ {
|
||||
inputFiles[filepath.Join(".", "file"+string(rune(i+'a'))+".kat")] = "content"
|
||||
}
|
||||
reader := createTestTarGz(t, inputFiles, nil)
|
||||
_, err := UntarQuadlets(reader)
|
||||
if err == nil {
|
||||
t.Fatal("UntarQuadlets() with too many files did not return an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too many files in quadlet bundle") {
|
||||
t.Errorf("Expected 'too many files' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntarQuadlets_UnsupportedFileType(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user