diff options
Diffstat (limited to 'src/tools/ak/liteparse/liteparse.go')
-rw-r--r-- | src/tools/ak/liteparse/liteparse.go | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/src/tools/ak/liteparse/liteparse.go b/src/tools/ak/liteparse/liteparse.go new file mode 100644 index 0000000..9ab50d8 --- /dev/null +++ b/src/tools/ak/liteparse/liteparse.go @@ -0,0 +1,436 @@ +// 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 +} |