1// Copyright 2023 The Bazel Authors. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package manifest 16 17import ( 18 "crypto/sha256" 19 "fmt" 20 "io" 21 "os" 22 23 "github.com/emirpasic/gods/sets/treeset" 24 25 yaml "gopkg.in/yaml.v2" 26) 27 28// File represents the gazelle_python.yaml file. 29type File struct { 30 Manifest *Manifest `yaml:"manifest,omitempty"` 31 // Integrity is the hash of the requirements.txt file and the Manifest for 32 // ensuring the integrity of the entire gazelle_python.yaml file. This 33 // controls the testing to keep the gazelle_python.yaml file up-to-date. 34 Integrity string `yaml:"integrity,omitempty"` 35} 36 37// NewFile creates a new File with a given Manifest. 38func NewFile(manifest *Manifest) *File { 39 return &File{Manifest: manifest} 40} 41 42// Encode encodes the manifest file to the given writer. 43func (f *File) EncodeWithIntegrity(w io.Writer, manifestGeneratorHashFile, requirements io.Reader) error { 44 integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) 45 if err != nil { 46 return fmt.Errorf("failed to encode manifest file: %w", err) 47 } 48 f.Integrity = fmt.Sprintf("%x", integrityBytes) 49 50 return f.encode(w) 51} 52 53func (f *File) EncodeWithoutIntegrity(w io.Writer) error { 54 return f.encode(w) 55} 56 57func (f *File) encode(w io.Writer) error { 58 encoder := yaml.NewEncoder(w) 59 defer encoder.Close() 60 if err := encoder.Encode(f); err != nil { 61 return fmt.Errorf("failed to encode manifest file: %w", err) 62 } 63 return nil 64} 65 66// VerifyIntegrity verifies if the integrity set in the File is valid. 67func (f *File) VerifyIntegrity(manifestGeneratorHashFile, requirements io.Reader) (bool, error) { 68 integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) 69 if err != nil { 70 return false, fmt.Errorf("failed to verify integrity: %w", err) 71 } 72 valid := (f.Integrity == fmt.Sprintf("%x", integrityBytes)) 73 return valid, nil 74} 75 76// calculateIntegrity calculates the integrity of the manifest file based on the 77// provided checksum for the requirements.txt file used as input to the modules 78// mapping, plus the manifest structure in the manifest file. This integrity 79// calculation ensures the manifest files are kept up-to-date. 80func (f *File) calculateIntegrity( 81 manifestGeneratorHash, requirements io.Reader, 82) ([]byte, error) { 83 hash := sha256.New() 84 85 // Sum the manifest part of the file. 86 encoder := yaml.NewEncoder(hash) 87 defer encoder.Close() 88 if err := encoder.Encode(f.Manifest); err != nil { 89 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 90 } 91 92 // Sum the manifest generator checksum bytes. 93 if _, err := io.Copy(hash, manifestGeneratorHash); err != nil { 94 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 95 } 96 97 // Sum the requirements.txt checksum bytes. 98 if _, err := io.Copy(hash, requirements); err != nil { 99 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 100 } 101 102 return hash.Sum(nil), nil 103} 104 105// Decode decodes the manifest file from the given path. 106func (f *File) Decode(manifestPath string) error { 107 file, err := os.Open(manifestPath) 108 if err != nil { 109 return fmt.Errorf("failed to decode manifest file: %w", err) 110 } 111 defer file.Close() 112 113 decoder := yaml.NewDecoder(file) 114 if err := decoder.Decode(f); err != nil { 115 return fmt.Errorf("failed to decode manifest file: %w", err) 116 } 117 118 return nil 119} 120 121// ModulesMapping is the type used to map from importable Python modules to 122// the wheel names that provide these modules. 123type ModulesMapping map[string]string 124 125// MarshalYAML makes sure that we sort the module names before marshaling 126// the contents of `ModulesMapping` to a YAML file. This ensures that the 127// file is deterministically generated from the map. 128func (m ModulesMapping) MarshalYAML() (interface{}, error) { 129 var mapslice yaml.MapSlice 130 keySet := treeset.NewWithStringComparator() 131 for key := range m { 132 keySet.Add(key) 133 } 134 for _, key := range keySet.Values() { 135 mapslice = append(mapslice, yaml.MapItem{Key: key, Value: m[key.(string)]}) 136 } 137 return mapslice, nil 138} 139 140// Manifest represents the structure of the Gazelle manifest file. 141type Manifest struct { 142 // ModulesMapping is the mapping from importable modules to which Python 143 // wheel name provides these modules. 144 ModulesMapping ModulesMapping `yaml:"modules_mapping"` 145 // PipDepsRepositoryName is the name of the pip_parse repository target. 146 // DEPRECATED 147 PipDepsRepositoryName string `yaml:"pip_deps_repository_name,omitempty"` 148 // PipRepository contains the information for pip_parse or pip_repository 149 // target. 150 PipRepository *PipRepository `yaml:"pip_repository,omitempty"` 151} 152 153type PipRepository struct { 154 // The name of the pip_parse or pip_repository target. 155 Name string 156} 157