compress

package
v3.0.0-next.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 11 Imported by: 0

README

Compress Package

Go Reference

Gzip and tar archive utilities with built-in protection against path traversal and zip bomb attacks.

Features

  • Gzip: stream compression (Gz) and decompression to a file (UnGz)
  • Tar: directory archiving (Tar) and extraction (UnTar)
  • Tar.gz: combined helpers (TarGz, UnTarGz)
  • Base64: tar.gz archives as base64 strings (TarGzBase64, UnTarGzBase64)
  • Security hardened: path traversal prevention, symlink resolution checks, per-file and total size limits, file mode sanitization
  • Error contract: every guard-rail rejection wraps a documented sentinel, matchable with errors.Is

Installation

go get github.com/jasoet/pkg/v3/compress

Quick Start

package main

import (
    "log"
    "os"
    "path/filepath"

    "github.com/jasoet/pkg/v3/compress"
)

func main() {
    // Compress a file with gzip.
    sourceFile, err := os.Open("input.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer sourceFile.Close()

    outputFile, err := os.Create("output.txt.gz")
    if err != nil {
        log.Fatal(err)
    }
    defer outputFile.Close()

    if err := compress.Gz(sourceFile, outputFile); err != nil {
        log.Fatal(err)
    }

    // Decompress it again. UnGz requires an ABSOLUTE destination path.
    gzFile, err := os.Open("output.txt.gz")
    if err != nil {
        log.Fatal(err)
    }
    defer gzFile.Close()

    dst, err := filepath.Abs("decompressed.txt")
    if err != nil {
        log.Fatal(err)
    }
    if _, err := compress.UnGz(gzFile, dst); err != nil {
        log.Fatal(err)
    }
}

API Reference

Gzip
func Gz(source io.Reader, writer io.Writer) error
func UnGz(src io.Reader, dst string, opts ...ExtractOption) (int64, error)

Gz streams gzip-compressed data from source into writer.

UnGz decompresses a gzip stream into the file at dst and returns the number of bytes written. dst must be an absolute path; relative paths are rejected with ErrPathTraversal. A gzip stream holds a single file, so both size options apply to the same output — the effective limit is the smaller of WithMaxFileSize and WithMaxArchiveSize.

Tar
func Tar(sourceDirectory string, writer io.Writer) error
func UnTar(src io.Reader, destinationDir string, opts ...ExtractOption) (int64, error)

Tar archives sourceDirectory into writer. Only regular files are included; symlinks and other special files are skipped.

UnTar extracts a tar stream into destinationDir, which must already exist and be a directory. Unlike UnGz, destinationDir may be a relative path — entry paths inside the archive are validated to stay within it. Only regular files and directories are extracted; other entry types are skipped.

Tar.gz
func TarGz(sourceDirectory string, writer io.Writer) error
func UnTarGz(src io.Reader, destinationDir string, opts ...ExtractOption) (int64, error)

Combined helpers: UnTarGz gunzips src and extracts it like UnTar.

Base64
func TarGzBase64(sourceDirectory string) (string, error)
func UnTarGzBase64(encoded string, destinationDir string, opts ...ExtractOption) (int64, error)

TarGzBase64 archives and compresses a directory, returning it as a base64-encoded string for text transport (JSON, API responses). UnTarGzBase64 reverses it, extracting like UnTarGz.

Options

All extraction functions (UnGz, UnTar, UnTarGz, UnTarGzBase64) accept ExtractOptions:

Option Default Effect
WithMaxFileSize(size int64) 100 MB (DefaultMaxFileSize) Maximum decompressed size of a single file
WithMaxArchiveSize(size int64) 1 GB (DefaultMaxArchiveSize) Maximum total extracted size of an archive

For UnGz (single-file stream) the effective limit is min(maxFileSize, maxArchiveSize).

// Allow single files up to 500 MB, archive total up to 2 GB.
written, err := compress.UnTarGz(reader, destDir,
    compress.WithMaxFileSize(500*1024*1024),
    compress.WithMaxArchiveSize(2*1024*1024*1024),
)

Error Handling

Guard-rail rejections wrap documented sentinels — match them with errors.Is, never by comparing message strings:

Sentinel Returned when
ErrPathTraversal UnGz destination is not absolute; a tar entry path is empty, absolute, has a .. path element or contains \, escapes the destination, resolves through a parent symlink outside it, or targets a pre-existing leaf symlink
ErrSizeLimitExceeded A file exceeds maxFileSize, or the running archive total would exceed maxArchiveSize (enforced mid-file)
ErrNotDirectory Tar source or UnTar/UnTarGz destination is not a directory
written, err := compress.UnTarGz(reader, destDir)
switch {
case errors.Is(err, compress.ErrPathTraversal):
    // Malicious or malformed entry path — reject the archive.
case errors.Is(err, compress.ErrSizeLimitExceeded):
    // Zip bomb protection triggered; written holds bytes extracted so far.
case errors.Is(err, compress.ErrNotDirectory):
    // Fix the destination and retry.
case err != nil:
    // I/O or corrupt-archive error (e.g. gzip.ErrHeader, io.ErrUnexpectedEOF).
}

Missing destinations, corrupt archives, and filesystem failures surface as the underlying os/gzip/tar errors and are matchable with errors.Is against fs.ErrNotExist and friends.

Security Details

  • Path traversal prevention: tar entry names are rejected when empty, absolute, or containing a .. path element or a \ (names that merely contain .., like report..final.txt, are allowed); the joined target is re-checked with filepath.Rel so it stays under the destination (a relative destination such as . is accepted); parent directories are resolved with filepath.EvalSymlinks to stop parent-symlink TOCTOU escapes.
  • Leaf-symlink protection: before writing a file, UnTar Lstats the target and refuses (ErrPathTraversal) to write through a pre-existing symlink; the open additionally uses O_NOFOLLOW on platforms that support it, closing the TOCTOU window so an archive can never overwrite a file outside the destination via a planted symlink.
  • Truncating overwrite: files are opened with O_TRUNC, so extracting a shorter file over a longer existing one leaves no stale trailing bytes.
  • Zip bomb protection: extraction streams through io.LimitReader capped at the smaller of the per-file limit and the remaining archive budget, so both maxFileSize and maxArchiveSize are enforced mid-file (no full-file overshoot); one extra byte is probed past the cap so oversized content is detected and reported with ErrSizeLimitExceeded.
  • No partial output: when extraction of a file aborts (size limit or I/O error), the partially written target is removed rather than left on disk.
  • File mode sanitization: extracted file modes are masked with 0o777, stripping setuid/setgid/sticky bits; directories are created 0o750.
  • UnGz vs UnTar path rules: UnGz requires an absolute destination path, while UnTar accepts a relative destination directory. The asymmetry is intentional: UnGz writes to a caller-supplied file path and fails closed on ambiguity, whereas UnTar constrains archive-controlled entry paths inside the destination instead.

Testing

go test ./compress/ -count=1        # all tests, including security suite
go test ./compress/ -v -run TestGuardRailSentinels

License

MIT License - see LICENSE for details.

Documentation

Overview

Package compress provides gzip and tar archive utilities with security protections.

Features include path traversal prevention, zip bomb protection (100 MB per-file limit), and file mode validation. Supports tar, gzip, tar.gz, and base64-encoded tar.gz formats.

Index

Examples

Constants

View Source
const (
	// DefaultMaxFileSize is the default maximum size for a single extracted file (100 MB).
	DefaultMaxFileSize int64 = 100 * 1024 * 1024
	// DefaultMaxArchiveSize is the default maximum total extracted size for an archive (1 GB).
	DefaultMaxArchiveSize int64 = 1024 * 1024 * 1024
)

Variables

View Source
var (
	// ErrSizeLimitExceeded is returned when an extraction guard rail rejects
	// content that exceeds a configured size limit (per-file or total archive).
	ErrSizeLimitExceeded = errors.New("size limit exceeded")
	// ErrPathTraversal is returned when a path guard rail rejects a destination
	// or archive entry that could escape the intended extraction location.
	ErrPathTraversal = errors.New("path traversal detected")
	// ErrNotDirectory is returned when a path that must be a directory
	// (tar source or extraction destination) is not one.
	ErrNotDirectory = errors.New("not a directory")
)

Functions

func Gz

func Gz(source io.Reader, writer io.Writer) error

Gz compresses data from source using gzip and writes the compressed output to writer.

The caller is responsible for closing writer if needed.

Example

Gz compresses data from any io.Reader into any io.Writer.

package main

import (
	"bytes"
	"fmt"

	"github.com/jasoet/pkg/v3/compress"
)

func main() {
	var buf bytes.Buffer
	if err := compress.Gz(bytes.NewReader([]byte("hello, world")), &buf); err != nil {
		panic(err)
	}

	fmt.Println("compressed bytes:", buf.Len())

}
Output:
compressed bytes: 36

func Tar

func Tar(sourceDirectory string, writer io.Writer) error

Tar creates a tar archive of sourceDirectory and writes it to writer.

Only regular files are included; symlinks and other special files are skipped. The caller is responsible for closing writer if needed.

func TarGz

func TarGz(sourceDirectory string, writer io.Writer) error

TarGz creates a gzip-compressed tar archive of sourceDirectory and writes it to writer.

The caller is responsible for closing writer if needed.

func TarGzBase64

func TarGzBase64(sourceDirectory string) (string, error)

TarGzBase64 creates a gzip-compressed tar archive of sourceDirectory and returns it as a base64-encoded string.

func UnGz

func UnGz(src io.Reader, dst string, opts ...ExtractOption) (int64, error)

UnGz decompresses gzip data from src and writes the result to the file at dst.

Decompression is limited to prevent zip bomb attacks: the effective limit is the smaller of maxFileSize (default 100 MB) and maxArchiveSize (default 1 GB), configurable via WithMaxFileSize and WithMaxArchiveSize. Unlike UnTar, dst must be an absolute path (relative paths are rejected with ErrPathTraversal); UnTar accepts relative destination directories. Returns the number of bytes written and any error encountered.

Example

UnGz decompresses a gzip stream into a file at an absolute destination path.

package main

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"

	"github.com/jasoet/pkg/v3/compress"
)

