Export the loaded manifest type as Manifest (closes #236) #238

Merged
clawbot merged 1 commits from issue-236-export-manifest-type into next 2026-10-21 13:12:21 +02:00
8 changed files with 74 additions and 30 deletions
+1 -1
View File
@@ -335,7 +335,7 @@ func (b *Builder) Build(ctx context.Context, w io.Writer) error {
} }
// Create a temporary manifest to use existing serialization // Create a temporary manifest to use existing serialization
m := &manifest{ m := &Manifest{
pbInner: inner, pbInner: inner,
signingOptions: b.signingOptions, signingOptions: b.signingOptions,
fixedUUID: b.fixedUUID, fixedUUID: b.fixedUUID,
+1 -1
View File
@@ -123,7 +123,7 @@ func NewChecker(opts *CheckerOptions) (*Checker, error) {
} }
var ( var (
m *manifest m *Manifest
manifestInfo os.FileInfo manifestInfo os.FileInfo
err error err error
) )
+7 -11
View File
@@ -48,7 +48,7 @@ func validateUUID(data []byte) error {
// validateOuterHeader checks the outer message's version, compression // validateOuterHeader checks the outer message's version, compression
// type, and UUID. // type, and UUID.
func (m *manifest) validateOuterHeader() error { func (m *Manifest) validateOuterHeader() error {
if m.pbOuter.GetVersion() != MFFileOuter_VERSION_ONE { if m.pbOuter.GetVersion() != MFFileOuter_VERSION_ONE {
return errUnknownVersion return errUnknownVersion
} }
@@ -70,7 +70,7 @@ func (m *manifest) validateOuterHeader() error {
// signature is present, verifies it against the embedded public key, which // signature is present, verifies it against the embedded public key, which
// must be one key, and checks that the signer field is that key's // must be one key, and checks that the signer field is that key's
// fingerprint. // fingerprint.
func (m *manifest) verifyOuterIntegrity() error { func (m *Manifest) verifyOuterIntegrity() error {
h := sha256.New() h := sha256.New()
_, err := h.Write(m.pbOuter.GetInnerMessage()) _, err := h.Write(m.pbOuter.GetInnerMessage())
@@ -117,7 +117,7 @@ func (m *manifest) verifyOuterIntegrity() error {
// decompressInner decompresses the inner payload, enforcing size limits // decompressInner decompresses the inner payload, enforcing size limits
// to prevent decompression bombs. // to prevent decompression bombs.
func (m *manifest) decompressInner() ([]byte, error) { func (m *Manifest) decompressInner() ([]byte, error) {
bb := bytes.NewBuffer(m.pbOuter.GetInnerMessage()) bb := bytes.NewBuffer(m.pbOuter.GetInnerMessage())
// By default the decoder decodes a payload under 128 KiB in full, // By default the decoder decodes a payload under 128 KiB in full,
@@ -259,7 +259,7 @@ func validateEntryHashes(entry *MFFilePath) error {
} }
} }
func (m *manifest) deserializeInner() error { func (m *Manifest) deserializeInner() error {
err := m.validateOuterHeader() err := m.validateOuterHeader()
if err != nil { if err != nil {
return err return err
@@ -364,10 +364,8 @@ func validateMagic(dat []byte) bool {
// NewManifestFromReader reads a manifest from an io.Reader. It refuses a // NewManifestFromReader reads a manifest from an io.Reader. It refuses a
// manifest larger than MaxManifestSize, reading at most one byte past it. // manifest larger than MaxManifestSize, reading at most one byte past it.
// func NewManifestFromReader(input io.Reader) (*Manifest, error) {
//nolint:revive // unexported-return: exporting manifest is owner question 13 m := &Manifest{}
func NewManifestFromReader(input io.Reader) (*manifest, error) {
m := &manifest{}
dat, err := readAtMost(input, MaxManifestSize) dat, err := readAtMost(input, MaxManifestSize)
if err != nil { if err != nil {
@@ -426,9 +424,7 @@ type ManifestFromFileOptions struct {
// NewManifestFromFile reads a manifest from a file. It returns an error if // NewManifestFromFile reads a manifest from a file. It returns an error if
// opts is nil or its path is empty. // opts is nil or its path is empty.
// func NewManifestFromFile(opts *ManifestFromFileOptions) (*Manifest, error) {
//nolint:revive // unexported-return: exporting manifest is owner question 13
func NewManifestFromFile(opts *ManifestFromFileOptions) (*manifest, error) {
if opts == nil || opts.Path == "" { if opts == nil || opts.Path == "" {
return nil, errManifestPathEmpty return nil, errManifestPathEmpty
} }
+1 -1
View File
@@ -85,7 +85,7 @@ func TestValidatePathMessagesVerbatim(t *testing.T) {
func TestSerializeInnerNotSetMessagesVerbatim(t *testing.T) { func TestSerializeInnerNotSetMessagesVerbatim(t *testing.T) {
t.Parallel() t.Parallel()
m := &manifest{} m := &Manifest{}
err := m.generate(context.Background()) err := m.generate(context.Background())
require.ErrorIs(t, err, errInnerNotSet) require.ErrorIs(t, err, errInnerNotSet)
+11 -12
View File
@@ -15,13 +15,10 @@ var (
errSHA256NotSet = errors.New("SHA256 hash not set") errSHA256NotSet = errors.New("SHA256 hash not set")
) )
// manifest holds the internal representation of a manifest file. // Manifest is a manifest loaded by NewManifestFromFile or
// Use NewManifestFromFile or NewManifestFromReader to load an existing // NewManifestFromReader. Its fields are private; only its methods are the
// manifest, or use Builder to create a new one. // library's API.
// type Manifest struct {
// Whether this type should be exported is an open design question owned by
// the repository owner; see README design question 13.
type manifest struct {
pbInner *MFFile pbInner *MFFile
pbOuter *MFFileOuter pbOuter *MFFileOuter
output *bytes.Buffer output *bytes.Buffer
@@ -29,7 +26,9 @@ type manifest struct {
fixedUUID []byte // if set, use this UUID instead of generating one fixedUUID []byte // if set, use this UUID instead of generating one
} }
func (m *manifest) String() string { // String returns a short description giving the number of files the
// manifest lists, such as "<Manifest count=3>".
func (m *Manifest) String() string {
count := 0 count := 0
if m.pbInner != nil { if m.pbInner != nil {
count = len(m.pbInner.GetFiles()) count = len(m.pbInner.GetFiles())
@@ -39,7 +38,7 @@ func (m *manifest) String() string {
} }
// Files returns all file entries from a loaded manifest. // Files returns all file entries from a loaded manifest.
func (m *manifest) Files() []*MFFilePath { func (m *Manifest) Files() []*MFFilePath {
if m.pbInner == nil { if m.pbInner == nil {
return nil return nil
} }
@@ -50,7 +49,7 @@ func (m *manifest) Files() []*MFFilePath {
// Signer returns the fingerprint of the key that made a loaded manifest's // Signer returns the fingerprint of the key that made a loaded manifest's
// signature, which loading the manifest checked, or nil if the manifest is // signature, which loading the manifest checked, or nil if the manifest is
// not signed. // not signed.
func (m *manifest) Signer() []byte { func (m *Manifest) Signer() []byte {
if len(m.pbOuter.GetSignature()) == 0 { if len(m.pbOuter.GetSignature()) == 0 {
return nil return nil
} }
@@ -60,14 +59,14 @@ func (m *manifest) Signer() []byte {
// CreatedAt returns the createdAt timestamp of a loaded manifest, or nil // CreatedAt returns the createdAt timestamp of a loaded manifest, or nil
// if the manifest has none. // if the manifest has none.
func (m *manifest) CreatedAt() *Timestamp { func (m *Manifest) CreatedAt() *Timestamp {
return m.pbInner.GetCreatedAt() return m.pbInner.GetCreatedAt()
} }
// signatureString generates the canonical string used for signing/verification. // signatureString generates the canonical string used for signing/verification.
// Format: MAGIC-UUID-MULTIHASH where UUID and multihash are hex-encoded. // Format: MAGIC-UUID-MULTIHASH where UUID and multihash are hex-encoded.
// Requires pbOuter to be set with Uuid and Sha256 fields. // Requires pbOuter to be set with Uuid and Sha256 fields.
func (m *manifest) signatureString() (string, error) { func (m *Manifest) signatureString() (string, error) {
if m.pbOuter == nil { if m.pbOuter == nil {
return "", errOuterNotSet return "", errOuterNotSet
} }
+49
View File
@@ -0,0 +1,49 @@
package mfer_test
import (
"bytes"
"context"
"testing"
"github.com/stretchr/testify/require"
"sneak.berlin/go/mfer/mfer"
)
// archive keeps a loaded manifest in a field, as a type in a program that
// imports mfer would.
type archive struct {
manifest *mfer.Manifest
}
// fileCount takes a loaded manifest, as a function in such a program would.
func fileCount(m *mfer.Manifest) int {
return len(m.Files())
}
// A program that imports mfer can name the type of a loaded manifest, keep
// one in a struct field, pass it to its own functions, and read it through
// its methods.
func TestManifestIsUsableOutsideThePackage(t *testing.T) {
t.Parallel()
content := []byte("hello\n")
builder := mfer.NewBuilder()
_, err := builder.AddFile("hello.txt", mfer.FileSize(len(content)),
mfer.ModTime{}, 0, bytes.NewReader(content), nil)
require.NoError(t, err)
var written bytes.Buffer
require.NoError(t, builder.Build(context.Background(), &written))
loaded, err := mfer.NewManifestFromReader(&written)
require.NoError(t, err)
a := archive{manifest: loaded}
require.Equal(t, 1, fileCount(a.manifest))
require.Equal(t, "hello.txt", a.manifest.Files()[0].GetPath())
require.Nil(t, a.manifest.Signer())
require.Nil(t, a.manifest.CreatedAt())
require.Equal(t, "<Manifest count=1>", a.manifest.String())
}
+1 -1
View File
@@ -21,7 +21,7 @@ func TestSignatureString(t *testing.T) {
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
require.NoError(t, err) require.NoError(t, err)
m := &manifest{pbOuter: &MFFileOuter{Uuid: manifestUUID[:], Sha256: sha256Hash}} m := &Manifest{pbOuter: &MFFileOuter{Uuid: manifestUUID[:], Sha256: sha256Hash}}
got, err := m.signatureString() got, err := m.signatureString()
require.NoError(t, err) require.NoError(t, err)
+3 -3
View File
@@ -51,7 +51,7 @@ func newTimestampFromTime(t time.Time) *Timestamp {
} }
} }
func (m *manifest) generate(ctx context.Context) error { func (m *Manifest) generate(ctx context.Context) error {
if m.pbInner == nil { if m.pbInner == nil {
return errInnerNotSet return errInnerNotSet
} }
@@ -78,7 +78,7 @@ func (m *manifest) generate(ctx context.Context) error {
return nil return nil
} }
func (m *manifest) generateOuter(ctx context.Context) error { func (m *Manifest) generateOuter(ctx context.Context) error {
if m.pbInner == nil { if m.pbInner == nil {
return errInternal return errInternal
} }
@@ -145,7 +145,7 @@ func (m *manifest) generateOuter(ctx context.Context) error {
// signOuter signs the outer message with the secret key in the signing // signOuter signs the outer message with the secret key in the signing
// options and embeds the signature, the key's fingerprint and its public // options and embeds the signature, the key's fingerprint and its public
// key. // key.
func (m *manifest) signOuter(ctx context.Context) error { func (m *Manifest) signOuter(ctx context.Context) error {
// Unlocking a protected key can take a while; do not start once ctx // Unlocking a protected key can take a while; do not start once ctx
// has ended. // has ended.
err := ctx.Err() err := ctx.Err()