internal/binres: provide decoding of resources.arsc in platform sdk

This decodes enough information to allow validation of resource
names but not their values, though a simple method for querying
does not yet exist.

Change-Id: I32023f3de0eda390b12d5c1df6c06202ee4d0778
Reviewed-on: https://go-review.googlesource.com/18055
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/internal/binres/binres.go b/internal/binres/binres.go
index d9e792f..2c745aa 100644
--- a/internal/binres/binres.go
+++ b/internal/binres/binres.go
@@ -50,6 +50,19 @@
 	"fmt"
 )
 
+type ErrWrongType struct {
+	have ResType
+	want []ResType
+}
+
+func errWrongType(have ResType, want ...ResType) ErrWrongType {
+	return ErrWrongType{have, want}
+}
+
+func (err ErrWrongType) Error() string {
+	return fmt.Sprintf("wrong resource type %s, want one of %v", err.have, err.want)
+}
+
 type ResType uint16
 
 func (t ResType) IsSupported() bool {
@@ -59,7 +72,8 @@
 		t == ResXMLStartElement || t == ResXMLEndElement ||
 		t == ResXMLCharData ||
 		t == ResXMLResourceMap ||
-		t == ResTable || t == ResTablePackage
+		t == ResTable || t == ResTablePackage ||
+		t == ResTableTypeSpec || t == ResTableType
 }
 
 // explicitly defined for clarity and resolvability with apt source
@@ -80,6 +94,7 @@
 	ResTablePackage  ResType = 0x0200
 	ResTableType     ResType = 0x0201
 	ResTableTypeSpec ResType = 0x0202
+	ResTableLibrary  ResType = 0x0203
 )
 
 var (
@@ -133,6 +148,7 @@
 	if !hdr.typ.IsSupported() {
 		return nil, fmt.Errorf("%s not supported", hdr.typ)
 	}
+
 	bin := make([]byte, 8)
 	putu16(bin, uint16(hdr.typ))
 	putu16(bin[2:], hdr.headerByteSize)
diff --git a/internal/binres/binres_string.go b/internal/binres/binres_string.go
index d1d08a8..3e8d891 100644
--- a/internal/binres/binres_string.go
+++ b/internal/binres/binres_string.go
@@ -8,14 +8,14 @@
 	_ResType_name_0 = "ResNullResStringPoolResTableResXML"
 	_ResType_name_1 = "ResXMLStartNamespaceResXMLEndNamespaceResXMLStartElementResXMLEndElementResXMLCharData"
 	_ResType_name_2 = "ResXMLResourceMap"
-	_ResType_name_3 = "ResTablePackageResTableTypeResTableTypeSpec"
+	_ResType_name_3 = "ResTablePackageResTableTypeResTableTypeSpecResTableLibrary"
 )
 
 var (
 	_ResType_index_0 = [...]uint8{7, 20, 28, 34}
 	_ResType_index_1 = [...]uint8{20, 38, 56, 72, 86}
 	_ResType_index_2 = [...]uint8{17}
-	_ResType_index_3 = [...]uint8{15, 27, 43}
+	_ResType_index_3 = [...]uint8{15, 27, 43, 58}
 )
 
 func (i ResType) String() string {
@@ -35,7 +35,7 @@
 		return _ResType_name_1[lo:_ResType_index_1[i]]
 	case i == 384:
 		return _ResType_name_2
-	case 512 <= i && i <= 514:
+	case 512 <= i && i <= 515:
 		i -= 512
 		lo := uint8(0)
 		if i > 0 {
diff --git a/internal/binres/binres_test.go b/internal/binres/binres_test.go
index 739e1e0..73571bc 100644
--- a/internal/binres/binres_test.go
+++ b/internal/binres/binres_test.go
@@ -5,7 +5,6 @@
 package binres
 
 import (
-	"archive/zip"
 	"bytes"
 	"encoding"
 	"encoding/xml"
@@ -15,6 +14,7 @@
 	"log"
 	"math"
 	"os"
+	"path"
 	"sort"
 	"strings"
 	"testing"
@@ -146,11 +146,11 @@
 	return fo
 }
 
-type ByNamespace []xml.Attr
+type byNamespace []xml.Attr
 
-func (a ByNamespace) Len() int      { return len(a) }
-func (a ByNamespace) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a ByNamespace) Less(i, j int) bool {
+func (a byNamespace) Len() int      { return len(a) }
+func (a byNamespace) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a byNamespace) Less(i, j int) bool {
 	if a[j].Name.Space == "" {
 		return a[i].Name.Space != ""
 	}
@@ -191,18 +191,18 @@
 			Space: "",
 			Local: "platformBuildVersionCode",
 		},
-		Value: "10",
+		Value: "15",
 	}
 	bvn := xml.Attr{
 		Name: xml.Name{
 			Space: "",
 			Local: "platformBuildVersionName",
 		},
-		Value: "2.3.3",
+		Value: "4.0.3",
 	}
 	attrs = append(attrs, bvc, bvn)
 