func main() {
	var buf bytes.Buffer
	if err := compress.Gz(bytes.NewReader([]byte("hello, world")), &buf); err != nil {
		panic(err)
	}

	dir, err := os.MkdirTemp("", "compress-example")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(dir)

	dst := filepath.Join(dir, "out.txt")
	written, err := compress.UnGz(&buf, dst)
	if err != nil {
		panic(err)
	}

	content, err := os.ReadFile(dst)
	if err != nil {
		panic(err)
	}
	fmt.Printf("wrote %d bytes: %s\n", written, content)

}
Output:
wrote 12 bytes: hello, world

func UnTar

func UnTar(src io.Reader, destinationDir string, opts ...ExtractOption) (written int64, err error)

UnTar extracts a tar archive from src into destinationDir.

Includes security protections: path traversal prevention, file mode validation, per-file size limit (default 100 MB), and total archive size limit (default 1 GB) to prevent zip bombs. Use ExtractOption to customize limits. Unlike UnGz, destinationDir may be a relative path; entry paths inside the archive are still validated to stay within destinationDir.

func UnTarGz

func UnTarGz(src io.Reader, destinationDir string, opts ...ExtractOption) (int64, error)

UnTarGz decompresses a gzip stream and extracts the tar archive to destinationDir.

func UnTarGzBase64

func UnTarGzBase64(encoded string, destinationDir string, opts ...ExtractOption) (int64, error)

UnTarGzBase64 decodes a base64-encoded gzip tar archive and extracts it to destinationDir.

Types

type ExtractOption

type ExtractOption func(*extractConfig)

ExtractOption configures extraction behavior for UnGz, UnTar, UnTarGz, and UnTarGzBase64.

func WithMaxArchiveSize

func WithMaxArchiveSize(size int64) ExtractOption

WithMaxArchiveSize sets the maximum total extracted size for the entire archive. Default: 1 GB.

func WithMaxFileSize

func WithMaxFileSize(size int64) ExtractOption

WithMaxFileSize sets the maximum allowed size for a single extracted file. Default: 100 MB.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL