xref: /aosp_15_r20/external/bazelbuild-rules_python/gazelle/manifest/manifest.go (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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