packfile

package
v1.40.4 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 18 Imported by: 0

README

Packfile Parser

This package provides efficient extraction of commit OIDs from Git packfiles stored in object storage (S3, GCS, etc.) using range requests to minimize data transfer and costs.

Overview

The BlobStorePackfileParser extracts commit OIDs from packfiles without downloading the entire file. It uses an iterative algorithm with batched range requests to resolve delta chains efficiently.

Problem Statement

Git packfiles can be large (gigabytes) and contain thousands of objects. To identify which objects are commits, we need to:

  1. Parse the packfile index (.idx) to get object offsets
  2. Read object headers from the packfile (.pack) to determine types
  3. Resolve delta objects (OFS_DELTA, REF_DELTA) to their base types

Downloading entire packfiles would be slow and expensive. This parser minimizes data transfer by:

  • Using HTTP range requests to fetch only necessary byte ranges
  • Batching objects into efficient 5MB clusters
  • Resolving deltas iteratively within fetched ranges

Architecture

Key Components
  1. OID Type (oid.go)

    • Binary representation of Git object identifiers
    • 20 bytes for SHA-1, 32 bytes for SHA-256
    • 50% memory savings vs hex strings
    • JSON marshaling with hex encoding
  2. BlobStorePackfileParser (parser.go)

    • Main parser that coordinates the resolution process
    • Uses Go Cloud CDK blob.Bucket for storage abstraction
    • Configurable max iterations for DoS protection
Data Structures
// workItem represents an object that needs to be resolved
type workItem struct {
    hash   plumbing.Hash
    offset int64
}

// offsetRange represents a contiguous byte range to fetch
type offsetRange struct {
    start int64
    end   int64
    items []workItem
}

Algorithm

The parser uses an iterative work queue algorithm with delta resolution:

Phase 1: Index Parsing
Read packfile.idx
  ↓
Extract all [hash, offset] pairs
  ↓
Initialize workQueue with all objects
Phase 2: Iterative Resolution
WHILE workQueue is not empty AND iterations < maxIterations:

    1. CLUSTER: Group work items by proximity
       ├─ Sort by offset
       ├─ Group objects within 100KB gaps
       └─ Cap each cluster at 5MB

    2. FETCH: For each cluster
       ├─ HTTP Range request: [start, end)
       └─ Read cluster data into memory

    3. RESOLVE: For each object in cluster
       ├─ Parse object header
       ├─ IF direct type (commit/tree/blob/tag)
       │    └─ Mark as resolved
       ├─ ELSE IF delta type
       │    ├─ IF base is in current range
       │    │    └─ Recursively resolve within range
       │    └─ ELSE
       │         └─ Add base to workQueue
       └─ Continue to next object

    4. ITERATE: Process unresolved items in next iteration
Phase 3: Collection
Filter resolved objects
  ↓
Return all commits

Delta Resolution Strategy

Git packfiles use delta compression to save space. A delta object stores only the differences from a base object.

Delta Types
  1. OFS_DELTA (type 6)

    • References base by negative offset
    • Example: Object at offset 5000 might reference base at offset 4500
    • Requires reverse hash lookup: offset → hash
  2. REF_DELTA (type 7)

    • References base by SHA-1 hash
    • Example: Delta directly specifies base hash
    • Requires forward lookup: hash → offset
Resolution Within Ranges

When a delta and its base are in the same fetched range:

Range: [1000, 6000)
├─ Object A at 1500 (commit) ────────┐
├─ Object B at 3000 (OFS_DELTA) ─────┤ base at 1500
│    └─ Resolve: B inherits type from A
└─ Object C at 5000 (direct commit)

Result: Both A and C are commits (resolved in one iteration)

Resolution Across Ranges

When a delta's base is outside the current range:

Iteration 1:
Range: [5000, 10000)
└─ Object D at 6000 (OFS_DELTA, base at 2000)
   └─ Add base to workQueue

Iteration 2:
Range: [2000, 7000)
├─ Object E at 2000 (commit) ← base for D
└─ Object D at 6000 (now in range)
   └─ Resolve: D inherits type from E

