|  | // Copyright 2023 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package manifest | 
|  |  | 
|  | import ( | 
|  | "crypto/sha256" | 
|  | "fmt" | 
|  | "io" | 
|  | "os" | 
|  |  | 
|  | "github.com/emirpasic/gods/sets/treeset" | 
|  |  | 
|  | yaml "gopkg.in/yaml.v2" | 
|  | ) | 
|  |  | 
|  | // File represents the gazelle_python.yaml file. | 
|  | type File struct { | 
|  | Manifest *Manifest `yaml:"manifest,omitempty"` | 
|  | // Integrity is the hash of the requirements.txt file and the Manifest for | 
|  | // ensuring the integrity of the entire gazelle_python.yaml file. This | 
|  | // controls the testing to keep the gazelle_python.yaml file up-to-date. | 
|  | Integrity string `yaml:"integrity"` | 
|  | } | 
|  |  | 
|  | // NewFile creates a new File with a given Manifest. | 
|  | func NewFile(manifest *Manifest) *File { | 
|  | return &File{Manifest: manifest} | 
|  | } | 
|  |  | 
|  | // Encode encodes the manifest file to the given writer. | 
|  | func (f *File) Encode(w io.Writer, manifestGeneratorHashFile, requirements io.Reader) error { | 
|  | integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to encode manifest file: %w", err) | 
|  | } | 
|  | f.Integrity = fmt.Sprintf("%x", integrityBytes) | 
|  | encoder := yaml.NewEncoder(w) | 
|  | defer encoder.Close() | 
|  | if err := encoder.Encode(f); err != nil { | 
|  | return fmt.Errorf("failed to encode manifest file: %w", err) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // VerifyIntegrity verifies if the integrity set in the File is valid. | 
|  | func (f *File) VerifyIntegrity(manifestGeneratorHashFile, requirements io.Reader) (bool, error) { | 
|  | integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) | 
|  | if err != nil { | 
|  | return false, fmt.Errorf("failed to verify integrity: %w", err) | 
|  | } | 
|  | valid := (f.Integrity == fmt.Sprintf("%x", integrityBytes)) | 
|  | return valid, nil | 
|  | } | 
|  |  | 
|  | // calculateIntegrity calculates the integrity of the manifest file based on the | 
|  | // provided checksum for the requirements.txt file used as input to the modules | 
|  | // mapping, plus the manifest structure in the manifest file. This integrity | 
|  | // calculation ensures the manifest files are kept up-to-date. | 
|  | func (f *File) calculateIntegrity( | 
|  | manifestGeneratorHash, requirements io.Reader, | 
|  | ) ([]byte, error) { | 
|  | hash := sha256.New() | 
|  |  | 
|  | // Sum the manifest part of the file. | 
|  | encoder := yaml.NewEncoder(hash) | 
|  | defer encoder.Close() | 
|  | if err := encoder.Encode(f.Manifest); err != nil { | 
|  | return nil, fmt.Errorf("failed to calculate integrity: %w", err) | 
|  | } | 
|  |  | 
|  | // Sum the manifest generator checksum bytes. | 
|  | if _, err := io.Copy(hash, manifestGeneratorHash); err != nil { | 
|  | return nil, fmt.Errorf("failed to calculate integrity: %w", err) | 
|  | } | 
|  |  | 
|  | // Sum the requirements.txt checksum bytes. | 
|  | if _, err := io.Copy(hash, requirements); err != nil { | 
|  | return nil, fmt.Errorf("failed to calculate integrity: %w", err) | 
|  | } | 
|  |  | 
|  | return hash.Sum(nil), nil | 
|  | } | 
|  |  | 
|  | // Decode decodes the manifest file from the given path. | 
|  | func (f *File) Decode(manifestPath string) error { | 
|  | file, err := os.Open(manifestPath) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to decode manifest file: %w", err) | 
|  | } | 
|  | defer file.Close() | 
|  |  | 
|  | decoder := yaml.NewDecoder(file) | 
|  | if err := decoder.Decode(f); err != nil { | 
|  | return fmt.Errorf("failed to decode manifest file: %w", err) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // ModulesMapping is the type used to map from importable Python modules to | 
|  | // the wheel names that provide these modules. | 
|  | type ModulesMapping map[string]string | 
|  |  | 
|  | // MarshalYAML makes sure that we sort the module names before marshaling | 
|  | // the contents of `ModulesMapping` to a YAML file. This ensures that the | 
|  | // file is deterministically generated from the map. | 
|  | func (m ModulesMapping) MarshalYAML() (interface{}, error) { | 
|  | var mapslice yaml.MapSlice | 
|  | keySet := treeset.NewWithStringComparator() | 
|  | for key := range m { | 
|  | keySet.Add(key) | 
|  | } | 
|  | for _, key := range keySet.Values() { | 
|  | mapslice = append(mapslice, yaml.MapItem{Key: key, Value: m[key.(string)]}) | 
|  | } | 
|  | return mapslice, nil | 
|  | } | 
|  |  | 
|  | // Manifest represents the structure of the Gazelle manifest file. | 
|  | type Manifest struct { | 
|  | // ModulesMapping is the mapping from importable modules to which Python | 
|  | // wheel name provides these modules. | 
|  | ModulesMapping ModulesMapping `yaml:"modules_mapping"` | 
|  | // PipDepsRepositoryName is the name of the pip_install repository target. | 
|  | // DEPRECATED | 
|  | PipDepsRepositoryName string `yaml:"pip_deps_repository_name,omitempty"` | 
|  | // PipRepository contains the information for pip_install or pip_repository | 
|  | // target. | 
|  | PipRepository *PipRepository `yaml:"pip_repository,omitempty"` | 
|  | } | 
|  |  | 
|  | type PipRepository struct { | 
|  | // The name of the pip_install or pip_repository target. | 
|  | Name string | 
|  | // UsePipRepositoryAliases allows to use aliases generated pip_repository | 
|  | // when passing incompatible_generate_aliases = True. | 
|  | UsePipRepositoryAliases *bool `yaml:"use_pip_repository_aliases,omitempty"` | 
|  | } |