-	sort.Sort(ByNamespace(attrs))
+	sort.Sort(byNamespace(attrs))
 	var names, vals []string
 	for _, attr := range attrs {
 		if strings.HasSuffix(attr.Name.Space, "tools") {
@@ -228,44 +228,41 @@
 	}
 }
 
-func TestAndroidJar(t *testing.T) {
-	zr, err := zip.OpenReader("/home/daniel/local/android-sdk/platforms/android-10/android.jar")
+func TestOpenTable(t *testing.T) {
+	sdkdir := os.Getenv("ANDROID_HOME")
+	if sdkdir == "" {
+		t.Fatal("ANDROID_HOME env var not set")
+	}
+	tbl, err := OpenTable(path.Join(sdkdir, "platforms/android-15/android.jar"))
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer zr.Close()
+	if len(tbl.pkgs) == 0 {
+		t.Fatal("failed to decode any resource packages")
+	}
 
-	buf := new(bytes.Buffer)
-	for _, f := range zr.File {
-		if f.Name == "resources.arsc" {
-			rc, err := f.Open()
-			if err != nil {
-				t.Fatal(err)
+	pkg := tbl.pkgs[0]
+
+	t.Log("package name:", pkg.name)
+
+	for i, x := range pkg.typePool.strings {
+		t.Logf("typePool[i=%v]: %s\n", i, x)
+	}
+
+	for i, spec := range pkg.specs {
+		t.Logf("spec[i=%v]: %v %q\n", i, spec.id, pkg.typePool.strings[spec.id-1])
+		for j, typ := range spec.types {
+			t.Logf("\ttype[i=%v]: %v\n", j, typ.id)
+			for k, nt := range typ.entries {
+				if nt == nil { // NoEntry
+					continue
+				}
+				t.Logf("\t\tentry[i=%v]: %v %q\n", k, nt.key, pkg.keyPool.strings[nt.key])
+				if k > 5 {
+					t.Logf("\t\t... truncating output")
+					break
+				}
 			}
-			_, err = io.Copy(buf, rc)
-			if err != nil {
-				t.Fatal(err)
-			}
-			rc.Close()
-			break
 		}
 	}
-	if buf.Len() == 0 {
-		t.Fatal("failed to read resources.arsc")
-	}
-
-	bin := buf.Bytes()
-
-	tbl := &Table{}
-	if err := tbl.UnmarshalBinary(bin); err != nil {
-		t.Fatal(err)
-	}
-
-	t.Logf("%+v\n", tbl.TableHeader)
-	t.Logf("%+v\n", tbl.pool.chunkHeader)
-	for i, x := range tbl.pool.strings[:10] {
-		t.Logf("pool(%v) %s\n", i, x)
-	}
-
-	t.Logf("%+v\n", tbl.pkg)
 }
diff --git a/internal/binres/package.go b/internal/binres/package.go
deleted file mode 100644
index 8b967dd..0000000
--- a/internal/binres/package.go
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright 2015 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package binres
-
-type TableHeader struct {
-	chunkHeader
-	packageCount uint32
-}
-
-func (hdr *TableHeader) UnmarshalBinary(bin []byte) error {
-	if err := (&hdr.chunkHeader).UnmarshalBinary(bin); err != nil {
-		return err
-	}
-	hdr.packageCount = btou32(bin[8:])
-	return nil
-}
-
-func (hdr TableHeader) MarshalBinary() ([]byte, error) {
-	bin := make([]byte, 12)
-	b, err := hdr.chunkHeader.MarshalBinary()
-	if err != nil {
-		return nil, err
-	}
-	copy(bin, b)
-	putu32(b[8:], hdr.packageCount)
-	return bin, nil
-}
-
-// TODO next up: package chunk
-// https://justanapplication.wordpress.com/2011/09/16/
-type Table struct {
-	TableHeader
-	pool *Pool
-	pkg  *Package
-}
-
-func (tbl *Table) UnmarshalBinary(bin []byte) error {
-	buf := bin
-	if err := (&tbl.TableHeader).UnmarshalBinary(buf); err != nil {
-		return err
-	}
-	buf = buf[tbl.headerByteSize:]
-	tbl.pool = new(Pool)
-	if err := tbl.pool.UnmarshalBinary(buf); err != nil {
-		return err
-	}
-	buf = buf[tbl.pool.size():]
-	tbl.pkg = new(Package)
-	if err := tbl.pkg.UnmarshalBinary(buf); err != nil {
-		return err
-	}
-	return nil
-}
-
-type Package struct {
-	chunkHeader
-
-	// If this is a base package, its ID.  Package IDs start
-	// at 1 (corresponding to the value of the package bits in a
-	// resource identifier).  0 means this is not a base package.
-	id uint32
-
-	// name of package, zero terminated
-	name [128]uint16
-
-	// Offset to a ResStringPool_header defining the resource
-	// type symbol table.  If zero, this package is inheriting from
-	// another base package (overriding specific values in it).
-	typeStrings uint32
-
-	// Last index into typeStrings that is for public use by others.
-	lastPublicType uint32
-
-	// Offset to a ResStringPool_header defining the resource
-	// key symbol table.  If zero, this package is inheriting from
-	// another base package (overriding specific values in it).
-	keyStrings uint32
-
-	// Last index into keyStrings that is for public use by others.
-	lastPublicKey uint32
-
-	typeIdOffset uint32
-}
-
-func (pkg *Package) UnmarshalBinary(bin []byte) error {
-	if err := (&pkg.chunkHeader).UnmarshalBinary(bin); err != nil {
-		return err
-	}
-	pkg.id = btou32(bin[8:])
-	for i := range pkg.name {
-		pkg.name[i] = btou16(bin[12+i*2:])
-	}
-	pkg.typeStrings = btou32(bin[140:])
-	pkg.lastPublicType = btou32(bin[144:])
-	pkg.keyStrings = btou32(bin[148:])
-	pkg.lastPublicKey = btou32(bin[152:])
-	pkg.typeIdOffset = btou32(bin[156:])
-	return nil
-}
diff --git a/internal/binres/table.go b/internal/binres/table.go
new file mode 100644
index 0000000..25015bd
--- /dev/null
+++ b/internal/binres/table.go
@@ -0,0 +1,287 @@
+// Copyright 2015 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package binres
+
+import (
+	"archive/zip"
+	"bytes"
+	"fmt"
+	"io"
+	"unicode/utf16"
+)
+
+const NoEntry = 0xFFFFFFFF
+
+// TableRef uniquely identifies entries within a resource table.
+type TableRef uint32
+
+// Table is a container for packaged resources. Resource values within a package
+// are obtained through pool while resource names and identifiers are obtained
+// through each package's type and key pools respectively.
+//
+// TODO provide method for querying resource types.
+type Table struct {
+	chunkHeader
+	pool *Pool
+	pkgs []*Package
+}
+
+// OpenTable decodes resources.arsc from an sdk platform jar.
+func OpenTable(path string) (*Table, error) {
+	zr, err := zip.OpenReader(path)
+	if err != nil {
+		return nil, err
+	}
+	defer zr.Close()
+
+	buf := new(bytes.Buffer)
+	for _, f := range zr.File {
+		if f.Name == "resources.arsc" {
+			rc, err := f.Open()
+			if err != nil {
+				return nil, err
+			}
+			_, err = io.Copy(buf, rc)
+			if err != nil {
+				return nil, err
+			}
+			rc.Close()
+			break
+		}
+	}
+	if buf.Len() == 0 {
+		return nil, fmt.Errorf("failed to read resources.arsc")
+	}
+
+	tbl := new(Table)
+	if err := tbl.UnmarshalBinary(buf.Bytes()); err != nil {
+		return nil, err
+	}
+	return tbl, nil
+}
+
+func (tbl *Table) UnmarshalBinary(bin []byte) error {
+	if err := (&tbl.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	if tbl.typ != ResTable {
+		return fmt.Errorf("unexpected resource type %s, want %s", tbl.typ, ResTable)
+	}
+
+	npkgs := btou32(bin[8:])
+	tbl.pkgs = make([]*Package, npkgs)
+
+	buf := bin[tbl.headerByteSize:]
+
+	tbl.pool = new(Pool)
+	if err := tbl.pool.UnmarshalBinary(buf); err != nil {
+		return err
+	}
+	buf = buf[tbl.pool.size():]
+	for i := range tbl.pkgs {
+		pkg := new(Package)
+		if err := pkg.UnmarshalBinary(buf); err != nil {
+			return err
+		}
+		tbl.pkgs[i] = pkg
+		buf = buf[pkg.byteSize:]
+	}
+
+	return nil
+}
+
+// Package contains a collection of resource data types.
+type Package struct {
+	chunkHeader
+
+	id   uint32
+	name string
+
+	lastPublicType uint32 // last index into typePool that is for public use
+	lastPublicKey  uint32 // last index into keyPool that is for public use
+
+	typePool *Pool // type names; e.g. theme
+	keyPool  *Pool // resource names; e.g. Theme.NoTitleBar
+
+	specs []*TypeSpec
+}
+
+func (pkg *Package) UnmarshalBinary(bin []byte) error {
+	if err := (&pkg.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	if pkg.typ != ResTablePackage {
+		return errWrongType(pkg.typ, ResTablePackage)
+	}
+
+	pkg.id = btou32(bin[8:])
+
+	var name []uint16
+	for i := 0; i < 128; i++ {
+		x := btou16(bin[12+i*2:])
+		if x == 0 {
+			break
+		}
+		name = append(name, x)
+	}
+	pkg.name = string(utf16.Decode(name))
+
+	typeOffset := btou32(bin[268:]) // 0 if inheriting from another package
+	pkg.lastPublicType = btou32(bin[272:])
+	keyOffset := btou32(bin[276:]) // 0 if inheriting from another package
+	pkg.lastPublicKey = btou32(bin[280:])
+
+	var idOffset uint32 // value determined by either typePool or keyPool below
+
+	if typeOffset != 0 {
+		pkg.typePool = new(Pool)
+		if err := pkg.typePool.UnmarshalBinary(bin[typeOffset:]); err != nil {
+			return err
+		}
+		idOffset = typeOffset + pkg.typePool.byteSize
+	}
+
+	if keyOffset != 0 {
+		pkg.keyPool = new(Pool)
+		if err := pkg.keyPool.UnmarshalBinary(bin[keyOffset:]); err != nil {
+			return err
+		}
+		idOffset = keyOffset + pkg.keyPool.byteSize
+	}
+
+	if idOffset == 0 {
+		return nil
+	}
+
+	buf := bin[idOffset:]
+	for len(pkg.specs) < len(pkg.typePool.strings) {
+		t := ResType(btou16(buf))
+		switch t {
+		case ResTableTypeSpec:
+			spec := new(TypeSpec)
+			if err := spec.UnmarshalBinary(buf); err != nil {
+				return err
+			}
+			pkg.specs = append(pkg.specs, spec)
+			buf = buf[spec.byteSize:]
+		case ResTableType:
+			typ := new(Type)
+			if err := typ.UnmarshalBinary(buf); err != nil {
+				return err
+			}
+			last := pkg.specs[len(pkg.specs)-1]
+			last.types = append(last.types, typ)
+			buf = buf[typ.byteSize:]
+		default:
+			return errWrongType(t, ResTableTypeSpec, ResTableType)
+		}
+	}
+
+	return nil
+}
+
+// TypeSpec provides a specification for the resources defined by a particular type.
+type TypeSpec struct {
+	chunkHeader
+	id         uint8  // id-1 is name index in Package.typePool
+	res0       uint8  // must be 0
+	res1       uint16 // must be 0
+	entryCount uint32 // number of uint32 entry configuration masks that follow
+
+	entries []uint32
+	types   []*Type
+}
+
+func (spec *TypeSpec) UnmarshalBinary(bin []byte) error {
+	if err := (&spec.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	if spec.typ != ResTableTypeSpec {
+		return errWrongType(spec.typ, ResTableTypeSpec)
+	}
+	spec.id = uint8(bin[8])
+	spec.res0 = uint8(bin[9])
+	spec.res1 = btou16(bin[10:])
+	spec.entryCount = btou32(bin[12:])
+
+	spec.entries = make([]uint32, spec.entryCount)
+	for i := range spec.entries {
+		spec.entries[i] = btou32(bin[16+i*4:])
+	}
+
+	return nil
+}
+
+type Type struct {
+	chunkHeader
+	id           uint8
+	res0         uint8  // must be 0
+	res1         uint16 // must be 0
+	entryCount   uint32 // number of uint32 entry configuration masks that follow
+	entriesStart uint32 // offset from header where Entry data starts
+
+	// TODO implement and decode Config if strictly necessary
+	// config Config // configuration this collection of entries is designed for
+
+	indices []uint32 // values that map to typePool
+	entries []*Entry
+}
+
+func (typ *Type) UnmarshalBinary(bin []byte) error {
+	if err := (&typ.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	if typ.typ != ResTableType {
+		return errWrongType(typ.typ, ResTableType)
+	}
+
+	typ.id = uint8(bin[8])
+	typ.res0 = uint8(bin[9])
+	typ.res1 = btou16(bin[10:])
+	typ.entryCount = btou32(bin[12:])
+	typ.entriesStart = btou32(bin[16:])
+
+	if typ.res0 != 0 || typ.res1 != 0 {
+		return fmt.Errorf("res0 res1 not zero")
+	}
+
+	buf := bin[typ.headerByteSize:typ.entriesStart]
+	for len(buf) > 0 {
+		typ.indices = append(typ.indices, btou32(buf))
+		buf = buf[4:]
+	}
+
+	if len(typ.indices) != int(typ.entryCount) {
+		return fmt.Errorf("indices len[%v] doesn't match entryCount[%v]", len(typ.indices), typ.entryCount)
+	}
+	typ.entries = make([]*Entry, typ.entryCount)
+
+	for i, x := range typ.indices {
+		if x == NoEntry {
+			continue
+		}
+		nt := &Entry{}
+		if err := nt.UnmarshalBinary(bin[typ.entriesStart+x:]); err != nil {
+			return err
+		}
+		typ.entries[i] = nt
+	}
+
+	return nil
+}
+
+// Entry is a resource key typically followed by a value or resource map.
+type Entry struct {
+	size  uint16
+	flags uint16
+	key   PoolRef // ref into key pool
+}
+
+func (nt *Entry) UnmarshalBinary(bin []byte) error {
+	nt.size = btou16(bin)
+	nt.flags = btou16(bin[2:])
+	nt.key = PoolRef(btou32(bin[4:]))
+	return nil
+}