Result: D is a commit (resolved in two iterations)

Clustering Strategy

Objects are grouped to minimize HTTP requests while respecting size limits:

const (
    maxClusterSize = 5 * 1024 * 1024  // 5MB max per range
    minClusterSize = 256 * 1024        // 256KB min per object
    maxGap         = 100 * 1024        // 100KB gap threshold
)
Clustering Rules
  1. Sort by offset: Process objects in packfile order
  2. Group nearby objects: Combine if gap < 100KB
  3. Respect size limit: Split if cluster would exceed 5MB
  4. Minimum read size: Read at least 256KB per object
Example Clustering
Objects:     A      B       C            D
Offsets:   [100] [500]  [1000]       [200000]
            └─────┬─────┘              └──────┘
              Cluster 1                Cluster 2
            Gap: 400B, 500B           Gap: 199KB
            Size: ~1KB                Size: ~256KB

Performance Characteristics

Best Case (No Deltas or Shallow Chains)
  • Iterations: 1
  • Range Requests: 20-50 for 10,000 objects
  • Data Transfer: ~100-250MB for large repos
Typical Case (30-40% Deltas, Depth 1-2)
  • Iterations: 2-3
  • Range Requests: 30-70 for 10,000 objects
  • First iteration: Resolves ~80-90% of objects
  • Second iteration: Resolves remaining dependencies
  • Data Transfer: ~150-350MB for large repos
Worst Case (Deep, Scattered Delta Chains)
  • Iterations: 4-5 (limited by maxIterations)
  • Range Requests: 100+ for pathological cases
  • Data Transfer: ~500MB+ for adversarial packfiles
Comparison to Alternatives
Approach Data Transfer Latency Complexity
Full download 100% (GBs) High Low
Naive per-object 5-10% Very high Low
This algorithm 10-30% Low-Medium Medium

Security Considerations

DoS Protection

The parser includes configurable iteration limits to prevent DoS attacks:

const defaultMaxIterations = 100

Attack Vector: Adversarial packfile with circular or pathological delta chains

  • Delta A references base B
  • Delta B references base C
  • Delta C references base A (cycle)
  • OR: Very deep chain (A→B→C→D→...→Z)

Mitigation:

  • Limit maximum iterations to 100 (default)
  • Return error if exceeded
  • Configurable via WithMaxIterations()
Trade-offs
  • Lower limit (e.g., 10): More DoS protection, may fail on legitimate complex packfiles
  • Higher limit (e.g., 200): Allows complex packfiles, increases vulnerability
  • Default (100): Balances security and functionality

Usage

Basic Usage
// Open bucket
bucket, _ := blob.OpenBucket(ctx, "s3://my-bucket?region=us-west-2")
defer bucket.Close()

// Create parser and extract commits
parser := packfile.NewBlobStorePackfileParser(bucket)
commits, err := parser.ExtractCommitOIDs(ctx, "pack-abc.pack", "pack-abc.idx")
With Custom Iteration Limit
// Stricter DoS protection
parser := packfile.NewBlobStorePackfileParser(bucket).WithMaxIterations(50)

// More permissive for trusted packfiles
parser := packfile.NewBlobStorePackfileParser(bucket).WithMaxIterations(200)

Implementation Details

Object Header Parsing

Git packfile object headers encode type and size:

Byte 0: MTTT SSSS
  M = MSB (more bytes follow)
  T = Type (3 bits)
  S = Size (4 bits)

Types:
  1 = Commit
  2 = Tree
  3 = Blob
  4 = Tag
  6 = OFS_DELTA
  7 = REF_DELTA
OFS_DELTA Offset Encoding

Negative offset uses variable-length encoding:

First byte:  0VVV VVVV (V = offset bits)
More bytes:  1VVV VVVV (continue)
Last byte:   0VVV VVVV (done)

Decode:
  offset = firstByte & 0x7F
  while (byte & 0x80):
      offset = ((offset + 1) << 7) | (byte & 0x7F)
  baseOffset = currentOffset - offset
REF_DELTA Hash Encoding

