Add public bundle manifest package

This commit is contained in:
2026-06-01 20:52:23 +00:00
parent e51bc28b05
commit bb68cb6602
17 changed files with 941 additions and 181 deletions

View File

@@ -1,12 +1,10 @@
package bundle
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"regexp"
"strconv"
"strings"
publicbundle "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
)
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
@@ -19,31 +17,13 @@ func ValidateDigest(value string) error {
}
func FileDigest(data []byte) string {
sum := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(sum[:])
return publicbundle.FileDigest(data)
}
func BundleDigest(files []ManifestFile) string {
canonical := CanonicalFilePayload(files)
sum := sha256.Sum256([]byte(canonical))
return "sha256:" + hex.EncodeToString(sum[:])
return publicbundle.BundleDigest(files)
}
func CanonicalFilePayload(files []ManifestFile) string {
var builder strings.Builder
builder.WriteByte('[')
for index, file := range files {
if index > 0 {
builder.WriteByte(',')
}
builder.WriteString(`{"path":`)
builder.WriteString(strconv.Quote(file.Path))
builder.WriteString(`,"sha256":`)
builder.WriteString(strconv.Quote(file.SHA256))
builder.WriteString(`,"size":`)
builder.WriteString(strconv.FormatInt(file.Size, 10))
builder.WriteByte('}')
}
builder.WriteByte(']')
return builder.String()
return publicbundle.CanonicalFilePayload(files)
}

View File

@@ -1,110 +1,32 @@
package bundle
import (
"bytes"
"encoding/json"
"fmt"
"io"
"time"
)
import publicbundle "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
const ManifestName = "manifest.json"
const ManifestName = publicbundle.ManifestName
type Manifest struct {
SchemaVersion int `json:"schema_version"`
ID string `json:"id"`
Digest string `json:"digest"`
Created time.Time `json:"created"`
Files []ManifestFile `json:"files"`
}
const SchemaVersion = publicbundle.SchemaVersion
type ManifestFile struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
type Manifest = publicbundle.Manifest
type ManifestFile = publicbundle.ManifestFile
type Bundle struct {
RootRelativePath string
Manifest Manifest
}
type rawManifest struct {
SchemaVersion *int `json:"schema_version"`
ID *string `json:"id"`
Digest *string `json:"digest"`
Created *string `json:"created"`
Files []rawManifestFile `json:"files"`
}
type rawManifestFile struct {
Path *string `json:"path"`
SHA256 *string `json:"sha256"`
Size *int64 `json:"size"`
}
func ParseManifest(data []byte) (Manifest, error) {
decoder := json.NewDecoder(bytes.NewReader(data))
var raw rawManifest
if err := decoder.Decode(&raw); err != nil {
return Manifest{}, fmt.Errorf("parse manifest: %w", err)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
return Manifest{}, fmt.Errorf("parse manifest: trailing data")
}
var manifest Manifest
if raw.SchemaVersion == nil {
return Manifest{}, fmt.Errorf("manifest schema_version is required")
}
manifest.SchemaVersion = *raw.SchemaVersion
if raw.ID == nil || *raw.ID == "" {
return Manifest{}, fmt.Errorf("manifest id is required")
}
manifest.ID = *raw.ID
if raw.Digest == nil || *raw.Digest == "" {
return Manifest{}, fmt.Errorf("manifest digest is required")
}
manifest.Digest = *raw.Digest
if raw.Created == nil || *raw.Created == "" {
return Manifest{}, fmt.Errorf("manifest created is required")
}
created, err := time.Parse(time.RFC3339, *raw.Created)
if err != nil {
return Manifest{}, fmt.Errorf("manifest created must be RFC3339: %w", err)
}
manifest.Created = created
if len(raw.Files) == 0 {
return Manifest{}, fmt.Errorf("manifest files is required")
}
for index, rawFile := range raw.Files {
file, err := parseManifestFile(index, rawFile)
if err != nil {
return Manifest{}, err
}
manifest.Files = append(manifest.Files, file)
}
if err := ValidateManifest(manifest); err != nil {
return Manifest{}, fmt.Errorf("manifest %w", err)
}
return manifest, nil
return publicbundle.ParseManifest(data)
}
func parseManifestFile(index int, raw rawManifestFile) (ManifestFile, error) {
if raw.Path == nil || *raw.Path == "" {
return ManifestFile{}, fmt.Errorf("manifest files[%d].path is required", index)
}
if raw.SHA256 == nil || *raw.SHA256 == "" {
return ManifestFile{}, fmt.Errorf("manifest files[%d].sha256 is required", index)
}
if raw.Size == nil {
return ManifestFile{}, fmt.Errorf("manifest files[%d].size is required", index)
}
return ManifestFile{
Path: *raw.Path,
SHA256: *raw.SHA256,
Size: *raw.Size,
}, nil
func MarshalManifest(manifest Manifest) ([]byte, error) {
return publicbundle.MarshalManifest(manifest)
}
func ValidateManifest(manifest Manifest) error {
return publicbundle.ValidateManifest(manifest)
}
func ValidateSourcePath(path string) error {
return publicbundle.ValidateSourcePath(path)
}

View File

@@ -7,55 +7,6 @@ import (
"gitea.maximumdirect.net/eric/distributor/internal/storage"
)
func ValidateSourcePath(path string) error {
if err := storage.ValidatePath(path); err != nil {
return err
}
switch path {
case ManifestName, storage.StateFileName:
return fmt.Errorf("%q is reserved", path)
}
return nil
}
func ValidateManifest(manifest Manifest) error {
if manifest.SchemaVersion != 1 {
return fmt.Errorf("schema_version must be 1")
}
if manifest.ID == "" {
return fmt.Errorf("id is required")
}
if err := ValidateDigest(manifest.Digest); err != nil {
return fmt.Errorf("digest: %w", err)
}
if manifest.Created.IsZero() {
return fmt.Errorf("created is required")
}
if len(manifest.Files) == 0 {
return fmt.Errorf("files is required")
}
seen := make(map[string]struct{}, len(manifest.Files))
for index, file := range manifest.Files {
if err := ValidateSourcePath(file.Path); err != nil {
return fmt.Errorf("files[%d].path: %w", index, err)
}
if err := ValidateDigest(file.SHA256); err != nil {
return fmt.Errorf("files[%d].sha256: %w", index, err)
}
if file.Size < 0 {
return fmt.Errorf("files[%d].size must be non-negative", index)
}
if _, exists := seen[file.Path]; exists {
return fmt.Errorf("files[%d].path duplicates %q", index, file.Path)
}
seen[file.Path] = struct{}{}
}
if actual := BundleDigest(manifest.Files); actual != manifest.Digest {
return fmt.Errorf("digest mismatch: got %s want %s", actual, manifest.Digest)
}
return nil
}
func Validate(ctx context.Context, backend storage.Backend, bundleRoot string) (Bundle, error) {
return validateAt(ctx, backend, bundleRoot, bundleRoot)
}