• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2024 Google Inc. 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 find_input_delta_lib
16
17import (
18	"errors"
19	"fmt"
20	"io/fs"
21	"regexp"
22	"slices"
23
24	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto_internal"
25	"android/soong/third_party/zip"
26	"github.com/google/blueprint/pathtools"
27	"google.golang.org/protobuf/proto"
28)
29
30// Load the internal state from a file.
31// If the file does not exist, an empty state is returned.
32func LoadState(filename string, fsys fs.ReadFileFS) (*fid_proto.PartialCompileInputs, error) {
33	var message = &fid_proto.PartialCompileInputs{}
34	data, err := fsys.ReadFile(filename)
35	if err != nil && !errors.Is(err, fs.ErrNotExist) {
36		return message, err
37	}
38	proto.Unmarshal(data, message)
39	return message, nil
40}
41
42type StatReadFileFS interface {
43	fs.StatFS
44	fs.ReadFileFS
45}
46
47// Create the internal state by examining the inputs.
48func CreateState(inputs []string, inspect_contents bool, fsys StatReadFileFS) (*fid_proto.PartialCompileInputs, error) {
49	ret := &fid_proto.PartialCompileInputs{}
50	slices.Sort(inputs)
51	for _, input := range inputs {
52		stat, err := fs.Stat(fsys, input)
53		if err != nil {
54			if errors.Is(err, fs.ErrNotExist) {
55				continue
56			}
57			return ret, err
58		}
59		pci := &fid_proto.PartialCompileInput{
60			Name:      proto.String(input),
61			MtimeNsec: proto.Int64(stat.ModTime().UnixNano()),
62			// If we ever have an easy hash, assign it here.
63		}
64		if inspect_contents {
65			// NOTE: When we find it useful, we can parallelize the file inspection for speed.
66			contents, err := InspectFileContents(input)
67			if err != nil {
68				return ret, err
69			}
70			if contents != nil {
71				pci.Contents = contents
72			}
73		}
74		ret.InputFiles = append(ret.InputFiles, pci)
75	}
76	return ret, nil
77}
78
79// We ignore any suffix digit caused by sharding.
80var InspectExtsZipRegexp = regexp.MustCompile("\\.(jar|apex|apk)[0-9]*$")
81
82// Inspect the file and extract the state of the elements in the archive.
83// If this is not an archive of some sort, nil is returned.
84func InspectFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
85	if InspectExtsZipRegexp.Match([]byte(name)) {
86		return inspectZipFileContents(name)
87	}
88	return nil, nil
89}
90
91func inspectZipFileContents(name string) ([]*fid_proto.PartialCompileInput, error) {
92	rc, err := zip.OpenReader(name)
93	if err != nil {
94		return nil, err
95	}
96	ret := []*fid_proto.PartialCompileInput{}
97	for _, v := range rc.File {
98		// Only include timestamp when there is no CRC.
99		timeNsec := proto.Int64(v.ModTime().UnixNano())
100		if v.CRC32 != 0 {
101			timeNsec = nil
102		}
103		pci := &fid_proto.PartialCompileInput{
104			Name:      proto.String(v.Name),
105			MtimeNsec: timeNsec,
106			Hash:      proto.String(fmt.Sprintf("%08x", v.CRC32)),
107		}
108		ret = append(ret, pci)
109		// We do not support nested inspection.
110	}
111	return ret, nil
112}
113
114func WriteState(s *fid_proto.PartialCompileInputs, path string) error {
115	data, err := proto.Marshal(s)
116	if err != nil {
117		return err
118	}
119	return pathtools.WriteFileIfChanged(path, data, 0644)
120}
121
122func CompareInternalState(prior, other *fid_proto.PartialCompileInputs, target string) *FileList {
123	return CompareInputFiles(prior.GetInputFiles(), other.GetInputFiles(), target)
124}
125
126func CompareInputFiles(prior, other []*fid_proto.PartialCompileInput, name string) *FileList {
127	fl := FileListFactory(name)
128	PriorMap := make(map[string]*fid_proto.PartialCompileInput, len(prior))
129	// We know that the lists are properly sorted, so we can simply compare them.
130	for _, v := range prior {
131		PriorMap[v.GetName()] = v
132	}
133	otherMap := make(map[string]*fid_proto.PartialCompileInput, len(other))
134	for _, v := range other {
135		name = v.GetName()
136		otherMap[name] = v
137		if _, ok := PriorMap[name]; !ok {
138			// Added file
139			fl.addFile(name)
140		} else if !proto.Equal(PriorMap[name], v) {
141			// Changed file
142			fl.changeFile(name, CompareInputFiles(PriorMap[name].GetContents(), v.GetContents(), name))
143		}
144	}
145	for _, v := range prior {
146		name := v.GetName()
147		if _, ok := otherMap[name]; !ok {
148			// Deleted file
149			fl.deleteFile(name)
150		}
151	}
152	return fl
153}
154