blob: f9b85dd51ea228219331e6c2c4f4e06e5bb8a40c [file] [log] [blame]
giob7df27f2026-07-28 10:36:17 +04001package e2e
2
3import (
4 "archive/tar"
5 "compress/gzip"
6 "context"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "time"
15)
16
17func extractBinary(archivePath, destinationDir, expectedName string) (string, error) {
18 f, err := os.Open(archivePath)
19 if err != nil {
20 return "", err
21 }
22 defer f.Close()
23 gz, err := gzip.NewReader(f)
24 if err != nil {
25 return "", fmt.Errorf("open gzip archive: %w", err)
26 }
27 defer gz.Close()
28
29 if err := os.MkdirAll(destinationDir, 0o755); err != nil {
30 return "", err
31 }
32 var found []byte
33 tr := tar.NewReader(gz)
34 for {
35 h, err := tr.Next()
36 if errors.Is(err, io.EOF) {
37 break
38 }
39 if err != nil {
40 return "", fmt.Errorf("read tar archive: %w", err)
41 }
42 clean := filepath.Clean(h.Name)
43 if filepath.IsAbs(h.Name) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
44 return "", fmt.Errorf("unsafe archive path %q", h.Name)
45 }
46 if filepath.Base(clean) != expectedName {
47 continue
48 }
49 if h.Typeflag != tar.TypeReg && h.Typeflag != tar.TypeRegA {
50 return "", fmt.Errorf("expected executable %q is not a regular file", h.Name)
51 }
52 if found != nil {
53 return "", fmt.Errorf("archive contains multiple %q executables", expectedName)
54 }
55 if h.Size < 1 || h.Size > 512<<20 {
56 return "", fmt.Errorf("invalid executable size %d", h.Size)
57 }
58 found, err = io.ReadAll(io.LimitReader(tr, h.Size+1))
59 if err != nil {
60 return "", err
61 }
62 if int64(len(found)) != h.Size {
63 return "", fmt.Errorf("truncated executable %q", h.Name)
64 }
65 }
66 if found == nil {
67 return "", fmt.Errorf("archive does not contain expected executable %q", expectedName)
68 }
69 destination := filepath.Join(destinationDir, expectedName)
70 if err := os.WriteFile(destination, found, 0o755); err != nil {
71 return "", err
72 }
73 if err := os.Chmod(destination, 0o755); err != nil {
74 return "", err
75 }
76 return destination, nil
77}
78
79func verifyBinaryVersion(path, expected string) error {
80 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
81 defer cancel()
82 out, err := exec.CommandContext(ctx, path, "version").CombinedOutput()
83 if ctx.Err() != nil {
84 return fmt.Errorf("verify %s version: %w", filepath.Base(path), ctx.Err())
85 }
86 if err != nil {
87 return fmt.Errorf("verify %s version: %w (%s)", filepath.Base(path), err, boundedText(out, 2048))
88 }
89 if !versionOutputMatches(out, expected) {
90 return fmt.Errorf("unexpected %s version: expected %s, got %s", filepath.Base(path), expected, boundedText(out, 2048))
91 }
92 return nil
93}
94
95func versionOutputMatches(output []byte, expected string) bool {
96 withoutV := strings.TrimPrefix(expected, "v")
97 for _, field := range strings.Fields(string(output)) {
98 field = strings.Trim(field, " \t\r\n,;:()[]{}")
99 if field == expected || field == withoutV {
100 return true
101 }
102 }
103 return false
104}