Base hash is stored directly as 20 bytes (SHA-1) following the object header.

Future Improvements

Potential optimizations:

  1. Reverse index cache: Pre-build offset→hash map for O(1) lookups
  2. Parallel fetching: Fetch multiple ranges concurrently
  3. Adaptive clustering: Adjust cluster size based on packfile structure
  4. Delta chain depth limit: Additional DoS protection per-chain
  5. Metrics: Track iterations, range requests, data transfer

References

Documentation

Overview

Package packfile provides efficient parsing of Git packfiles stored in object storage. It extracts commit OIDs using range requests to minimize data transfer costs.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidOIDLength is returned when an OID has an invalid length.
	ErrInvalidOIDLength = errors.New("invalid OID length: expected 20 (SHA-1) or 32 (SHA-256) bytes")
	// ErrInvalidHexString is returned when a hex string cannot be decoded.
	ErrInvalidHexString = errors.New("invalid hex string")
)
View Source
var (
	// ErrMaxIterationsExceeded is returned when delta resolution exceeds the maximum number of iterations.
	ErrMaxIterationsExceeded = errors.New("exceeded maximum iterations while resolving delta chains")
	// ErrEmptyData is returned when attempting to parse an empty data buffer.
	ErrEmptyData = errors.New("empty data")
	// ErrTruncatedOFSDelta is returned when an OFS_DELTA object is truncated.
	ErrTruncatedOFSDelta = errors.New("truncated OFS_DELTA")
	// ErrTruncatedREFDelta is returned when a REF_DELTA object is truncated.
	ErrTruncatedREFDelta = errors.New("truncated REF_DELTA")
	// ErrUnknownObjectType is returned when an unknown object type is encountered.
	ErrUnknownObjectType = errors.New("unknown object type")
	// ErrOffsetTooLarge is returned when an offset exceeds int64 maximum.
	ErrOffsetTooLarge = errors.New("offset too large")
)
View Source
var (
	// ErrPackfileNotFound is returned when a packfile doesn't exist for the given repository.
	ErrPackfileNotFound = errors.New("base packfile not found")
)

Functions

func EmptyOIDIterator added in v1.24.1

func EmptyOIDIterator() iter.Seq[OID]

EmptyOIDIterator returns an iterator that yields no OIDs.

Types

type BasePackfile

type BasePackfile struct {
	// Repository is the repository path (e.g., "gitlab-org/gitlab").
	Repository string `json:"repository"`

	// PackfileURI is the full URI to the .pack file in object storage.
	// Example: "s3://packhorse-packfiles/gitlab-org/gitlab/base-20250126.pack"
	PackfileURI string `json:"packfile_uri"`

	// IndexURI is the full URI to the .idx file in object storage.
	// Derived from PackfileURI by replacing .pack with .idx.
	IndexURI string `json:"index_uri"`

	// HashAlgorithm is the hash algorithm used ("sha1" or "sha256").
	HashAlgorithm string `json:"hash_algorithm"`

	// PackfileHash is the hash of the packfile content (binary format).
	PackfileHash OID `json:"packfile_hash"`

	// CreatedAt is when the packfile became available (extraction completed).
	CreatedAt time.Time `json:"created_at"`

	// UpdatedAt is when the packfile metadata was last updated.
	UpdatedAt time.Time `json:"updated_at"`

	// SizeBytes is the size of the .pack file in bytes.
	SizeBytes int64 `json:"size_bytes,omitempty"`
}

BasePackfile tracks an available base packfile and its metadata. Base packfiles are generated externally and registered with Packhorse via API.

BasePackfile instances only exist after successful commit ID extraction. The lifecycle is atomic from the client's perspective: either a packfile exists with all its commit OIDs, or it doesn't exist at all.

BasePackfile contains only the metadata that would be stored in a distributed store like Redis. Commit OIDs are returned alongside the metadata by the manager's Get() method as a separate iterator, not stored in this struct.

BasePackfile instances are immutable after creation. Callers must not modify any fields.

type BasePackfileManager

