Consolidate artifact declarations

This commit is contained in:
2026-10-06 17:29:41 +00:00
parent 3e9b5c1b99
commit 0eec4a30ed
9 changed files with 175 additions and 76 deletions

View File

@@ -0,0 +1,62 @@
// Package artifact owns shared build artifact identity rules.
package artifact
import (
"fmt"
"strings"
)
const ManifestFilename = "SHA256SUMS"
type Target struct {
OS string
Arch string
}
type Declaration struct {
Target Target
Filename string
}
// NormalizeTargets removes duplicate targets while preserving their first
// occurrence.
func NormalizeTargets(targets []Target) []Target {
seen := make(map[Target]struct{}, len(targets))
result := make([]Target, 0, len(targets))
for _, target := range targets {
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
// ValidComponent reports whether value is a nonempty filename component under
// the build and receipt contracts.
func ValidComponent(value string) bool {
return value != "" && !strings.ContainsAny(value, "/\\\r\n")
}
// Filename returns the declared binary filename for one target.
func Filename(binaryName, version string, target Target) string {
filename := fmt.Sprintf("%s-%s-%s-%s", binaryName, version, target.OS, target.Arch)
if target.OS == "windows" {
filename += ".exe"
}
return filename
}
// ExpectedBuild returns artifact declarations in target order. Callers retain
// responsibility for validating their boundary-specific build identity.
func ExpectedBuild(binaryName, version string, targets []Target) []Declaration {
declarations := make([]Declaration, 0, len(targets))
for _, target := range targets {
declarations = append(declarations, Declaration{
Target: target,
Filename: Filename(binaryName, version, target),
})
}
return declarations
}

View File

@@ -0,0 +1,42 @@
package artifact
import (
"reflect"
"testing"
)
func TestNormalizeTargetsPreservesOrderAndFirstOccurrence(t *testing.T) {
targets := []Target{
{OS: "linux", Arch: "amd64"},
{OS: "windows", Arch: "arm64"},
{OS: "linux", Arch: "amd64"},
}
want := []Target{{OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "arm64"}}
if got := NormalizeTargets(targets); !reflect.DeepEqual(got, want) {
t.Fatalf("NormalizeTargets() = %#v, want %#v", got, want)
}
}
func TestExpectedBuildUsesSharedNamesInTargetOrder(t *testing.T) {
targets := []Target{{OS: "windows", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}}
want := []Declaration{
{Target: targets[0], Filename: "app-v1.2.3-windows-arm64.exe"},
{Target: targets[1], Filename: "app-v1.2.3-linux-amd64"},
}
if got := ExpectedBuild("app", "v1.2.3", targets); !reflect.DeepEqual(got, want) {
t.Fatalf("ExpectedBuild() = %#v, want %#v", got, want)
}
}
func TestValidComponentPreservesBuildAndReceiptRules(t *testing.T) {
for _, value := range []string{"", "path/name", `path\name`, "line\nbreak", "line\rbreak"} {
if ValidComponent(value) {
t.Errorf("ValidComponent(%q) = true, want false", value)
}
}
for _, value := range []string{"app", "..", "nul\x00byte"} {
if !ValidComponent(value) {
t.Errorf("ValidComponent(%q) = false, want true", value)
}
}
}