1// Copyright 2023 Google LLC 2// 3// Use of this source code is governed by a BSD-style license that can be 4// found in the LICENSE file. 5 6package common 7 8import ( 9 "archive/zip" 10 "context" 11 "fmt" 12 "io" 13 "os" 14 "path/filepath" 15 "regexp" 16 "strings" 17 18 "go.skia.org/infra/go/skerr" 19 "go.skia.org/infra/go/util" 20 "go.skia.org/infra/task_driver/go/lib/os_steps" 21 "go.skia.org/infra/task_driver/go/td" 22) 23 24// validBazelLabelRegexps represent valid, fully-qualified Bazel labels. 25var validBazelLabelRegexps = []*regexp.Regexp{ 26 regexp.MustCompile(`^//:[a-zA-Z0-9_-]+$`), // Matches "//:foo". 27 regexp.MustCompile(`^/(/[a-zA-Z0-9_-]+)+:[a-zA-Z0-9_-]+$`), // Matches "//foo:bar", "//foo/bar:baz", etc. 28} 29 30// ValidateLabelAndReturnOutputsZipPath validates the given Bazel label and returns the path within 31// the checkout directory where the ZIP archive with undeclared test outputs will be found, if 32// applicable. 33func ValidateLabelAndReturnOutputsZipPath(checkoutDir, label string) (string, error) { 34 valid := false 35 for _, re := range validBazelLabelRegexps { 36 if re.MatchString(label) { 37 valid = true 38 break 39 } 40 } 41 if !valid { 42 return "", skerr.Fmt("invalid label: %q", label) 43 } 44 45 return filepath.Join( 46 checkoutDir, 47 "bazel-testlogs", 48 strings.ReplaceAll(strings.TrimPrefix(label, "//"), ":", "/"), 49 "test.outputs", 50 "outputs.zip"), nil 51} 52 53// ExtractOutputsZip extracts the undeclared outputs ZIP archive into a temporary directory, and 54// returns the path to said directory. 55func ExtractOutputsZip(ctx context.Context, outputsZipPath string) (string, error) { 56 // Create extraction directory. 57 extractionDir, err := os_steps.TempDir(ctx, "", "bazel-test-output-dir-*") 58 if err != nil { 59 return "", skerr.Wrap(err) 60 } 61 62 // Extract ZIP archive. 63 if err := td.Do(ctx, td.Props(fmt.Sprintf("Extract undeclared outputs archive %s into %s", outputsZipPath, extractionDir)), func(ctx context.Context) error { 64 outputsZip, err := zip.OpenReader(outputsZipPath) 65 if err != nil { 66 return skerr.Wrap(err) 67 } 68 defer util.Close(outputsZip) 69 70 for _, file := range outputsZip.File { 71 // Skip directories. We assume all output files are at the root directory of the archive. 72 if file.FileInfo().IsDir() { 73 if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting subdirectory: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil { 74 return skerr.Wrap(err) 75 } 76 continue 77 } 78 79 // Skip files within subdirectories. 80 if strings.Contains(file.Name, "/") { 81 if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting file within subdirectory: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil { 82 return skerr.Wrap(err) 83 } 84 continue 85 } 86 87 // Ignore anything that is not a PNG or JSON file. 88 if !strings.HasSuffix(strings.ToLower(file.Name), ".png") && !strings.HasSuffix(strings.ToLower(file.Name), ".json") { 89 if err := td.Do(ctx, td.Props(fmt.Sprintf("Not extracting non-PNG / non-JSON file: %s", file.Name)), func(ctx context.Context) error { return nil }); err != nil { 90 return skerr.Wrap(err) 91 } 92 continue 93 } 94 95 // Extract file. 96 if err := td.Do(ctx, td.Props(fmt.Sprintf("Extracting file: %s", file.Name)), func(ctx context.Context) error { 97 reader, err := file.Open() 98 if err != nil { 99 return skerr.Wrap(err) 100 } 101 defer util.Close(reader) 102 103 bytes, err := io.ReadAll(reader) 104 if err != nil { 105 return skerr.Wrap(err) 106 } 107 return skerr.Wrap(os.WriteFile(filepath.Join(extractionDir, file.Name), bytes, 0644)) 108 }); err != nil { 109 return skerr.Wrap(err) 110 } 111 } 112 113 return nil 114 }); err != nil { 115 return "", skerr.Wrap(err) 116 } 117 118 return extractionDir, nil 119} 120