Initial DDC release

This commit is contained in:
SirBlob committed 2026-10-25 20:24:50 -04:00
commit 434f6441da
57 files changed
+4606

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
package extract
import (
"context"
"errors"
"os"
"path/filepath"
)
var (
ErrUnsupportedFormat = errors.New("unsupported archive format")
ErrUnsafeEntry = errors.New("archive contains an unsafe path")
ErrTooLarge = errors.New("archive exceeds the extraction size limit")
ErrTooManyEntries = errors.New("archive contains too many entries")
)
type Limits struct {
MaxBytes int64
MaxEntries int
}
type Progress interface {
SetTotal(total int64)
Add(delta int64)
}
func Archive(ctx context.Context, archivePath, destination string, limits Limits, progress Progress) error {
archiveFormat, _ := detect(filepath.Base(archivePath))
if archiveFormat == formatNone {
return ErrUnsupportedFormat
}
if err := os.Mkdir(destination, 0o755); err != nil {
return err
}
writer := &entryWriter{ctx: ctx, root: destination, limits: limits, progress: progress}
var err error
if archiveFormat == formatZip {
err = extractZip(archivePath, writer)
} else {
err = extractTar(archivePath, archiveFormat, writer)
}
if err != nil {
os.RemoveAll(destination)
}
return err
}
+50
View File
@@ -0,0 +1,50 @@
package extract
import "strings"
type format int
const (
formatNone format = iota
formatZip
formatTar
formatTarGzip
formatTarBzip2
)
var knownSuffixes = []struct {
suffix string
format format
}{
{".tar.gz", formatTarGzip},
{".tgz", formatTarGzip},
{".tar.bz2", formatTarBzip2},
{".tbz2", formatTarBzip2},
{".tbz", formatTarBzip2},
{".tar", formatTar},
{".zip", formatZip},
}
func detect(name string) (format, string) {
lowerName := strings.ToLower(name)
for _, known := range knownSuffixes {
if strings.HasSuffix(lowerName, known.suffix) {
return known.format, known.suffix
}
}
return formatNone, ""
}
func IsArchive(name string) bool {
archiveFormat, _ := detect(name)
return archiveFormat != formatNone
}
func DirectoryName(archiveName string) string {
_, suffix := detect(archiveName)
base := strings.Trim(archiveName[:len(archiveName)-len(suffix)], ". ")
if base == "" {
return "extracted"
}
return base
}
+61
View File
@@ -0,0 +1,61 @@
package extract
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"errors"
"io"
"os"
)
func extractTar(archivePath string, archiveFormat format, writer *entryWriter) error {
file, err := os.Open(archivePath)
if err != nil {
return err
}
defer file.Close()
if archiveFormat == formatTar {
if info, err := file.Stat(); err == nil {
writer.progress.SetTotal(info.Size())
}
}
source, err := decompress(file, archiveFormat)
if err != nil {
return err
}
reader := tar.NewReader(source)
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return nil
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
err = writer.writeDirectory(header.Name)
case tar.TypeReg:
err = writer.writeFile(header.Name, reader)
}
if err != nil {
return err
}
}
}
func decompress(file *os.File, archiveFormat format) (io.Reader, error) {
switch archiveFormat {
case formatTarGzip:
return gzip.NewReader(file)
case formatTarBzip2:
return bzip2.NewReader(file), nil
default:
return file, nil
}
}
+106
View File
@@ -0,0 +1,106 @@
package extract
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
type entryWriter struct {
ctx context.Context
root string
limits Limits
progress Progress
written int64
entries int
}
func (writer *entryWriter) writeDirectory(name string) error {
target, err := writer.prepare(name)
if err != nil || target == "" {
return err
}
return os.MkdirAll(target, 0o755)
}
func (writer *entryWriter) writeFile(name string, content io.Reader) error {
target, err := writer.prepare(name)
if err != nil || target == "" {
return err
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if err != nil {
return err
}
remaining := writer.limits.MaxBytes - writer.written
source := &progressReader{ctx: writer.ctx, reader: io.LimitReader(content, remaining+1), progress: writer.progress}
copied, copyErr := io.Copy(file, source)
closeErr := file.Close()
writer.written += copied
if copyErr != nil {
return copyErr
}
if writer.written > writer.limits.MaxBytes {
return ErrTooLarge
}
return closeErr
}
func (writer *entryWriter) prepare(name string) (string, error) {
writer.entries++
if writer.entries > writer.limits.MaxEntries {
return "", ErrTooManyEntries
}
if err := writer.ctx.Err(); err != nil {
return "", err
}
return writer.resolve(name)
}
func (writer *entryWriter) resolve(name string) (string, error) {
normalized := strings.ReplaceAll(name, "\\", "/")
if strings.HasPrefix(normalized, "/") {
return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name)
}
cleaned := filepath.Clean(filepath.FromSlash(normalized))
if cleaned == "." {
return "", nil
}
if !filepath.IsLocal(cleaned) {
return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name)
}
target := filepath.Join(writer.root, cleaned)
relative, err := filepath.Rel(writer.root, target)
if err != nil || !filepath.IsLocal(relative) {
return "", fmt.Errorf("%w: %s", ErrUnsafeEntry, name)
}
return target, nil
}
type progressReader struct {
ctx context.Context
reader io.Reader
progress Progress
}
func (reader *progressReader) Read(buffer []byte) (int, error) {
if err := reader.ctx.Err(); err != nil {
return 0, err
}
count, err := reader.reader.Read(buffer)
if count > 0 {
reader.progress.Add(int64(count))
}
return count, err
}
+44
View File
@@ -0,0 +1,44 @@
package extract
import "archive/zip"
func extractZip(archivePath string, writer *entryWriter) error {
reader, err := zip.OpenReader(archivePath)
if err != nil {
return err
}
defer reader.Close()
var declaredBytes uint64
for _, file := range reader.File {
declaredBytes += file.UncompressedSize64
}
if declaredBytes > uint64(writer.limits.MaxBytes) {
return ErrTooLarge
}
writer.progress.SetTotal(int64(declaredBytes))
for _, file := range reader.File {
if err := extractZipEntry(file, writer); err != nil {
return err
}
}
return nil
}
func extractZipEntry(file *zip.File, writer *entryWriter) error {
mode := file.Mode()
switch {
case mode.IsDir():
return writer.writeDirectory(file.Name)
case mode.IsRegular():
content, err := file.Open()
if err != nil {
return err
}
defer content.Close()
return writer.writeFile(file.Name, content)
default:
return nil
}
}