package delivery import ( "crypto/sha256" "encoding/hex" "fmt" "io" "os" "path/filepath" "strings" internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" "gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" ) func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) { if len(result.Targets) == 0 { if len(result.Artifacts) != 0 { return nil, fmt.Errorf("source-only build result contains artifacts") } return nil, nil } if len(result.Artifacts) != len(result.Targets) { return nil, fmt.Errorf("build result must contain one artifact for each selected target") } wantByTarget := make(map[string]string, len(result.Targets)) for _, target := range result.Targets { if target.OS == "" || target.Arch == "" || strings.ContainsAny(target.OS+target.Arch, "/\\\r\n") { return nil, fmt.Errorf("build result contains an invalid target") } key := target.OS + "/" + target.Arch if _, exists := wantByTarget[key]; exists { return nil, fmt.Errorf("build result repeats target %q", key) } name := result.BinaryName + "-" + result.Version + "-" + target.OS + "-" + target.Arch if target.OS == "windows" { name += ".exe" } wantByTarget[key] = name } assets := make([]gitea.ExpectedAsset, 0, len(result.Artifacts)+1) seenTargets := make(map[string]bool, len(result.Artifacts)) for _, artifact := range result.Artifacts { key := artifact.Target.OS + "/" + artifact.Target.Arch wantName, ok := wantByTarget[key] if !ok || seenTargets[key] || artifact.Filename != wantName { return nil, fmt.Errorf("build artifact %q does not match a selected target and its declared name", artifact.Filename) } seenTargets[key] = true if filepath.Base(artifact.Path) != artifact.Filename { return nil, fmt.Errorf("build artifact path %q does not end with filename %q", artifact.Path, artifact.Filename) } info, err := os.Stat(artifact.Path) if err != nil { return nil, fmt.Errorf("inspect build artifact %q: %w", artifact.Filename, err) } if !info.Mode().IsRegular() { return nil, fmt.Errorf("build artifact %q is not a regular file", artifact.Filename) } file, err := os.Open(artifact.Path) if err != nil { return nil, fmt.Errorf("open build artifact %q: %w", artifact.Filename, err) } hash := sha256.New() _, copyErr := io.Copy(hash, file) closeErr := file.Close() if copyErr != nil { return nil, fmt.Errorf("hash build artifact %q: %w", artifact.Filename, copyErr) } if closeErr != nil { return nil, fmt.Errorf("close build artifact %q: %w", artifact.Filename, closeErr) } if artifact.SHA256 != hex.EncodeToString(hash.Sum(nil)) { return nil, fmt.Errorf("build artifact %q does not match its declared SHA-256 digest", artifact.Filename) } assets = append(assets, gitea.ExpectedAsset{ Filename: artifact.Filename, SHA256: artifact.SHA256, Size: info.Size(), Path: artifact.Path, }) } manifest, err := gitea.BuildManifest(assets) if err != nil { return nil, fmt.Errorf("build checksum manifest: %w", err) } if manifest.Filename != "" { assets = append(assets, manifest) } return assets, nil }