type BasePackfileManager interface {
	// Register starts asynchronous commit ID extraction for a base packfile.
	// The packfile won't be visible via Get() until extraction completes successfully.
	// Returns immediately (non-blocking).
	//
	// Parameters:
	//   - repository: Repository path (e.g., "gitlab-org/gitlab")
	//   - packfileURI: URI to .pack file in object storage
	//   - hashAlgorithm: "sha1" or "sha256"
	//   - packfileHash: Hash of the packfile (hex-encoded string)
	//
	// If extraction fails, the packfile never becomes visible. The error is logged
	// but not exposed to clients.
	Register(ctx context.Context, repository, packfileURI, hashAlgorithm, packfileHash string) error

	// Get returns the base packfile metadata and a commit OID iterator for a repository.
	// Both are captured from the same snapshot, guaranteeing consistency.
	// Returns (nil, empty iterator, nil) if the packfile hasn't been extracted yet or doesn't exist.
	// The returned BasePackfile is immutable and must not be modified.
	//
	// Usage:
	//   bp, commitOIDs, err := manager.Get(ctx, "gitlab-org/gitlab")
	//   if bp != nil {
	//       for oid := range commitOIDs {
	//           // use oid
	//       }
	//   }
	Get(ctx context.Context, repository string) (*BasePackfile, iter.Seq[OID], error)

	// Delete removes a base packfile.
	// Returns an error if the packfile doesn't exist.
	Delete(ctx context.Context, repository string) error
}

BasePackfileManager tracks base packfiles and their metadata. Base packfiles are generated externally and registered via API.

The lifecycle is atomic from the client's perspective: either a packfile exists with all its commit OIDs, or it doesn't exist at all. Clients never see intermediate extraction states or failures.

Phase 1 uses an in-memory implementation with sync.RWMutex. Phase 2 will use Redis for multi-instance coordination.

type BlobStorePackfileParser

type BlobStorePackfileParser struct {
	// contains filtered or unexported fields
}

BlobStorePackfileParser efficiently extracts commit OIDs from packfiles stored in object storage by using range requests to fetch only necessary data.

func NewBlobStorePackfileParser

func NewBlobStorePackfileParser(bucket *blob.Bucket) *BlobStorePackfileParser

NewBlobStorePackfileParser creates a new parser that reads from the given bucket. The parser will use default values for clustering and iteration limits.

func (*BlobStorePackfileParser) ExtractCommitOIDs

func (p *BlobStorePackfileParser) ExtractCommitOIDs(ctx context.Context, packfileKey, indexKey string) ([]OID, error)

ExtractCommitOIDs extracts all commit OIDs from a packfile by: 1. Parsing the index file to get all [OID, offset] pairs 2. Iteratively resolving object types with delta resolution:

  • Cluster work items into efficient byte ranges
  • Fetch each range and resolve objects within it
  • Follow delta chains recursively within ranges
  • Add unresolved items (bases outside range) back to workqueue

3. Repeat until all objects are resolved

This algorithm efficiently handles OFS_DELTA and REF_DELTA objects by batching range requests and resolving delta chains within each range.

Returns all commit OIDs found in the packfile.

func (*BlobStorePackfileParser) WithMaxClusterSize

func (p *BlobStorePackfileParser) WithMaxClusterSize(size int64) *BlobStorePackfileParser

WithMaxClusterSize sets the maximum size of a single range cluster. Larger values reduce the number of range requests but may fetch more unnecessary data.

func (*BlobStorePackfileParser) WithMaxConcurrentFetches

func (p *BlobStorePackfileParser) WithMaxConcurrentFetches(n int) *BlobStorePackfileParser

WithMaxConcurrentFetches sets the maximum number of concurrent range fetch operations. Higher values increase parallelism but also increase memory usage and connection count.

func (*BlobStorePackfileParser) WithMaxGap

WithMaxGap sets the maximum gap between objects before creating a new cluster. Smaller values create more separate ranges, larger values may fetch more unused data.

func (*BlobStorePackfileParser) WithMaxIterations

func (p *BlobStorePackfileParser) WithMaxIterations(maxIter int) *BlobStorePackfileParser

