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 }