package ingest import ( "errors" "fmt" "hash/crc32" "io" "os" "lindenii.org/go/furgit/internal/compress/zlib" "lindenii.org/go/furgit/internal/format/packfile" "lindenii.org/go/furgit/internal/progress" "lindenii.org/go/furgit/object/id" "lindenii.org/go/furgit/object/store" "lindenii.org/go/furgit/object/typ" "lindenii.org/go/lgo/intconv" ) // fixThin completes a thin pack // by appending the external bases it references, // rewriting the pack header and trailer, // and resolving the deltas reached from the appended bases. func (ingestion *ingestion) fixThin(external []id.ObjectID, adjacency adjacency, meter *progress.Meter) error { if ingestion.opts.ThinBase == nil { return ErrThinPackNotPermitted } hashSize := ingestion.objectFormat.Size() if ingestion.scanner.consumed < packfile.HeaderLen+hashSize { return fmt.Errorf("%w: pack shorter than trailer", ErrMalformedPack) } // Drop the trailer from the write cursor. ingestion.scanner.consumed -= hashSize appended := make([]int, 0, len(external)) for _, baseOID := range external { ty, content, err := ingestion.opts.ThinBase.ReadBytesContent(baseOID) if errors.Is(err, store.ErrObjectNotFound) { continue } if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: reading thin base %s: %w", baseOID, err) } index, err := ingestion.appendBaseObject(baseOID, ty, content) if err != nil { return err } appended = append(appended, index) } err := ingestion.rewriteHeaderTrailer() if err != nil { return err } err = ingestion.resolveFrom(appended, adjacency, meter) if err != nil { return err } missing := ingestion.unresolvedExternalBases() if len(missing) > 0 { return &ThinBasesMissingError{OIDs: missing} } if ingestion.countUnresolved() > 0 { return fmt.Errorf("%w: unresolvable delta entries after thin completion", ErrMalformedPack) } ingestion.thinFixed = len(appended) > 0 return nil } // appendBaseObject appends one external thin base // as a non-delta pack entry at the current write cursor, // verifying that its content hashes to the requested object ID. func (ingestion *ingestion) appendBaseObject(objectID id.ObjectID, objectType typ.Type, content []byte) (int, error) { entryType, err := packfile.EntryTypeFromObjectType(objectType) if err != nil { return 0, fmt.Errorf("object/store/packed/internal/ingest: %w", err) } computed, err := ingestion.hashObject(entryType, content) if err != nil { return 0, err } if computed != objectID { return 0, fmt.Errorf("%w: thin base %s content hashes to %s", ErrMalformedPack, objectID, computed) } start := ingestion.scanner.consumed startOffset := int64(start) headerBytes := packfile.AppendTypeSize(nil, entryType, uint64(len(content))) _, err = ingestion.packFile.WriteAt(headerBytes, startOffset) if err != nil { return 0, fmt.Errorf("object/store/packed/internal/ingest: writing thin base header: %w", err) } crc := crc32.NewIEEE() _, _ = crc.Write(headerBytes) dataOffset := startOffset + int64(len(headerBytes)) writer := &offsetWriter{file: ingestion.packFile, offset: dataOffset} zw := zlib.NewWriter(io.MultiWriter(writer, crc)) _, err = zw.Write(content) if err != nil { _ = zw.Close() return 0, fmt.Errorf("object/store/packed/internal/ingest: compressing thin base: %w", err) } err = zw.Close() if err != nil { return 0, fmt.Errorf("object/store/packed/internal/ingest: compressing thin base: %w", err) } headerLen := len(headerBytes) packedLen := headerLen + writer.written ingestion.scanner.consumed = start + packedLen rec := record{ offset: start, headerLen: headerLen, packedLen: packedLen, crc32: crc.Sum32(), packedType: entryType, declaredSize: len(content), baseOffset: 0, baseOID: id.ObjectID{}, objectType: entryType, oid: objectID, resolved: true, } index := len(ingestion.records) ingestion.records = append(ingestion.records, rec) ingestion.byOffset[start] = index ingestion.byOID[objectID] = index return index, nil } // rewriteHeaderTrailer updates the pack object count // and recomputes the pack trailer hash // over the entries left after thin completion. func (ingestion *ingestion) rewriteHeaderTrailer() error { count, err := intconv.IntToUint32(len(ingestion.records)) if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: %w", err) } _, err = ingestion.packFile.WriteAt(packfile.AppendHeader(nil, count), 0) if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: rewriting header: %w", err) } bodyEnd := int64(ingestion.scanner.consumed) hashImpl, err := ingestion.objectFormat.New() if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: %w", err) } _, err = io.Copy(hashImpl, io.NewSectionReader(ingestion.packFile, 0, bodyEnd)) if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: rehashing pack: %w", err) } sum := hashImpl.Sum(nil) _, err = ingestion.packFile.WriteAt(sum, bodyEnd) if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: writing trailer: %w", err) } packHash, err := ingestion.objectFormat.FromBytes(sum) if err != nil { return fmt.Errorf("object/store/packed/internal/ingest: %w", err) } ingestion.packHash = packHash return nil } // offsetWriter writes to a file via WriteAt, // advancing sequentially from a base offset // and counting the bytes written. type offsetWriter struct { file *os.File offset int64 written int } // Write implements [io.Writer]. func (writer *offsetWriter) Write(p []byte) (int, error) { n, err := writer.file.WriteAt(p, writer.offset) writer.offset += int64(n) writer.written += n return n, err //nolint:wrapcheck }