package main
import (
"bufio"
"bytes"
"encoding/hex"
"flag"
"fmt"
"io"
"os"
"strings"
"lindenii.org/go/furgit/internal/format/packfile"
"lindenii.org/go/furgit/internal/format/packidx"
"lindenii.org/go/furgit/internal/mmap"
"lindenii.org/go/furgit/internal/utils"
"lindenii.org/go/furgit/object/id"
)
func main() {
format := flag.String("format", "", "object format of the pack: sha1 or sha256 (required)")
flag.Parse()
err := run(*format, flag.Args(), os.Stdin, os.Stdout)
if err != nil {
fmt.Fprintln(os.Stderr, "explain-pack:", err)
os.Exit(1)
}
}
type explainer struct {
data []byte
objectFormat id.ObjectFormat
out *bufio.Writer
idx *packidx.Packidx
cache *baseCache
oidIndex map[id.ObjectID]int
}
func run(format string, args []string, stdin io.Reader, stdout io.Writer) error {
if format == "" {
return fmt.Errorf("the -format flag is required (sha1 or sha256)")
}
objectFormat, err := id.ParseObjectFormat(format)
if err != nil {
return fmt.Errorf("invalid -format %q: %w", format, err)
}
if len(args) > 1 {
return fmt.Errorf("at most one pack file argument is accepted, got %d", len(args))
}
data, idx, closers, err := openInput(args, objectFormat, stdin)
if err != nil {
return err
}
defer func() {
for _, c := range closers {
_ = c.Close()
}
}()
out := bufio.NewWriter(stdout)
explainer := &explainer{
data: data,
objectFormat: objectFormat,
out: out,
idx: idx,
cache: newBaseCache(),
oidIndex: make(map[id.ObjectID]int),
}
err = explainer.explain()
if err != nil {
return err
}
return out.Flush()
}
func openInput(args []string, objectFormat id.ObjectFormat, stdin io.Reader) ([]byte, *packidx.Packidx, []io.Closer, error) {
if len(args) == 0 {
data, err := io.ReadAll(stdin)
if err != nil {
return nil, nil, nil, fmt.Errorf("reading pack from stdin: %w", err)
}
return data, nil, nil, nil
}
packPath := args[0]
packMapping, err := mapPath(packPath)
if err != nil {
return nil, nil, nil, err
}
closers := []io.Closer{packMapping}
idx, idxMapping, err := openIndex(packPath, objectFormat)
if err != nil {
_ = packMapping.Close()
return nil, nil, nil, err
}
if idxMapping != nil {
closers = append(closers, idxMapping)
}
return packMapping.Data(), idx, closers, nil
}
func openIndex(packPath string, objectFormat id.ObjectFormat) (*packidx.Packidx, *mmap.Mmap, error) {
idxPath := strings.TrimSuffix(packPath, ".pack") + ".idx"
file, err := os.Open(idxPath) //#nosec G304
if err != nil {
if os.IsNotExist(err) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("opening index %q: %w", idxPath, err)
}
defer func() { _ = file.Close() }()
mapping, err := mmap.Open(file)
if err != nil {
return nil, nil, fmt.Errorf("mapping index %q: %w", idxPath, err)
}
idx, err := packidx.Parse(mapping.Data(), objectFormat.Size())
if err != nil {
_ = mapping.Close()
return nil, nil, fmt.Errorf("parsing index %q: %w", idxPath, err)
}
return &idx, mapping, nil
}
func mapPath(path string) (*mmap.Mmap, error) {
file, err := os.Open(path) //#nosec G304
if err != nil {
return nil, fmt.Errorf("opening pack %q: %w", path, err)
}
defer func() { _ = file.Close() }()
mapping, err := mmap.Open(file)
if err != nil {
return nil, fmt.Errorf("mapping pack %q: %w", path, err)
}
return mapping, nil
}
func (explainer *explainer) printf(format string, args ...any) {
utils.BestEffortFprintf(explainer.out, format, args...)
}
func (explainer *explainer) explain() error {
hashSize := explainer.objectFormat.Size()
if len(explainer.data) < packfile.HeaderLen+hashSize {
return fmt.Errorf("pack is too short to contain a header and a %d-byte trailer", hashSize)
}
count, err := explainer.explainHeader()
if err != nil {
return err
}
cursor := packfile.HeaderLen
for num := 1; num <= count; num++ {
next, err := explainer.explainEntry(num, count, cursor)
if err != nil {
return err
}
cursor = next
}
return explainer.explainTrailer(cursor)
}
func (explainer *explainer) explainHeader() (int, error) {
header, err := packfile.ParseHeader(explainer.data[:packfile.HeaderLen])
if err != nil {
return 0, fmt.Errorf("pack header: %w", err)
}
explainer.printf("pack header\n")
explainer.printf("\tmagic\t\"PACK\"\n")
explainer.printf("\tversion\t2\n")
explainer.printf("\tobjects\t%d\n", header.ObjectCount)
explainer.printf("\n")
return int(header.ObjectCount), nil
}
func (explainer *explainer) explainTrailer(cursor int) error {
hashSize := explainer.objectFormat.Size()
trailerStart := len(explainer.data) - hashSize
if cursor != trailerStart {
explainer.printf(
"note\t%d byte(s) between the last entry and the trailer were unaccounted for\n",
trailerStart-cursor,
)
}
trailer := explainer.data[trailerStart:]
explainer.printf("pack trailer\n")
explainer.printf("\tchecksum\t%s\n", hex.EncodeToString(trailer))
hashImpl, err := explainer.objectFormat.New()
if err != nil {
return fmt.Errorf("object/store: %w", err)
}
_, _ = hashImpl.Write(explainer.data[:trailerStart])
if bytes.Equal(hashImpl.Sum(nil), trailer) {
explainer.printf("\trecomputed\tmatches\n")
} else {
explainer.printf("\trecomputed\tMISMATCH (corrupt pack or wrong -format)\n")
}
return nil
}