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 }