package loose import ( "bufio" "bytes" "errors" "fmt" "io" "io/fs" "os" "lindenii.org/go/furgit/internal/compress/zlib" "lindenii.org/go/furgit/internal/iolimit" "lindenii.org/go/furgit/object/id" "lindenii.org/go/furgit/object/store" "lindenii.org/go/furgit/object/typ" ) // ReadBytesFull reads a full serialized object as "type size\x00content". // // It inflates and parses the full loose object, // including verifying the zlib Adler-32 trailer. func (loose *Loose) ReadBytesFull(objectID id.ObjectID) ([]byte, error) { raw, _, _, err := loose.readBytesParsed(objectID) if err != nil { return nil, err } return raw, nil } // ReadBytesContent reads an object's type and content bytes. // // Like ReadBytesFull, // it inflates and parses the full loose object, // including verifying the zlib Adler-32 trailer. func (loose *Loose) ReadBytesContent(objectID id.ObjectID) (typ.Type, []byte, error) { _, ty, content, err := loose.readBytesParsed(objectID) if err != nil { return typ.TypeUnknown, nil, err } return ty, content, nil } // ReadHeader reads an object's type and declared content length. // // It parses only enough of the zlib-decoded object to recover the object header. // It does not verify that the remaining object content is readable // and does not verify the zlib Adler-32 trailer. func (loose *Loose) ReadHeader(objectID id.ObjectID) (typ.Type, uint64, error) { file, err := loose.openObject(objectID) if err != nil { return typ.TypeUnknown, 0, err } defer func() { _ = file.Close() }() zr, err := zlib.NewReader(file) if err != nil { return typ.TypeUnknown, 0, fmt.Errorf("object/store/loose: %w", err) } defer func() { _ = zr.Close() }() _, ty, size, err := readHeader(bufio.NewReader(zr)) if err != nil { return typ.TypeUnknown, 0, err } return ty, size, nil } // ReadSize reads an object's declared content length. // // Like ReadHeader, // it parses only enough of the zlib-decoded object to recover the header // and does not verify the zlib Adler-32 trailer. func (loose *Loose) ReadSize(objectID id.ObjectID) (uint64, error) { _, size, err := loose.ReadHeader(objectID) return size, err } // ReadReaderFull reads a full serialized object stream as "type size\x00content". // // Close releases resources only. // It does not drain unread data for additional validation. // In particular, // malformed trailing compressed data, // trailing bytes past the declared object size, // and the zlib Adler-32 trailer // may go unverified unless the caller reads to io.EOF. func (loose *Loose) ReadReaderFull(objectID id.ObjectID) (io.ReadCloser, error) { file, zr, err := loose.openInflated(objectID) if err != nil { return nil, err } br := bufio.NewReader(zr) headerBytes, _, size, err := readHeader(br) if err != nil { _ = zr.Close() _ = file.Close() return nil, err } return &objectReader{ reader: io.MultiReader( bytes.NewReader(headerBytes), iolimit.ExpectLengthReader(br, size), ), file: file, zr: zr, }, nil } // ReadReaderContent reads an object's type, declared content length, // and content stream. // // Close releases resources only. // It does not drain unread data for additional validation. // In particular, // malformed trailing compressed data, // trailing bytes past the declared object size, // and the zlib Adler-32 trailer // may go unverified unless the caller reads to io.EOF. func (loose *Loose) ReadReaderContent(objectID id.ObjectID) (typ.Type, uint64, io.ReadCloser, error) { file, zr, err := loose.openInflated(objectID) if err != nil { return typ.TypeUnknown, 0, nil, err } br := bufio.NewReader(zr) _, ty, size, err := readHeader(br) if err != nil { _ = zr.Close() _ = file.Close() return typ.TypeUnknown, 0, nil, err } return ty, size, &objectReader{ reader: iolimit.ExpectLengthReader(br, size), file: file, zr: zr, }, nil } // Refresh is a no-op for loose object stores. func (loose *Loose) Refresh() error { return nil } // openObject opens the loose object file for objectID. // Missing files cause store.ErrObjectNotFound. func (loose *Loose) openObject(objectID id.ObjectID) (*os.File, error) { relPath, err := loose.objectPath(objectID) if err != nil { return nil, err } file, err := loose.root.Open(relPath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, store.ErrObjectNotFound } return nil, fmt.Errorf("object/store/loose: %w", err) } return file, nil } // readBytesParsed reads, inflates, and parses a loose object in one pass. // It returns the full raw payload and its parsed type and content. func (loose *Loose) readBytesParsed(objectID id.ObjectID) ([]byte, typ.Type, []byte, error) { file, err := loose.openObject(objectID) if err != nil { return nil, typ.TypeUnknown, nil, err } defer func() { _ = file.Close() }() raw, err := decodeAll(file) if err != nil { return nil, typ.TypeUnknown, nil, err } ty, content, err := parseRaw(raw) if err != nil { return nil, typ.TypeUnknown, nil, err } return raw, ty, content, nil } // openInflated opens and zlib-decodes a loose object file. // The caller owns both returned closers and must close them. func (loose *Loose) openInflated(objectID id.ObjectID) (*os.File, io.ReadCloser, error) { file, err := loose.openObject(objectID) if err != nil { return nil, nil, err } zr, err := zlib.NewReader(file) if err != nil { _ = file.Close() return nil, nil, fmt.Errorf("object/store/loose: %w", err) } return file, zr, nil } // objectReader streams one inflated loose object // and owns the underlying file and zlib decoder. type objectReader struct { // reader is the stream exposed by Read. reader io.Reader // file is the underlying loose object file and is closed by Close. file *os.File // zr is the zlib decoder and is closed by Close. zr io.ReadCloser } func (reader *objectReader) Read(dst []byte) (int, error) { return reader.reader.Read(dst) //nolint:wrapcheck } func (reader *objectReader) Close() error { errZlib := reader.zr.Close() errFile := reader.file.Close() return errors.Join(errZlib, errFile) }