blob: 9ab50d852145131b09022a12643cf72dfe2c20c9 [file]
// Copyright 2018 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 liteparse does a light parsing of android resources files that can be used at a later
// stage to generate R.java files.
package liteparse
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"sync"
"src/common/golang/flags"
"src/common/golang/walk"
rdpb "src/tools/ak/res/proto/res_data_go_proto"
"src/tools/ak/res/res"
"src/tools/ak/res/respipe/respipe"
"src/tools/ak/res/resxml/resxml"
"src/tools/ak/types"
"google.golang.org/protobuf/proto"
)
var (
// Cmd defines the command to run the res parser.
Cmd = types.Command{
Init: Init,
Run: Run,
Desc: desc,
Flags: []string{"resourceFiles", "rPbOutput"},
}
resourceFiles flags.StringList
rPbOutput string
pkg string
initOnce sync.Once
)
const (
numParsers = 25
)
// Init initializes parse. Flags here need to match flags in AndroidResourceParsingAction.
func Init() {
initOnce.Do(func() {
flag.Var(&resourceFiles, "res_files", "Resource files and asset directories to parse.")
flag.StringVar(&rPbOutput, "out", "", "Path to the output proto file.")
flag.StringVar(&pkg, "pkg", "", "Java package name.")
})
}
func desc() string {
return "Lite parses the resource files to generate an R.pb."
}
// Run runs the parser.
func Run() {
rscs := ParseAll(context.Background(), resourceFiles, pkg)
b, err := proto.Marshal(rscs)
if err != nil {
log.Fatal(err)
}
if err = ioutil.WriteFile(rPbOutput, b, 0644); err != nil {
log.Fatal(err)
}
}
type resourceFile struct {
pathInfo *res.PathInfo
contents []byte
}
// ParseAll parses all the files in resPaths, which can contain both files and directories,
// and returns pb.
func ParseAll(ctx context.Context, resPaths []string, packageName string) *rdpb.Resources {
resFiles, err := walk.Files(resPaths)
if err != nil {
log.Fatal(err)
}
pifs, rscs, err := initializeFileParse(resFiles, packageName)
if err != nil {
log.Fatal(err)
}
if len(pifs) == 0 {
return rscs
}
piC := make(chan *res.PathInfo, len(pifs))
for _, pi := range pifs {
piC <- pi
}
close(piC)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resC, errC := ResParse(ctx, piC)
rscs.Resource, err = processResAndErr(resC, errC)
if err != nil {
cancel()
log.Fatal(err)
}
return rscs
}
// ResParse consumes a stream of resource paths and converts them into resource protos. These
// protos will only have the minimal name/type info set.
func ResParse(ctx context.Context, piC <-chan *res.PathInfo) (<-chan *rdpb.Resource, <-chan error) {
parserC := make(chan *res.PathInfo)
var parsedResCs []<-chan *rdpb.Resource
var parsedErrCs []<-chan error
for i := 0; i < numParsers; i++ {
parsedResC, parsedErrC := xmlParser(ctx, parserC)
parsedResCs = append(parsedResCs, parsedResC)
parsedErrCs = append(parsedErrCs, parsedErrC)
}
pathResC := make(chan *rdpb.Resource)
pathErrC := make(chan error)
go func() {
defer close(pathResC)
defer close(pathErrC)
defer close(parserC)
for pi := range piC {
np, err := needsParse(pi)
if err != nil {
pathErrC <- err
return
} else if np {
parserC <- pi
}
if !parsePathInfo(ctx, pi, pathResC, pathErrC) {
return
}
}
}()
parsedResCs = append(parsedResCs, pathResC)
parsedErrCs = append(parsedErrCs, pathErrC)
resC := respipe.MergeResStreams(ctx, parsedResCs)
errC := respipe.MergeErrStreams(ctx, parsedErrCs)
return resC, errC
}
// ParseAllContents parses all resource files with paths and contents and returns pb representing
// the R class that is generated from the files with the package packageName.
// paths and contents must have the same length, and a file with paths[i] file path
// has file contents contents[i].
func ParseAllContents(ctx context.Context, paths []string, contents [][]byte, packageName string) (*rdpb.Resources, error) {
if len(paths) != len(contents) {
return nil, fmt.Errorf("length of paths (%v) and contents (%v) are not equal", len(paths), len(contents))
}
pifs, rscs, err := initializeFileParse(paths, packageName)
if err != nil {
return nil, err
}
if len(pifs) == 0 {
return rscs, nil
}
var rfC []*resourceFile
for i, pi := range pifs {
rfC = append(rfC, &resourceFile{
pathInfo: pi,
contents: contents[i],
})
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resC, errC := resParseContents(ctx, rfC)
rscs.Resource, err = processResAndErr(resC, errC)
if err != nil {
return nil, err
}
return rscs, nil
}
// resParseContents consumes resource files and converts them into resource protos.
// These protos will only have the minimal name/type info set.
// The returned channels will be consumed by processRessAndErr.
func resParseContents(ctx context.Context, rfC []*resourceFile) (<-chan *rdpb.Resource, <-chan error) {
parserC := make(chan *resourceFile)
var parsedResCs []<-chan *rdpb.Resource
var parsedErrCs []<-chan error
for i := 0; i < numParsers; i++ {
parsedResC, parsedErrC := xmlParserContents(ctx, parserC)
parsedResCs = append(parsedResCs, parsedResC)
parsedErrCs = append(parsedErrCs, parsedErrC)
}
pathResC := make(chan *rdpb.Resource)
pathErrC := make(chan error)
go func() {
defer close(pathResC)
defer close(pathErrC)
defer close(parserC)
for _, rf := range rfC {
if needsParseContents(rf.pathInfo, bytes.NewReader(rf.contents)) {
parserC <- rf
}
if !parsePathInfo(ctx, rf.pathInfo, pathResC, pathErrC) {
return
}
}
}()
parsedResCs = append(parsedResCs, pathResC)
parsedErrCs = append(parsedErrCs, pathErrC)
resC := respipe.MergeResStreams(ctx, parsedResCs)
errC := respipe.MergeErrStreams(ctx, parsedErrCs)
return resC, errC
}
// initializeFileParse returns a slice of all PathInfos of files contained in each file path,
// which must be a file (not a directory). It also returns Resources with packageName.
func initializeFileParse(filePaths []string, packageName string) ([]*res.PathInfo, *rdpb.Resources, error) {
rscs := &rdpb.Resources{
Pkg: packageName,
}
pifs, err := res.MakePathInfos(filePaths)
if err != nil {
return nil, nil, err
}
return pifs, rscs, nil
}
// parsePathInfo attempts to parse the PathInfo and send the provided Resource and error to the
// provided chan. If the context is canceled, returns false, and otherwise, returns true.
func parsePathInfo(ctx context.Context, pi *res.PathInfo, pathResC chan<- *rdpb.Resource, pathErrC chan<- error) bool {
if rawName, ok := pathAsRes(pi); ok {
fqn, err := res.ParseName(rawName, pi.Type)
if err != nil {
return respipe.SendErr(ctx, pathErrC, respipe.Errorf(ctx, "%s: name parse failed: %v", pi.Path, err))
}
r := new(rdpb.Resource)
if err := fqn.SetResource(r); err != nil {
return respipe.SendErr(ctx, pathErrC, respipe.Errorf(ctx, "%s: name->proto failed: %v", fqn, err))
}
return respipe.SendRes(ctx, pathResC, r)
}
return true
}
// processResAndErr processes the res and err channels and returns the resources if successful
// or the first encountered error.
func processResAndErr(resC <-chan *rdpb.Resource, errC <-chan error) ([]*rdpb.Resource, error) {
parseErrChan := make(chan error, 1)
go func() {
for err := range errC {
if err != nil {
parseErrChan <- err
return
}
}
}()
doneChan := make(chan struct{}, 1)
var res []*rdpb.Resource
go func() {
for r := range resC {
res = append(res, r)
}
doneChan <- struct{}{}
}()
select {
case err := <-parseErrChan:
return nil, err
case <-doneChan:
}
return res, nil
}
// xmlParser consumes a stream of paths that need to have their xml contents parsed into resource
// protos. We only need to get names and types - so the parsing is very quick.
func xmlParser(ctx context.Context, piC <-chan *res.PathInfo) (<-chan *rdpb.Resource, <-chan error) {
resC := make(chan *rdpb.Resource)
errC := make(chan error)
go func() {
defer close(resC)
defer close(errC)
for p := range piC {
if !syncParse(respipe.PrefixErr(ctx, fmt.Sprintf("%s xml-parse: ", p.Path)), p, resC, errC) {
// ctx must have been canceled - exit.
return
}
}
}()
return resC, errC
}
// xmlParserContents consumes a stream of resource files that need to have their xml contents
// parsed into resource protos. We only need to get names and types - so the parsing is very quick.
func xmlParserContents(ctx context.Context, rfC <-chan *resourceFile) (<-chan *rdpb.Resource, <-chan error) {
resC := make(chan *rdpb.Resource)
errC := make(chan error)
go func() {
defer close(resC)
defer close(errC)
for rf := range rfC {
if !syncParseContents(respipe.PrefixErr(ctx, fmt.Sprintf("%s xml-parse: ", rf.pathInfo.Path)), rf.pathInfo, bytes.NewReader(rf.contents), resC, errC) {
// ctx must have been canceled - exit.
return
}
}
}()
return resC, errC
}
func syncParse(ctx context.Context, p *res.PathInfo, resC chan<- *rdpb.Resource, errC chan<- error) bool {
f, err := os.Open(p.Path)
if err != nil {
return respipe.SendErr(ctx, errC, respipe.Errorf(ctx, "open failed: %v", err))
}
defer f.Close()
return syncParseContents(ctx, p, f, resC, errC)
}
func syncParseContents(ctx context.Context, p *res.PathInfo, fileReader io.Reader, resC chan<- *rdpb.Resource, errC chan<- error) bool {
parsedResC, mergedErrC := parseContents(ctx, p, fileReader)
for parsedResC != nil || mergedErrC != nil {
select {
case r, ok := <-parsedResC:
if !ok {
parsedResC = nil
continue
}
if !respipe.SendRes(ctx, resC, r) {
return false
}
case e, ok := <-mergedErrC:
if !ok {
mergedErrC = nil
continue
}
if !respipe.SendErr(ctx, errC, e) {
return false
}
}
}
return true
}
func parseContents(ctx context.Context, filePathInfo *res.PathInfo, fileReader io.Reader) (resC <-chan *rdpb.Resource, errC <-chan error) {
xmlC, xmlErrC := resxml.StreamDoc(ctx, fileReader)
var parsedErrC <-chan error
if filePathInfo.Type == res.ValueType {
ctx := respipe.PrefixErr(ctx, "mini-values-parse: ")
resC, parsedErrC = valuesParse(ctx, xmlC)
} else {
ctx := respipe.PrefixErr(ctx, "mini-non-values-parse: ")
resC, parsedErrC = nonValuesParse(ctx, xmlC)
}
errC = respipe.MergeErrStreams(ctx, []<-chan error{parsedErrC, xmlErrC})
return resC, errC
}
// needsParse determines if a path needs to have a values / nonvalues xml parser run to extract
// resource information.
func needsParse(pi *res.PathInfo) (bool, error) {
r, err := os.Open(pi.Path)
if err != nil {
return false, fmt.Errorf("Unable to open file %s: %s", pi.Path, err)
}
defer r.Close()
return needsParseContents(pi, r), nil
}
// needsParseContents determines if a path with the corresponding reader for contents needs to have a
// values / nonvalues xml parser run to extract resource information.
func needsParseContents(pi *res.PathInfo, r io.Reader) bool {
if pi.Type == res.Raw {
return false
}
if filepath.Ext(pi.Path) == ".xml" {
return true
}
if filepath.Ext(pi.Path) == "" {
var header [5]byte
_, err := io.ReadFull(r, header[:])
if err != nil && err != io.EOF {
log.Fatal("Unable to read file %s: %s", pi.Path, err)
}
if string(header[:]) == "<?xml" {
return true
}
}
return false
}
// pathAsRes determines if a particular res.PathInfo is also a standalone resource.
func pathAsRes(pi *res.PathInfo) (string, bool) {
if pi.Type.Kind() == res.Value || (pi.Type.Kind() == res.Both && strings.HasPrefix(pi.TypeDir, "values")) {
return "", false
}
p := path.Base(pi.Path)
// Only split on last index of dot when the resource is of RAW type.
// Some drawable resources (Nine-Patch files) ends with .9.png which should not
// be included in the resource name.
if dot := strings.LastIndex(p, "."); dot >= 0 && pi.Type == res.Raw {
return p[:dot], true
}
if dot := strings.Index(p, "."); dot >= 0 {
return p[:dot], true
}
return p, true
}