WithMaxIterations sets the maximum number of resolution iterations. This is useful for testing or adjusting security constraints. A lower value provides more DoS protection but may fail on legitimate packfiles with deep or scattered delta chains. A higher value allows more complex packfiles but increases vulnerability to adversarial inputs.

func (*BlobStorePackfileParser) WithMinClusterSize

func (p *BlobStorePackfileParser) WithMinClusterSize(size int64) *BlobStorePackfileParser

WithMinClusterSize sets the minimum size to read for each object. This ensures enough data is read to handle objects with delta information.

type ExtractionCallback

type ExtractionCallback func(repository, packfileURI, indexURI string) ([]OID, error)

ExtractionCallback performs synchronous commit ID extraction from a packfile. It receives the repository name, packfile URI, and index URI, and returns the extracted commit OIDs or an error.

The manager calls this in a goroutine to perform async extraction. On success, the packfile becomes available. On error, the packfile never appears to clients.

type InMemoryBasePackfileManager

type InMemoryBasePackfileManager struct {
	// contains filtered or unexported fields
}

InMemoryBasePackfileManager is a thread-safe in-memory implementation of BasePackfileManager. This is the Phase 1 implementation suitable for single-instance deployments. Phase 2 will introduce a Redis-backed implementation for multi-instance coordination.

The implementation only stores packfiles after successful extraction. Failed extractions never create entries, so no cleanup is needed.

func NewInMemoryBasePackfileManager

func NewInMemoryBasePackfileManager(callback ExtractionCallback) *InMemoryBasePackfileManager

NewInMemoryBasePackfileManager creates a new in-memory base packfile manager. The extraction callback performs synchronous extraction and is called in a goroutine.

func (*InMemoryBasePackfileManager) Delete

func (m *InMemoryBasePackfileManager) Delete(ctx context.Context, repository string) error

Delete removes a base packfile registration.

func (*InMemoryBasePackfileManager) Get

Get returns the base packfile metadata and a commit OID iterator for a repository. Both are captured from the same snapshot under a single lock acquisition, guaranteeing consistency between metadata and OIDs. Returns (nil, empty iterator, nil) if no packfile exists.

func (*InMemoryBasePackfileManager) Register

func (m *InMemoryBasePackfileManager) Register(ctx context.Context, repository, packfileURI, hashAlgorithm, packfileHash string) error

Register starts asynchronous commit ID extraction for a base packfile. The packfile won't be visible until extraction completes successfully.

type OID

type OID struct {
	// contains filtered or unexported fields
}

OID represents an immutable Git object identifier in binary form. For SHA-1: 20 bytes, for SHA-256: 32 bytes. The internal data slice is unexported to enforce immutability. This reduces memory usage by 50% compared to hex-encoded strings.

func OIDFromHex

func OIDFromHex(s string) (OID, error)

OIDFromHex creates an OID from a hex-encoded string.

func OIDMustParse

func OIDMustParse(s string) OID

OIDMustParse creates an OID from a hex-encoded string, panicking on error. This is primarily useful in tests and initialization code where the input is known to be valid.

func (OID) Bytes

func (o OID) Bytes() []byte

Bytes returns a copy of the OID bytes. The copy ensures the OID remains immutable even if the caller modifies the result.

func (OID) Equal

func (o OID) Equal(other OID) bool

Equal returns true if two OIDs are equal.

func (OID) MarshalJSON

func (o OID) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, encoding OID as a hex string.

func (OID) String

func (o OID) String() string

String returns the hex-encoded representation of the OID.

func (*OID) UnmarshalJSON

func (o *OID) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler, decoding from a hex string.

type ObjectType

type ObjectType int

ObjectType represents the type of a Git object in a packfile.

const (
	ObjectCommit   ObjectType = 1
	ObjectTree     ObjectType = 2
	ObjectBlob     ObjectType = 3
	ObjectTag      ObjectType = 4
	ObjectOfsDelta ObjectType = 6
	ObjectRefDelta ObjectType = 7
)

Jump to

Keyboard shortcuts

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