Initial DDC release
This commit is contained in:
commit
434f6441da
57 files changed
+4606
No files matched your search
Executable
+48
@@ -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
|
||||
}
|
||||
Executable
+50
@@ -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
|
||||
}
|
||||
Executable
+61
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+106
@@ -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
|
||||
}
|
||||
Executable
+44
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user