feat: backend native executor implement

This commit is contained in:
2026-08-16 01:27:30 +08:00
parent fbce91d797
commit 390e0565d3
44 changed files with 6430 additions and 31 deletions
+483
View File
@@ -0,0 +1,483 @@
// Package updatepackage reads the exact repack ZIP format currently emitted by deploy.
package updatepackage
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"yms-daemon/internal/filestore"
)
const (
manifestName = "artifact-selection.json"
backendArtifactKind = "BACKEND"
nativeArtifactType = "native"
maximumManifestBytes = 1 << 20
)
// BackendNativePackage is an opened repack ZIP containing one exact native backend artifact.
type BackendNativePackage struct {
archive *zip.ReadCloser
artifactEntry *zip.File
PackagePath string
PackageSHA256 string
CustomerCode string
VersionID string
ArtifactID int64
FileName string
Identity filestore.Identity
}
// OpenBackendNative validates the ZIP structure and selects one explicitly declared native backend artifact.
func OpenBackendNative(packagePath string) (*BackendNativePackage, error) {
if err := validateAbsoluteRegularFile(packagePath, "update package"); err != nil {
return nil, err
}
packageDigest, err := hashFile(packagePath)
if err != nil {
return nil, err
}
archive, err := zip.OpenReader(packagePath)
if err != nil {
return nil, fmt.Errorf("open update package ZIP %s: %w", packagePath, err)
}
closeOnError := func(cause error) (*BackendNativePackage, error) {
return nil, errors.Join(cause, archive.Close())
}
entries, err := validateEntries(archive.File)
if err != nil {
return closeOnError(err)
}
manifestEntry, found := entries[manifestName]
if !found {
return closeOnError(errors.New("update package is missing artifact-selection.json"))
}
manifest, err := decodeManifest(manifestEntry)
if err != nil {
return closeOnError(err)
}
selected, err := selectNativeBackend(manifest.BackendArtifacts)
if err != nil {
return closeOnError(err)
}
if err := validateRootFileName(selected.FileName); err != nil {
return closeOnError(fmt.Errorf("invalid backendArtifacts.fileName: %w", err))
}
artifactEntry, found := entries[selected.FileName]
if !found {
return closeOnError(fmt.Errorf("native backend artifact declared by backendArtifacts.fileName is missing: %s", selected.FileName))
}
if artifactEntry.FileInfo().IsDir() {
return closeOnError(fmt.Errorf("native backend artifact is not a file: %s", selected.FileName))
}
identity := filestore.Identity{
Size: int64(artifactEntry.UncompressedSize64),
SHA256: selected.SHA256,
}
if err := identity.Validate(); err != nil {
return closeOnError(fmt.Errorf("invalid backendArtifacts.sha256: %w", err))
}
return &BackendNativePackage{
archive: archive,
artifactEntry: artifactEntry,
PackagePath: packagePath,
PackageSHA256: packageDigest,
CustomerCode: manifest.CustomerCode,
VersionID: manifest.VersionID,
ArtifactID: selected.ID,
FileName: selected.FileName,
Identity: identity,
}, nil
}
// Close releases the opened ZIP file.
func (p *BackendNativePackage) Close() error {
if p == nil || p.archive == nil {
return nil
}
archive := p.archive
p.archive = nil
p.artifactEntry = nil
return archive.Close()
}
// ExtractArtifact writes the selected JAR to destination and verifies size and SHA-256 before publishing it.
func (p *BackendNativePackage) ExtractArtifact(destination string) error {
if p == nil || p.archive == nil || p.artifactEntry == nil {
return errors.New("native backend update package is not open")
}
if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
return errors.New("native backend extraction destination must be a clean absolute path")
}
parent := filepath.Dir(destination)
if err := os.MkdirAll(parent, 0o750); err != nil {
return fmt.Errorf("create native backend extraction directory: %w", err)
}
parentInfo, err := os.Lstat(parent)
if err != nil {
return fmt.Errorf("inspect native backend extraction directory: %w", err)
}
if !parentInfo.IsDir() || parentInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("native backend extraction parent is not a direct directory: %s", parent)
}
source, err := p.artifactEntry.Open()
if err != nil {
return fmt.Errorf("open native backend artifact %s: %w", p.FileName, err)
}
temporary, err := os.CreateTemp(parent, ".backend-jar-*")
if err != nil {
_ = source.Close()
return fmt.Errorf("create native backend extraction file: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(0o640); err != nil {
_ = source.Close()
_ = temporary.Close()
return fmt.Errorf("set native backend extraction permissions: %w", err)
}
digest := sha256.New()
written, copyErr := io.Copy(io.MultiWriter(temporary, digest), source)
closeSourceErr := source.Close()
if err := errors.Join(copyErr, closeSourceErr); err != nil {
_ = temporary.Close()
return fmt.Errorf("extract native backend artifact: %w", err)
}
if written != p.Identity.Size {
_ = temporary.Close()
return fmt.Errorf("native backend artifact size mismatch: got %d, want %d", written, p.Identity.Size)
}
actualDigest := hex.EncodeToString(digest.Sum(nil))
if !strings.EqualFold(actualDigest, p.Identity.SHA256) {
_ = temporary.Close()
return fmt.Errorf("native backend artifact SHA-256 mismatch: got %s, want %s", actualDigest, p.Identity.SHA256)
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return fmt.Errorf("flush native backend extraction file: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close native backend extraction file: %w", err)
}
if err := os.Rename(temporaryPath, destination); err != nil {
return fmt.Errorf("publish native backend extraction file: %w", err)
}
return syncDirectory(parent)
}
type artifactSelectionManifest struct {
CustomerCode string `json:"customerCode"`
CustomerDisplayName *string `json:"customerDisplayName"`
VersionID string `json:"versionId"`
Items []string `json:"items"`
BackendArtifacts []manifestArtifact `json:"backendArtifacts"`
FrontendArtifacts []manifestArtifact `json:"frontendArtifacts"`
NodeSSRArtifacts []manifestArtifact `json:"nodeSsrArtifacts"`
Remark *string `json:"remark"`
}
type manifestArtifact struct {
ID int64 `json:"id"`
VersionCode *string `json:"versionCode"`
ArtifactKind string `json:"artifactKind"`
Type string `json:"type"`
SelectedType string `json:"selectedType"`
Platform *string `json:"platform"`
FileName string `json:"fileName"`
FilePath *string `json:"filePath"`
SHA256 string `json:"sha256"`
ImageRef *string `json:"imageRef"`
}
func decodeManifest(entry *zip.File) (artifactSelectionManifest, error) {
if entry.FileInfo().IsDir() {
return artifactSelectionManifest{}, errors.New("artifact-selection.json is not a file")
}
if entry.UncompressedSize64 > maximumManifestBytes {
return artifactSelectionManifest{}, errors.New("artifact-selection.json exceeds size limit")
}
reader, err := entry.Open()
if err != nil {
return artifactSelectionManifest{}, fmt.Errorf("open artifact-selection.json: %w", err)
}
defer reader.Close()
document, err := io.ReadAll(io.LimitReader(reader, maximumManifestBytes+1))
if err != nil {
return artifactSelectionManifest{}, fmt.Errorf("read artifact-selection.json: %w", err)
}
if len(document) > maximumManifestBytes {
return artifactSelectionManifest{}, errors.New("artifact-selection.json exceeds size limit")
}
if err := validateExactManifestJSON(document); err != nil {
return artifactSelectionManifest{}, fmt.Errorf("validate artifact-selection.json keys: %w", err)
}
var manifest artifactSelectionManifest
decoder := json.NewDecoder(bytes.NewReader(document))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return artifactSelectionManifest{}, fmt.Errorf("decode artifact-selection.json: %w", err)
}
if err := ensureJSONEnd(decoder); err != nil {
return artifactSelectionManifest{}, err
}
if strings.TrimSpace(manifest.CustomerCode) == "" || strings.TrimSpace(manifest.VersionID) == "" {
return artifactSelectionManifest{}, errors.New("artifact-selection.json customerCode and versionId are required")
}
return manifest, nil
}
var rootManifestKeys = map[string]struct{}{
"customerCode": {},
"customerDisplayName": {},
"versionId": {},
"items": {},
"backendArtifacts": {},
"frontendArtifacts": {},
"nodeSsrArtifacts": {},
"remark": {},
}
var artifactManifestKeys = map[string]struct{}{
"id": {},
"versionCode": {},
"artifactKind": {},
"type": {},
"selectedType": {},
"platform": {},
"fileName": {},
"filePath": {},
"sha256": {},
"imageRef": {},
}
func validateExactManifestJSON(document []byte) error {
decoder := json.NewDecoder(bytes.NewReader(document))
decoder.UseNumber()
if err := validateObject(decoder, "", rootManifestKeys, true); err != nil {
return err
}
return ensureJSONEnd(decoder)
}
func validateObject(decoder *json.Decoder, objectPath string, allowed map[string]struct{}, root bool) error {
token, err := decoder.Token()
if err != nil {
return err
}
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
return fmt.Errorf("%s must be a JSON object", displayJSONPath(objectPath))
}
seen := make(map[string]struct{}, len(allowed))
for decoder.More() {
keyToken, err := decoder.Token()
if err != nil {
return err
}
key, ok := keyToken.(string)
if !ok {
return fmt.Errorf("%s contains a non-string key", displayJSONPath(objectPath))
}
fieldPath := key
if objectPath != "" {
fieldPath = objectPath + "." + key
}
if _, duplicate := seen[key]; duplicate {
return fmt.Errorf("duplicate JSON field %s", fieldPath)
}
seen[key] = struct{}{}
if _, known := allowed[key]; !known {
return fmt.Errorf("unknown JSON field %s", fieldPath)
}
if root && (key == "backendArtifacts" || key == "frontendArtifacts" || key == "nodeSsrArtifacts") {
if err := validateArtifactArray(decoder, fieldPath); err != nil {
return err
}
continue
}
if err := consumeJSONValue(decoder, fieldPath); err != nil {
return err
}
}
_, err = decoder.Token()
return err
}
func validateArtifactArray(decoder *json.Decoder, arrayPath string) error {
token, err := decoder.Token()
if err != nil {
return err
}
if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
return fmt.Errorf("%s must be a JSON array", arrayPath)
}
index := 0
for decoder.More() {
if err := validateObject(decoder, fmt.Sprintf("%s[%d]", arrayPath, index), artifactManifestKeys, false); err != nil {
return err
}
index++
}
_, err = decoder.Token()
return err
}
func consumeJSONValue(decoder *json.Decoder, valuePath string) error {
token, err := decoder.Token()
if err != nil {
return err
}
delimiter, composite := token.(json.Delim)
if !composite {
return nil
}
switch delimiter {
case '[':
for decoder.More() {
if err := consumeJSONValue(decoder, valuePath); err != nil {
return err
}
}
_, err = decoder.Token()
return err
case '{':
seen := make(map[string]struct{})
for decoder.More() {
keyToken, err := decoder.Token()
if err != nil {
return err
}
key := keyToken.(string)
if _, duplicate := seen[key]; duplicate {
return fmt.Errorf("duplicate JSON field %s.%s", valuePath, key)
}
seen[key] = struct{}{}
if err := consumeJSONValue(decoder, valuePath+"."+key); err != nil {
return err
}
}
_, err = decoder.Token()
return err
default:
return fmt.Errorf("unexpected JSON delimiter %q at %s", delimiter, valuePath)
}
}
func displayJSONPath(value string) string {
if value == "" {
return "artifact-selection.json"
}
return value
}
func ensureJSONEnd(decoder *json.Decoder) error {
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("artifact-selection.json contains multiple JSON values")
}
return fmt.Errorf("decode artifact-selection.json trailing content: %w", err)
}
return nil
}
func selectNativeBackend(artifacts []manifestArtifact) (manifestArtifact, error) {
var selected manifestArtifact
count := 0
for _, artifact := range artifacts {
if artifact.ArtifactKind == backendArtifactKind && artifact.Type == nativeArtifactType && artifact.SelectedType == nativeArtifactType {
selected = artifact
count++
}
}
if count != 1 {
return manifestArtifact{}, fmt.Errorf("backendArtifacts must contain exactly one BACKEND native selection, got %d", count)
}
if selected.ID <= 0 || selected.FileName == "" || selected.SHA256 == "" {
return manifestArtifact{}, errors.New("selected native backend artifact requires id, fileName and sha256")
}
return selected, nil
}
func validateEntries(files []*zip.File) (map[string]*zip.File, error) {
entries := make(map[string]*zip.File, len(files))
for _, file := range files {
name := file.Name
if name == "" || strings.Contains(name, "\\") || path.IsAbs(name) || path.Clean(name) != name || name == "." || strings.HasPrefix(name, "../") {
return nil, fmt.Errorf("update package contains unsafe ZIP entry: %q", name)
}
if file.Mode()&os.ModeSymlink != 0 {
return nil, fmt.Errorf("update package contains symbolic link entry: %s", name)
}
if _, duplicate := entries[name]; duplicate {
return nil, fmt.Errorf("update package contains duplicate ZIP entry: %s", name)
}
entries[name] = file
}
return entries, nil
}
func validateRootFileName(name string) error {
if name == "" || strings.ContainsAny(name, "/\\") || path.Clean(name) != name || name == "." || name == ".." {
return fmt.Errorf("expected one exact ZIP root file name, got %q", name)
}
return nil
}
func validateAbsoluteRegularFile(filePath string, description string) error {
if !filepath.IsAbs(filePath) || filepath.Clean(filePath) != filePath {
return fmt.Errorf("%s path must be a clean absolute path", description)
}
info, err := os.Lstat(filePath)
if err != nil {
return fmt.Errorf("inspect %s %s: %w", description, filePath, err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%s is not a direct regular file: %s", description, filePath)
}
return nil
}
func hashFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("open update package for SHA-256: %w", err)
}
digest := sha256.New()
_, copyErr := io.Copy(digest, file)
closeErr := file.Close()
if err := errors.Join(copyErr, closeErr); err != nil {
return "", fmt.Errorf("hash update package: %w", err)
}
return hex.EncodeToString(digest.Sum(nil)), nil
}
func syncDirectory(directory string) error {
file, err := os.Open(directory)
if err != nil {
return fmt.Errorf("open extraction directory for flush: %w", err)
}
syncErr := file.Sync()
closeErr := file.Close()
if err := errors.Join(syncErr, closeErr); err != nil {
return fmt.Errorf("flush extraction directory: %w", err)
}
return nil
}
@@ -0,0 +1,195 @@
package updatepackage
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestOpenAndExtractNativeBackend(t *testing.T) {
jar := []byte("native backend JAR")
packagePath := writeBackendPackage(t, jar, nil)
updatePackage, err := OpenBackendNative(packagePath)
if err != nil {
t.Fatalf("open native backend package: %v", err)
}
defer updatePackage.Close()
if updatePackage.CustomerCode != "customer-01" || updatePackage.VersionID != "V1.1.8" || updatePackage.ArtifactID != 42 {
t.Fatalf("unexpected package metadata: %+v", updatePackage)
}
if updatePackage.Identity.Size != int64(len(jar)) || updatePackage.PackageSHA256 == "" {
t.Fatalf("unexpected package identity: package=%s artifact=%+v", updatePackage.PackageSHA256, updatePackage.Identity)
}
destination := filepath.Join(t.TempDir(), "incoming", "backend.jar")
if err := updatePackage.ExtractArtifact(destination); err != nil {
t.Fatalf("extract native backend artifact: %v", err)
}
actual, err := os.ReadFile(destination)
if err != nil || !bytes.Equal(actual, jar) {
t.Fatalf("unexpected extracted artifact: content=%q err=%v", actual, err)
}
}
func TestOpenRejectsUnknownManifestField(t *testing.T) {
jar := []byte("native backend JAR")
packagePath := writeBackendPackage(t, jar, func(manifest map[string]any) {
manifest["VersionID"] = "wrong-case"
})
_, err := OpenBackendNative(packagePath)
if err == nil || !strings.Contains(err.Error(), "VersionID") {
t.Fatalf("expected unknown manifest field rejection, got %v", err)
}
}
func TestOpenRejectsMissingDeclaredArtifact(t *testing.T) {
jar := []byte("native backend JAR")
packagePath := writePackageEntries(t, map[string][]byte{
manifestName: manifestJSON(t, jar, func(manifest map[string]any) {
artifacts := manifest["backendArtifacts"].([]any)
artifacts[0].(map[string]any)["fileName"] = "missing.jar"
}),
})
_, err := OpenBackendNative(packagePath)
if err == nil || !strings.Contains(err.Error(), "missing.jar") {
t.Fatalf("expected missing artifact rejection, got %v", err)
}
}
func TestExtractRejectsArtifactDigestMismatch(t *testing.T) {
jar := []byte("native backend JAR")
packagePath := writeBackendPackage(t, jar, func(manifest map[string]any) {
artifacts := manifest["backendArtifacts"].([]any)
artifacts[0].(map[string]any)["sha256"] = strings.Repeat("0", 64)
})
updatePackage, err := OpenBackendNative(packagePath)
if err != nil {
t.Fatalf("open package before digest verification: %v", err)
}
defer updatePackage.Close()
destination := filepath.Join(t.TempDir(), "backend.jar")
if err := updatePackage.ExtractArtifact(destination); err == nil || !strings.Contains(err.Error(), "SHA-256 mismatch") {
t.Fatalf("expected artifact digest mismatch, got %v", err)
}
if _, err := os.Stat(destination); !os.IsNotExist(err) {
t.Fatalf("digest mismatch published artifact: %v", err)
}
}
func TestOpenRejectsUnsafeAndDuplicateEntries(t *testing.T) {
jar := []byte("native backend JAR")
manifest := manifestJSON(t, jar, nil)
t.Run("unsafe", func(t *testing.T) {
packagePath := writePackageEntries(t, map[string][]byte{
manifestName: manifest,
"backend.jar": jar,
"../escape": []byte("escape"),
})
if _, err := OpenBackendNative(packagePath); err == nil {
t.Fatal("expected unsafe ZIP entry rejection")
}
})
t.Run("duplicate", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "package.zip")
file, err := os.Create(path)
if err != nil {
t.Fatalf("create duplicate package: %v", err)
}
writer := zip.NewWriter(file)
for _, content := range [][]byte{manifest, manifest} {
entry, err := writer.Create(manifestName)
if err != nil {
t.Fatalf("create duplicate manifest entry: %v", err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write duplicate manifest entry: %v", err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("close duplicate package writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close duplicate package: %v", err)
}
if _, err := OpenBackendNative(path); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate ZIP entry rejection, got %v", err)
}
})
}
func writeBackendPackage(t *testing.T, jar []byte, modify func(map[string]any)) string {
t.Helper()
return writePackageEntries(t, map[string][]byte{
manifestName: manifestJSON(t, jar, modify),
"backend.jar": jar,
})
}
func manifestJSON(t *testing.T, jar []byte, modify func(map[string]any)) []byte {
t.Helper()
digest := sha256.Sum256(jar)
manifest := map[string]any{
"customerCode": "customer-01",
"customerDisplayName": "Customer 01",
"versionId": "V1.1.8",
"items": []string{"deploy-sync.sh"},
"backendArtifacts": []any{map[string]any{
"id": int64(42),
"versionCode": "V1.1.8",
"artifactKind": backendArtifactKind,
"type": nativeArtifactType,
"selectedType": nativeArtifactType,
"platform": nil,
"fileName": "backend.jar",
"filePath": "/archive/backend.jar",
"sha256": hex.EncodeToString(digest[:]),
"imageRef": nil,
}},
"frontendArtifacts": []any{},
"nodeSsrArtifacts": []any{},
"remark": nil,
}
if modify != nil {
modify(manifest)
}
content, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("encode manifest: %v", err)
}
return content
}
func writePackageEntries(t *testing.T, entries map[string][]byte) string {
t.Helper()
packagePath := filepath.Join(t.TempDir(), "package.zip")
file, err := os.Create(packagePath)
if err != nil {
t.Fatalf("create package: %v", err)
}
writer := zip.NewWriter(file)
for name, content := range entries {
entry, err := writer.Create(name)
if err != nil {
t.Fatalf("create package entry %s: %v", name, err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write package entry %s: %v", name, err)
}
}
if err := writer.Close(); err != nil {
t.Fatalf("close package writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close package: %v", err)
}
return packagePath
}
+152
View File
@@ -0,0 +1,152 @@
package updatepackage
import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"yms-daemon/internal/filestore"
)
// DirectNativeJAR is one JAR supplied directly by Jenkins or an operator.
// FileName is treated as opaque text; no version is parsed from it.
type DirectNativeJAR struct {
Path string
SHA256 string
FileName string
Identity filestore.Identity
}
// OpenDirectNativeJAR validates the direct file, verifies every ZIP entry and
// records the immutable identity used by the transaction.
func OpenDirectNativeJAR(jarPath string) (DirectNativeJAR, error) {
if err := validateAbsoluteRegularFile(jarPath, "native backend JAR"); err != nil {
return DirectNativeJAR{}, err
}
fileName := filepath.Base(jarPath)
if filepath.Ext(fileName) != ".jar" {
return DirectNativeJAR{}, fmt.Errorf("native backend JAR file name must end with .jar: %s", fileName)
}
if err := verifyJARArchive(jarPath); err != nil {
return DirectNativeJAR{}, err
}
identity, err := identifyFile(jarPath)
if err != nil {
return DirectNativeJAR{}, err
}
return DirectNativeJAR{
Path: jarPath,
SHA256: identity.SHA256,
FileName: fileName,
Identity: identity,
}, nil
}
// CopyArtifact copies the exact JAR into transaction storage and rejects a
// source file that changes after OpenDirectNativeJAR returns.
func (j DirectNativeJAR) CopyArtifact(destination string) error {
if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
return errors.New("native backend JAR destination must be a clean absolute path")
}
parent := filepath.Dir(destination)
if err := os.MkdirAll(parent, 0o750); err != nil {
return fmt.Errorf("create native backend JAR destination directory: %w", err)
}
parentInfo, err := os.Lstat(parent)
if err != nil {
return fmt.Errorf("inspect native backend JAR destination directory: %w", err)
}
if !parentInfo.IsDir() || parentInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("native backend JAR destination parent is not a direct directory: %s", parent)
}
source, err := os.Open(j.Path)
if err != nil {
return fmt.Errorf("open native backend JAR: %w", err)
}
temporary, err := os.CreateTemp(parent, ".backend-jar-*")
if err != nil {
_ = source.Close()
return fmt.Errorf("create native backend JAR transaction file: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(0o640); err != nil {
_ = source.Close()
_ = temporary.Close()
return fmt.Errorf("set native backend JAR transaction file permissions: %w", err)
}
digest := sha256.New()
written, copyErr := io.Copy(io.MultiWriter(temporary, digest), source)
closeSourceErr := source.Close()
if err := errors.Join(copyErr, closeSourceErr); err != nil {
_ = temporary.Close()
return fmt.Errorf("copy native backend JAR: %w", err)
}
actualDigest := hex.EncodeToString(digest.Sum(nil))
if written != j.Identity.Size || !strings.EqualFold(actualDigest, j.Identity.SHA256) {
_ = temporary.Close()
return errors.New("native backend JAR changed while entering the transaction")
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return fmt.Errorf("flush native backend JAR transaction file: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close native backend JAR transaction file: %w", err)
}
if err := verifyJARArchive(temporaryPath); err != nil {
return fmt.Errorf("verify copied native backend JAR: %w", err)
}
if err := os.Rename(temporaryPath, destination); err != nil {
return fmt.Errorf("publish native backend JAR transaction file: %w", err)
}
return syncDirectory(parent)
}
func verifyJARArchive(jarPath string) error {
archive, err := zip.OpenReader(jarPath)
if err != nil {
return fmt.Errorf("open native backend JAR %s: %w", jarPath, err)
}
defer archive.Close()
if len(archive.File) == 0 {
return errors.New("native backend JAR contains no ZIP entries")
}
for _, entry := range archive.File {
if entry.FileInfo().IsDir() {
continue
}
reader, err := entry.Open()
if err != nil {
return fmt.Errorf("open native backend JAR entry %s: %w", entry.Name, err)
}
_, readErr := io.Copy(io.Discard, reader)
closeErr := reader.Close()
if err := errors.Join(readErr, closeErr); err != nil {
return fmt.Errorf("verify native backend JAR entry %s: %w", entry.Name, err)
}
}
return nil
}
func identifyFile(filePath string) (filestore.Identity, error) {
file, err := os.Open(filePath)
if err != nil {
return filestore.Identity{}, fmt.Errorf("open native backend JAR for identity: %w", err)
}
digest := sha256.New()
size, copyErr := io.Copy(digest, file)
closeErr := file.Close()
if err := errors.Join(copyErr, closeErr); err != nil {
return filestore.Identity{}, fmt.Errorf("identify native backend JAR: %w", err)
}
return filestore.Identity{Size: size, SHA256: hex.EncodeToString(digest.Sum(nil))}, nil
}
@@ -0,0 +1,88 @@
package updatepackage
import (
"archive/zip"
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestOpenAndCopyDirectNativeJAR(t *testing.T) {
jarPath := writeDirectNativeJAR(t, "glory-soft-yms.jar", []byte("backend classes"))
jar, err := OpenDirectNativeJAR(jarPath)
if err != nil {
t.Fatalf("open direct native backend JAR: %v", err)
}
if jar.FileName != "glory-soft-yms.jar" || jar.SHA256 == "" || jar.Identity.Size <= 0 {
t.Fatalf("unexpected direct native backend JAR metadata: %+v", jar)
}
destination := filepath.Join(t.TempDir(), "transaction", "backend.jar")
if err := jar.CopyArtifact(destination); err != nil {
t.Fatalf("copy direct native backend JAR: %v", err)
}
expected, err := os.ReadFile(jarPath)
if err != nil {
t.Fatalf("read source JAR: %v", err)
}
actual, err := os.ReadFile(destination)
if err != nil || !bytes.Equal(actual, expected) {
t.Fatalf("unexpected copied JAR: err=%v", err)
}
}
func TestOpenDirectNativeJARRejectsInvalidArchive(t *testing.T) {
path := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
if err := os.WriteFile(path, []byte("not a JAR"), 0o600); err != nil {
t.Fatalf("write invalid JAR: %v", err)
}
if _, err := OpenDirectNativeJAR(path); err == nil {
t.Fatal("expected invalid direct JAR rejection")
}
}
func TestCopyDirectNativeJARRejectsChangedSource(t *testing.T) {
jarPath := writeDirectNativeJAR(t, "glory-soft-yms.jar", []byte("first"))
jar, err := OpenDirectNativeJAR(jarPath)
if err != nil {
t.Fatalf("open direct native backend JAR: %v", err)
}
replacement := writeDirectNativeJAR(t, "replacement.jar", []byte("second"))
content, err := os.ReadFile(replacement)
if err != nil {
t.Fatalf("read replacement JAR: %v", err)
}
if err := os.WriteFile(jarPath, content, 0o600); err != nil {
t.Fatalf("replace source JAR: %v", err)
}
err = jar.CopyArtifact(filepath.Join(t.TempDir(), "backend.jar"))
if err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("expected changed source rejection, got %v", err)
}
}
func writeDirectNativeJAR(t *testing.T, fileName string, content []byte) string {
t.Helper()
jarPath := filepath.Join(t.TempDir(), fileName)
file, err := os.Create(jarPath)
if err != nil {
t.Fatalf("create direct JAR: %v", err)
}
writer := zip.NewWriter(file)
entry, err := writer.Create("BOOT-INF/classes/application.properties")
if err != nil {
t.Fatalf("create direct JAR entry: %v", err)
}
if _, err := entry.Write(content); err != nil {
t.Fatalf("write direct JAR entry: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close direct JAR writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close direct JAR: %v", err)
}
return jarPath
}