1// Copyright 2020 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
15// Copies all the entries (APKs/APEXes) matching the target configuration from the given
16// APK set into a zip file. Run it without arguments to see usage details.
17package main
18
19import (
20	"flag"
21	"fmt"
22	"io"
23	"log"
24	"math"
25	"os"
26	"regexp"
27	"sort"
28	"strings"
29
30	"google.golang.org/protobuf/proto"
31
32	"android/soong/cmd/extract_apks/bundle_proto"
33	android_bundle_proto "android/soong/cmd/extract_apks/bundle_proto"
34	"android/soong/third_party/zip"
35)
36
37type TargetConfig struct {
38	sdkVersion int32
39	screenDpi  map[android_bundle_proto.ScreenDensity_DensityAlias]bool
40	// Map holding <ABI alias>:<its sequence number in the flag> info.
41	abis             map[android_bundle_proto.Abi_AbiAlias]int
42	allowPrereleased bool
43	stem             string
44	skipSdkCheck     bool
45}
46
47// An APK set is a zip archive. An entry 'toc.pb' describes its contents.
48// It is a protobuf message BuildApkResult.
49type Toc *android_bundle_proto.BuildApksResult
50
51type ApkSet struct {
52	path    string
53	reader  *zip.ReadCloser
54	entries map[string]*zip.File
55}
56
57func newApkSet(path string) (*ApkSet, error) {
58	apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)}
59	var err error
60	if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil {
61		return nil, err
62	}
63	for _, f := range apkSet.reader.File {
64		apkSet.entries[f.Name] = f
65	}
66	return apkSet, nil
67}
68
69func (apkSet *ApkSet) getToc() (Toc, error) {
70	var err error
71	tocFile, ok := apkSet.entries["toc.pb"]
72	if !ok {
73		return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path)
74	}
75	rc, err := tocFile.Open()
76	if err != nil {
77		return nil, err
78	}
79	bytes, err := io.ReadAll(rc)
80	if err != nil {
81		return nil, err
82	}
83	rc.Close()
84	buildApksResult := new(android_bundle_proto.BuildApksResult)
85	if err = proto.Unmarshal(bytes, buildApksResult); err != nil {
86		return nil, err
87	}
88	return buildApksResult, nil
89}
90
91func (apkSet *ApkSet) close() {
92	apkSet.reader.Close()
93}
94
95// Matchers for selection criteria
96
97type abiTargetingMatcher struct {
98	*android_bundle_proto.AbiTargeting
99}
100
101func (m abiTargetingMatcher) matches(config TargetConfig) bool {
102	if m.AbiTargeting == nil {
103		return true
104	}
105	if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
106		return true
107	}
108	// Find the one that appears first in the abis flags.
109	abiIdx := math.MaxInt32
110	for _, v := range m.GetValue() {
111		if i, ok := config.abis[v.Alias]; ok {
112			if i < abiIdx {
113				abiIdx = i
114			}
115		}
116	}
117	if abiIdx == math.MaxInt32 {
118		return false
119	}
120	// See if any alternatives appear before the above one.
121	for _, a := range m.GetAlternatives() {
122		if i, ok := config.abis[a.Alias]; ok {
123			if i < abiIdx {
124				// There is a better alternative. Skip this one.
125				return false
126			}
127		}
128	}
129	return true
130}
131
132type apkDescriptionMatcher struct {
133	*android_bundle_proto.ApkDescription
134}
135
136func (m apkDescriptionMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool {
137	return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config, allAbisMustMatch)
138}
139
140type apkTargetingMatcher struct {
141	*android_bundle_proto.ApkTargeting
142}
143
144func (m apkTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool {
145	return m.ApkTargeting == nil ||
146		(abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
147			languageTargetingMatcher{m.LanguageTargeting}.matches(config) &&
148			screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
149			sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
150			multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config, allAbisMustMatch))
151}
152
153type languageTargetingMatcher struct {
154	*android_bundle_proto.LanguageTargeting
155}
156
157func (m languageTargetingMatcher) matches(_ TargetConfig) bool {
158	if m.LanguageTargeting == nil {
159		return true
160	}
161	log.Fatal("language based entry selection is not implemented")
162	return false
163}
164
165type moduleMetadataMatcher struct {
166	*android_bundle_proto.ModuleMetadata
167}
168
169func (m moduleMetadataMatcher) matches(config TargetConfig) bool {
170	return m.ModuleMetadata == nil ||
171		(m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME &&
172			moduleTargetingMatcher{m.Targeting}.matches(config) &&
173			!m.IsInstant)
174}
175
176type moduleTargetingMatcher struct {
177	*android_bundle_proto.ModuleTargeting
178}
179
180func (m moduleTargetingMatcher) matches(config TargetConfig) bool {
181	return m.ModuleTargeting == nil ||
182		(sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
183			userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config))
184}
185
186// A higher number means a higher priority.
187// This order must be kept identical to bundletool's.
188var multiAbiPriorities = map[android_bundle_proto.Abi_AbiAlias]int{
189	android_bundle_proto.Abi_ARMEABI:     1,
190	android_bundle_proto.Abi_ARMEABI_V7A: 2,
191	android_bundle_proto.Abi_ARM64_V8A:   3,
192	android_bundle_proto.Abi_X86:         4,
193	android_bundle_proto.Abi_X86_64:      5,
194	android_bundle_proto.Abi_MIPS:        6,
195	android_bundle_proto.Abi_MIPS64:      7,
196}
197
198type multiAbiTargetingMatcher struct {
199	*android_bundle_proto.MultiAbiTargeting
200}
201
202type multiAbiValue []*bundle_proto.Abi
203
204func (m multiAbiValue) compare(other multiAbiValue) int {
205	min := func(a, b int) int {
206		if a < b {
207			return a
208		}
209		return b
210	}
211
212	sortAbis := func(abiSlice multiAbiValue) func(i, j int) bool {
213		return func(i, j int) bool {
214			// sort priorities greatest to least
215			return multiAbiPriorities[abiSlice[i].Alias] > multiAbiPriorities[abiSlice[j].Alias]
216		}
217	}
218
219	sortedM := append(multiAbiValue{}, m...)
220	sort.Slice(sortedM, sortAbis(sortedM))
221	sortedOther := append(multiAbiValue{}, other...)
222	sort.Slice(sortedOther, sortAbis(sortedOther))
223
224	for i := 0; i < min(len(sortedM), len(sortedOther)); i++ {
225		if multiAbiPriorities[sortedM[i].Alias] > multiAbiPriorities[sortedOther[i].Alias] {
226			return 1
227		}
228		if multiAbiPriorities[sortedM[i].Alias] < multiAbiPriorities[sortedOther[i].Alias] {
229			return -1
230		}
231	}
232
233	return len(sortedM) - len(sortedOther)
234}
235
236// this logic should match the logic in bundletool at
237// https://github.com/google/bundletool/blob/ae0fc0162fd80d92ef8f4ef4527c066f0106942f/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java#L43
238// (note link is the commit at time of writing; but logic should always match the latest)
239func (t multiAbiTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool {
240	if t.MultiAbiTargeting == nil {
241		return true
242	}
243	if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
244		return true
245	}
246
247	multiAbiIsValid := func(m multiAbiValue) bool {
248		numValid := 0
249		for _, abi := range m {
250			if _, ok := config.abis[abi.Alias]; ok {
251				numValid += 1
252			}
253		}
254		if numValid == 0 {
255			return false
256		} else if numValid > 0 && !allAbisMustMatch {
257			return true
258		} else {
259			return numValid == len(m)
260		}
261	}
262
263	// ensure that the current value is valid for our config
264	valueSetContainsViableAbi := false
265	multiAbiSet := t.GetValue()
266	for _, multiAbi := range multiAbiSet {
267		if multiAbiIsValid(multiAbi.GetAbi()) {
268			valueSetContainsViableAbi = true
269			break
270		}
271	}
272
273	if !valueSetContainsViableAbi {
274		return false
275	}
276
277	// See if there are any matching alternatives with a higher priority.
278	for _, altMultiAbi := range t.GetAlternatives() {
279		if !multiAbiIsValid(altMultiAbi.GetAbi()) {
280			continue
281		}
282
283		for _, multiAbi := range multiAbiSet {
284			valueAbis := multiAbiValue(multiAbi.GetAbi())
285			altAbis := multiAbiValue(altMultiAbi.GetAbi())
286			if valueAbis.compare(altAbis) < 0 {
287				// An alternative has a higher priority, don't use this one
288				return false
289			}
290		}
291	}
292
293	return true
294}
295
296type screenDensityTargetingMatcher struct {
297	*android_bundle_proto.ScreenDensityTargeting
298}
299
300func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool {
301	if m.ScreenDensityTargeting == nil {
302		return true
303	}
304	if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok {
305		return true
306	}
307	for _, v := range m.GetValue() {
308		switch x := v.GetDensityOneof().(type) {
309		case *android_bundle_proto.ScreenDensity_DensityAlias_:
310			if _, ok := config.screenDpi[x.DensityAlias]; ok {
311				return true
312			}
313		default:
314			log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented")
315		}
316	}
317	return false
318}
319
320type sdkVersionTargetingMatcher struct {
321	*android_bundle_proto.SdkVersionTargeting
322}
323
324func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool {
325	const preReleaseVersion = 10000
326	// TODO (b274518686) This check should only be used while SHA based targeting is active
327	// Once we have switched to an SDK version, this can be changed to throw an error if
328	// it was accidentally set
329	if config.skipSdkCheck == true {
330		return true
331	}
332	if m.SdkVersionTargeting == nil {
333		return true
334	}
335	if len(m.Value) > 1 {
336		log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value))
337	}
338	// Inspect only sdkVersionTargeting.Value.
339	// Even though one of the SdkVersionTargeting.Alternatives values may be
340	// better matching, we will select all of them
341	return m.Value[0].Min == nil ||
342		m.Value[0].Min.Value <= config.sdkVersion ||
343		(config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion)
344}
345
346type textureCompressionFormatTargetingMatcher struct {
347	*android_bundle_proto.TextureCompressionFormatTargeting
348}
349
350func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool {
351	if m.TextureCompressionFormatTargeting == nil {
352		return true
353	}
354	log.Fatal("texture based entry selection is not implemented")
355	return false
356}
357
358type userCountriesTargetingMatcher struct {
359	*android_bundle_proto.UserCountriesTargeting
360}
361
362func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool {
363	if m.UserCountriesTargeting == nil {
364		return true
365	}
366	log.Fatal("country based entry selection is not implemented")
367	return false
368}
369
370type variantTargetingMatcher struct {
371	*android_bundle_proto.VariantTargeting
372}
373
374func (m variantTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool {
375	if m.VariantTargeting == nil {
376		return true
377	}
378	return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
379		abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
380		multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config, allAbisMustMatch) &&
381		screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
382		textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config)
383}
384
385type SelectionResult struct {
386	moduleName string
387	entries    []string
388}
389
390// Return all entries matching target configuration
391func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult {
392	checkMatching := func(allAbisMustMatch bool) SelectionResult {
393		var result SelectionResult
394		for _, variant := range (*toc).GetVariant() {
395			if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig, allAbisMustMatch)) {
396				continue
397			}
398			for _, as := range variant.GetApkSet() {
399				if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) {
400					continue
401				}
402				for _, apkdesc := range as.GetApkDescription() {
403					if (apkDescriptionMatcher{apkdesc}).matches(targetConfig, allAbisMustMatch) {
404						result.entries = append(result.entries, apkdesc.GetPath())
405						// TODO(asmundak): As it turns out, moduleName which we get from
406						// the ModuleMetadata matches the module names of the generated
407						// entry paths just by coincidence, only for the split APKs. We
408						// need to discuss this with bundletool folks.
409						result.moduleName = as.GetModuleMetadata().GetName()
410					}
411				}
412				// we allow only a single module, so bail out here if we found one
413				if result.moduleName != "" {
414					return result
415				}
416			}
417		}
418		return result
419	}
420	result := checkMatching(true)
421	if result.moduleName == "" {
422		// if there are no matches where all of the ABIs are available in the
423		// TargetConfig, then search again with a looser requirement of at
424		// least one matching ABI
425		// NOTE(b/260130686): this logic diverges from the logic in bundletool
426		// https://github.com/google/bundletool/blob/ae0fc0162fd80d92ef8f4ef4527c066f0106942f/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java#L43
427		result = checkMatching(false)
428	}
429	return result
430}
431
432type Zip2ZipWriter interface {
433	CopyFrom(file *zip.File, name string) error
434}
435
436// Writes out selected entries, renaming them as needed
437func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig,
438	outFile io.Writer, zipWriter Zip2ZipWriter, partition string) ([]string, error) {
439	// Renaming rules:
440	//  splits/MODULE-master.apk to STEM.apk
441	// else
442	//  splits/MODULE-*.apk to STEM>-$1.apk
443	// TODO(asmundak):
444	//  add more rules, for .apex files
445	renameRules := []struct {
446		rex  *regexp.Regexp
447		repl string
448	}{
449		{
450			regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`),
451			config.stem + `.apk`,
452		},
453		{
454			regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`),
455			config.stem + `$1`,
456		},
457		{
458			regexp.MustCompile(`^universal\.apk$`),
459			config.stem + ".apk",
460		},
461	}
462	renamer := func(path string) (string, bool) {
463		for _, rr := range renameRules {
464			if rr.rex.MatchString(path) {
465				return rr.rex.ReplaceAllString(path, rr.repl), true
466			}
467		}
468		return "", false
469	}
470
471	entryOrigin := make(map[string]string) // output entry to input entry
472	var apkcerts []string
473	for _, apk := range selected.entries {
474		apkFile, ok := apkSet.entries[apk]
475		if !ok {
476			return nil, fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
477		}
478		inName := apkFile.Name
479		outName, ok := renamer(inName)
480		if !ok {
481			log.Fatalf("selected an entry with unexpected name %s", inName)
482		}
483		if origin, ok := entryOrigin[inName]; ok {
484			log.Fatalf("selected entries %s and %s will have the same output name %s",
485				origin, inName, outName)
486		}
487		entryOrigin[outName] = inName
488		if outName == config.stem+".apk" {
489			if err := writeZipEntryToFile(outFile, apkFile); err != nil {
490				return nil, err
491			}
492		} else {
493			if err := zipWriter.CopyFrom(apkFile, outName); err != nil {
494				return nil, err
495			}
496		}
497		if partition != "" {
498			apkcerts = append(apkcerts, fmt.Sprintf(
499				`name="%s" certificate="PRESIGNED" private_key="" partition="%s"`, outName, partition))
500		}
501	}
502	sort.Strings(apkcerts)
503	return apkcerts, nil
504}
505
506func (apkSet *ApkSet) extractAndCopySingle(selected SelectionResult, outFile *os.File) error {
507	if len(selected.entries) != 1 {
508		return fmt.Errorf("Too many matching entries for extract-single:\n%v", selected.entries)
509	}
510	apk, ok := apkSet.entries[selected.entries[0]]
511	if !ok {
512		return fmt.Errorf("Couldn't find apk path %s", selected.entries[0])
513	}
514	return writeZipEntryToFile(outFile, apk)
515}
516
517// Arguments parsing
518var (
519	outputFile   = flag.String("o", "", "output file for primary entry")
520	zipFile      = flag.String("zip", "", "output file containing additional extracted entries")
521	targetConfig = TargetConfig{
522		screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
523		abis:      map[android_bundle_proto.Abi_AbiAlias]int{},
524	}
525	extractSingle = flag.Bool("extract-single", false,
526		"extract a single target and output it uncompressed. only available for standalone apks and apexes.")
527	apkcertsOutput = flag.String("apkcerts", "",
528		"optional apkcerts.txt output file containing signing info of all outputted apks")
529	partition = flag.String("partition", "", "partition string. required when -apkcerts is used.")
530)
531
532// Parse abi values
533type abiFlagValue struct {
534	targetConfig *TargetConfig
535}
536
537func (a abiFlagValue) String() string {
538	return "all"
539}
540
541func (a abiFlagValue) Set(abiList string) error {
542	for i, abi := range strings.Split(abiList, ",") {
543		v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
544		if !ok {
545			return fmt.Errorf("bad ABI value: %q", abi)
546		}
547		targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = i
548	}
549	return nil
550}
551
552// Parse screen density values
553type screenDensityFlagValue struct {
554	targetConfig *TargetConfig
555}
556
557func (s screenDensityFlagValue) String() string {
558	return "none"
559}
560
561func (s screenDensityFlagValue) Set(densityList string) error {
562	if densityList == "none" {
563		return nil
564	}
565	if densityList == "all" {
566		targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
567		return nil
568	}
569	for _, density := range strings.Split(densityList, ",") {
570		v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
571		if !found {
572			return fmt.Errorf("bad screen density value: %q", density)
573		}
574		targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
575	}
576	return nil
577}
578
579func processArgs() {
580	flag.Usage = func() {
581		fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-file> [-zip <output-zip-file>] `+
582			`-sdk-version value -abis value [-skip-sdk-check]`+
583			`-screen-densities value {-stem value | -extract-single} [-allow-prereleased] `+
584			`[-apkcerts <apkcerts output file> -partition <partition>] <APK set>`)
585		flag.PrintDefaults()
586		os.Exit(2)
587	}
588	version := flag.Uint("sdk-version", 0, "SDK version")
589	flag.Var(abiFlagValue{&targetConfig}, "abis",
590		"comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
591	flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
592		"'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
593	flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
594		"allow prereleased")
595	flag.BoolVar(&targetConfig.skipSdkCheck, "skip-sdk-check", false, "Skip the SDK version check")
596	flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name in the output zip file")
597	flag.Parse()
598	if (*outputFile == "") || len(flag.Args()) != 1 || *version == 0 ||
599		((targetConfig.stem == "" || *zipFile == "") && !*extractSingle) ||
600		(*apkcertsOutput != "" && *partition == "") {
601		flag.Usage()
602	}
603	targetConfig.sdkVersion = int32(*version)
604
605}
606
607func main() {
608	processArgs()
609	var toc Toc
610	apkSet, err := newApkSet(flag.Arg(0))
611	if err == nil {
612		defer apkSet.close()
613		toc, err = apkSet.getToc()
614	}
615	if err != nil {
616		log.Fatal(err)
617	}
618	sel := selectApks(toc, targetConfig)
619	if len(sel.entries) == 0 {
620		log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
621	}
622
623	outFile, err := os.Create(*outputFile)
624	if err != nil {
625		log.Fatal(err)
626	}
627	defer outFile.Close()
628
629	if *extractSingle {
630		err = apkSet.extractAndCopySingle(sel, outFile)
631	} else {
632		zipOutputFile, err := os.Create(*zipFile)
633		if err != nil {
634			log.Fatal(err)
635		}
636		defer zipOutputFile.Close()
637
638		zipWriter := zip.NewWriter(zipOutputFile)
639		defer func() {
640			if err := zipWriter.Close(); err != nil {
641				log.Fatal(err)
642			}
643		}()
644
645		apkcerts, err := apkSet.writeApks(sel, targetConfig, outFile, zipWriter, *partition)
646		if err == nil && *apkcertsOutput != "" {
647			apkcertsFile, err := os.Create(*apkcertsOutput)
648			if err != nil {
649				log.Fatal(err)
650			}
651			defer apkcertsFile.Close()
652			for _, a := range apkcerts {
653				_, err = apkcertsFile.WriteString(a + "\n")
654				if err != nil {
655					log.Fatal(err)
656				}
657			}
658		}
659	}
660	if err != nil {
661		log.Fatal(err)
662	}
663}
664
665func writeZipEntryToFile(outFile io.Writer, zipEntry *zip.File) error {
666	reader, err := zipEntry.Open()
667	if err != nil {
668		return err
669	}
670	defer reader.Close()
671	_, err = io.Copy(outFile, reader)
672	return err
673}
674