xref: /aosp_15_r20/external/bazelbuild-rules_android/src/tools/ak/bucketize/partitioner.go (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
1// Copyright 2018 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 bucketize
16
17import (
18	"archive/zip"
19	"bytes"
20	"encoding/xml"
21	"fmt"
22	"io"
23	"os"
24	"path"
25	"path/filepath"
26	"sort"
27	"strings"
28
29	"src/common/golang/shard"
30	"src/common/golang/xml2"
31	"src/tools/ak/res/res"
32)
33
34// Helper struct to sort paths by index
35type indexedPaths struct {
36	order map[string]int
37	ps    []string
38}
39
40type byPathIndex indexedPaths
41
42func (b byPathIndex) Len() int      { return len(b.ps) }
43func (b byPathIndex) Swap(i, j int) { b.ps[i], b.ps[j] = b.ps[j], b.ps[i] }
44func (b byPathIndex) Less(i, j int) bool {
45	iIdx := pathIdx(b.ps[i], b.order)
46	jIdx := pathIdx(b.ps[j], b.order)
47	// Files exist in the same directory
48	if iIdx == jIdx {
49		return b.ps[i] < b.ps[j]
50	}
51	return iIdx < jIdx
52}
53
54// Helper struct to sort valuesKeys by index
55type indexedValuesKeys struct {
56	order map[string]int
57	ks    []valuesKey
58}
59
60type byValueKeyIndex indexedValuesKeys
61
62func (b byValueKeyIndex) Len() int      { return len(b.ks) }
63func (b byValueKeyIndex) Swap(i, j int) { b.ks[i], b.ks[j] = b.ks[j], b.ks[i] }
64func (b byValueKeyIndex) Less(i, j int) bool {
65	iIdx := pathIdx(b.ks[i].sourcePath.Path, b.order)
66	jIdx := pathIdx(b.ks[j].sourcePath.Path, b.order)
67	// Files exist in the same directory
68	if iIdx == jIdx {
69		return b.ks[i].sourcePath.Path < b.ks[j].sourcePath.Path
70	}
71	return iIdx < jIdx
72}
73
74type valuesKey struct {
75	sourcePath res.PathInfo
76	resType    res.Type
77}
78
79// PartitionSession consumes resources and partitions them into archives by the resource type.
80// The typewise partitions can be further sharded by the provided shardFn
81type PartitionSession struct {
82	typedOutput    map[res.Type][]*zip.Writer
83	sharder        shard.Func
84	collectedVals  map[valuesKey]map[string][]byte
85	collectedPaths map[string]res.PathInfo
86	collectedRAs   map[string][]xml.Attr
87	resourceOrder  map[string]int
88}
89
90// Partitioner takes the provided resource values and paths and stores the data sharded
91type Partitioner interface {
92	Close() error
93	CollectValues(vr *res.ValuesResource) error
94	CollectPathResource(src res.PathInfo)
95	CollectResourcesAttribute(attr *ResourcesAttribute)
96}
97
98// makePartitionSession creates a PartitionSession that writes to the given outputs.
99func makePartitionSession(outputs map[res.Type][]io.Writer, sharder shard.Func, resourceOrder map[string]int) (*PartitionSession, error) {
100	typeToArchs := make(map[res.Type][]*zip.Writer)
101	for t, ws := range outputs {
102		archs := make([]*zip.Writer, 0, len(ws))
103		for _, w := range ws {
104			archs = append(archs, zip.NewWriter(w))
105		}
106		typeToArchs[t] = archs
107	}
108	return &PartitionSession{
109		typeToArchs,
110		sharder,
111		make(map[valuesKey]map[string][]byte),
112		make(map[string]res.PathInfo),
113		make(map[string][]xml.Attr),
114		resourceOrder,
115	}, nil
116}
117
118// Close finalizes all archives in this partition session.
119func (ps *PartitionSession) Close() error {
120	if err := ps.flushCollectedPaths(); err != nil {
121		return fmt.Errorf("got error flushing collected paths: %v", err)
122	}
123	if err := ps.flushCollectedVals(); err != nil {
124		return fmt.Errorf("got error flushing collected values: %v", err)
125	}
126	// close archives.
127	for _, as := range ps.typedOutput {
128		for _, a := range as {
129			if err := a.Close(); err != nil {
130				return fmt.Errorf("%s: could not close: %v", a, err)
131			}
132		}
133	}
134	return nil
135}
136
137// CollectPathResource takes a file system resource and tracks it so that it can be stored in an output partition and shard.
138func (ps *PartitionSession) CollectPathResource(src res.PathInfo) {
139	// store the path only if the type is accepted by the underlying partitions.
140	if ps.isTypeAccepted(src.Type) {
141		ps.collectedPaths[src.Path] = src
142	}
143}
144
145// CollectValues stores the xml representation of a particular resource from a particular file.
146func (ps *PartitionSession) CollectValues(vr *res.ValuesResource) error {
147	// store the value only if the type is accepted by the underlying partitions.
148	if ps.isTypeAccepted(vr.N.Type) {
149		// Don't store style attr's from other packages
150		if !(vr.N.Type == res.Attr && vr.N.Package != "res-auto") {
151			k := valuesKey{*vr.Src, vr.N.Type}
152			if tv, ok := ps.collectedVals[k]; !ok {
153				ps.collectedVals[k] = make(map[string][]byte)
154				ps.collectedVals[k][vr.N.String()] = vr.Payload
155			} else {
156				if p, ok := tv[vr.N.String()]; !ok {
157					ps.collectedVals[k][vr.N.String()] = vr.Payload
158				} else if len(p) < len(vr.Payload) {
159					ps.collectedVals[k][vr.N.String()] = vr.Payload
160				} else if len(p) == len(vr.Payload) && bytes.Compare(p, vr.Payload) != 0 {
161					return fmt.Errorf("different values for resource %q", vr.N.String())
162				}
163			}
164		}
165	}
166	return nil
167}
168
169// CollectResourcesAttribute stores the xml attributes of the resources tag from a particular file.
170func (ps *PartitionSession) CollectResourcesAttribute(ra *ResourcesAttribute) {
171	ps.collectedRAs[ra.ResFile.Path] = append(ps.collectedRAs[ra.ResFile.Path], ra.Attribute)
172}
173
174func (ps *PartitionSession) isTypeAccepted(t res.Type) bool {
175	_, ok := ps.typedOutput[t]
176	return ok
177}
178
179func (ps *PartitionSession) flushCollectedPaths() error {
180	// sort keys so that data is written to the archives in a deterministic order
181	// specifically the same order in which they were declared
182	ks := make([]string, 0, len(ps.collectedPaths))
183	for k := range ps.collectedPaths {
184		ks = append(ks, k)
185	}
186	sort.Sort(byPathIndex(indexedPaths{order: ps.resourceOrder, ps: ks}))
187	for _, k := range ks {
188		v := ps.collectedPaths[k]
189		f, err := os.Open(v.Path)
190		if err != nil {
191			return fmt.Errorf("%s: could not be opened for reading: %v", v.Path, err)
192		}
193		if err := ps.storePathResource(v, f); err != nil {
194			return fmt.Errorf("%s: got error storing path resource: %v", v.Path, err)
195		}
196		f.Close()
197	}
198	return nil
199}
200
201func (ps *PartitionSession) storePathResource(src res.PathInfo, r io.Reader) error {
202	p := path.Base(src.Path)
203	if dot := strings.Index(p, "."); dot == 0 {
204		// skip files where the name starts with a ".", these are already ignored by aapt
205		return nil
206	} else if dot > 0 {
207		p = p[:dot]
208	}
209	fqn, err := res.ParseName(p, src.Type)
210	if err != nil {
211		return fmt.Errorf("%s: %q could not be parsed into a res name: %v", src.Path, p, err)
212	}
213	arch, err := ps.archiveFor(fqn)
214	if err != nil {
215		return fmt.Errorf("%s: could not get partitioned archive: %v", src.Path, err)
216	}
217	w, err := arch.Create(pathResSuffix(src.Path))
218	if err != nil {
219		return fmt.Errorf("%s: could not create writer: %v", src.Path, err)
220	}
221	if _, err = io.Copy(w, r); err != nil {
222		return fmt.Errorf("%s: could not copy into archive: %v", src.Path, err)
223	}
224	return nil
225}
226
227func (ps *PartitionSession) archiveFor(fqn res.FullyQualifiedName) (*zip.Writer, error) {
228	archs, ok := ps.typedOutput[fqn.Type]
229	if !ok {
230		return nil, fmt.Errorf("%s: do not have output stream for this res type", fqn.Type)
231	}
232	shard := ps.sharder(fqn.String(), len(archs))
233	if shard > len(archs) || 0 > shard {
234		return nil, fmt.Errorf("%v: bad sharder f(%v, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(archs), shard, len(archs))
235	}
236	return archs[shard], nil
237}
238
239var (
240	resXMLHeader = []byte("<?xml version='1.0' encoding='utf-8'?>")
241	resXMLFooter = []byte("</resources>")
242)
243
244func (ps *PartitionSession) flushCollectedVals() error {
245	// sort keys so that data is written to the archives in a deterministic order
246	// specifically the same order in which blaze provides them
247	ks := make([]valuesKey, 0, len(ps.collectedVals))
248	for k := range ps.collectedVals {
249		ks = append(ks, k)
250	}
251	sort.Sort(byValueKeyIndex(indexedValuesKeys{order: ps.resourceOrder, ks: ks}))
252	for _, k := range ks {
253		as, ok := ps.typedOutput[k.resType]
254		if !ok {
255			return fmt.Errorf("%s: no output for res type", k.resType)
256		}
257		ws := make([]io.Writer, 0, len(as))
258		// For each given source file, create a corresponding file in each of the shards. A file in a particular shard may be empty, if none of the resources defined in the source file ended up in that shard.
259		for _, a := range as {
260			w, err := a.Create(pathResSuffix(k.sourcePath.Path))
261			if err != nil {
262				return fmt.Errorf("%s: could not create entry: %v", k.sourcePath.Path, err)
263			}
264			if _, err = w.Write(resXMLHeader); err != nil {
265				return fmt.Errorf("%s: could not write xml header: %v", k.sourcePath.Path, err)
266			}
267			// Write the resources open tag, with the attributes collected.
268			b := bytes.Buffer{}
269			xml2.NewEncoder(&b).EncodeToken(xml.StartElement{
270				Name: res.ResourcesTagName,
271				Attr: ps.collectedRAs[k.sourcePath.Path],
272			})
273			if _, err = w.Write(b.Bytes()); err != nil {
274				return fmt.Errorf("%s: could not write resources tag %q: %v", k.sourcePath.Path, b.String(), err)
275			}
276			ws = append(ws, w)
277		}
278		v := ps.collectedVals[k]
279		var keys []string
280		for k := range v {
281			keys = append(keys, k)
282		}
283		sort.Strings(keys)
284		for _, fqn := range keys {
285			p := v[fqn]
286			shard := ps.sharder(fqn, len(ws))
287			if shard < 0 || shard >= len(ws) {
288				return fmt.Errorf("%v: bad sharder f(%s, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(ws), shard, len(ws))
289			}
290			if _, err := ws[shard].Write(p); err != nil {
291				return fmt.Errorf("%s: writing resource %s failed: %v", k.sourcePath.Path, fqn, err)
292			}
293		}
294		for _, w := range ws {
295			if _, err := w.Write(resXMLFooter); err != nil {
296				return fmt.Errorf("%s: could not write xml footer: %v", k.sourcePath.Path, err)
297			}
298		}
299	}
300	return nil
301}
302
303func pathIdx(path string, order map[string]int) int {
304	if idx, ok := order[path]; ok == true {
305		return idx
306	}
307	// TODO(mauriciogg): maybe replace with prefix search
308	// list of resources might contain directories so exact match might not exist
309	dirPos := strings.LastIndex(path, "/res/")
310	idx, _ := order[path[0:dirPos+4]]
311	return idx
312}
313
314func pathResSuffix(path string) string {
315	// returns the relative resource path from the full path
316	// e.g. /foo/bar/res/values/strings.xml -> res/values/strings.xml
317	parentDir := filepath.Dir(filepath.Dir(filepath.Dir(path)))
318	return strings.TrimPrefix(path, parentDir+string(filepath.Separator))
319}
320