cmd/gomobile: new binary resource implementation

Change-Id: I9708170ac2c5914bb8e978b4703f4e6f7415c737
Reviewed-on: https://go-review.googlesource.com/16553
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/internal/binres/binres.go b/internal/binres/binres.go
new file mode 100644
index 0000000..d9e792f
--- /dev/null
+++ b/internal/binres/binres.go
@@ -0,0 +1,342 @@
+// 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.
+
+//go:generate stringer -output binres_string.go -type ResType,DataType
+
+// Package binres implements encoding and decoding of android binary resources.
+//
+// Binary resource structs support unmarshalling the binary output of aapt.
+// Implementations of marshalling for each struct must produce the exact input
+// sent to unmarshalling. This allows tests to validate each struct representation
+// of the binary format as follows:
+//
+//  * unmarshal the output of aapt
+//  * marshal the struct representation
+//  * perform byte-to-byte comparison with aapt output per chunk header and body
+//
+// This process should strive to make structs idiomatic to make parsing xml text
+// into structs trivial.
+//
+// Once the struct representation is validated, tests for parsing xml text
+// into structs can become self-referential as the following holds true:
+//
+//  * the unmarshalled input of aapt output is the only valid target
+//  * the unmarshalled input of xml text may be compared to the unmarshalled
+//    input of aapt output to identify errors, e.g. text-trims, wrong flags, etc
+//
+// This provides validation, byte-for-byte, for producing binary xml resources.
+//
+// It should be made clear that unmarshalling binary resources is currently only
+// in scope for proving that the BinaryMarshaler works correctly. Any other use
+// is currently out of scope.
+//
+// A simple view of binary xml document structure:
+//
+//  XML
+//    Pool
+//    Map
+//    Namespace
+//    [...node]
+//
+// Additional resources:
+// https://android.googlesource.com/platform/frameworks/base/+/master/include/androidfw/ResourceTypes.h
+// https://justanapplication.wordpress.com/2011/09/13/ (a series of articles, increment date)
+package binres
+
+import (
+	"encoding"
+	"encoding/binary"
+	"fmt"
+)
+
+type ResType uint16
+
+func (t ResType) IsSupported() bool {
+	// explicit for clarity
+	return t == ResStringPool || t == ResXML ||
+		t == ResXMLStartNamespace || t == ResXMLEndNamespace ||
+		t == ResXMLStartElement || t == ResXMLEndElement ||
+		t == ResXMLCharData ||
+		t == ResXMLResourceMap ||
+		t == ResTable || t == ResTablePackage
+}
+
+// explicitly defined for clarity and resolvability with apt source
+const (
+	ResNull       ResType = 0x0000
+	ResStringPool ResType = 0x0001
+	ResTable      ResType = 0x0002
+	ResXML        ResType = 0x0003
+
+	ResXMLStartNamespace ResType = 0x0100
+	ResXMLEndNamespace   ResType = 0x0101
+	ResXMLStartElement   ResType = 0x0102
+	ResXMLEndElement     ResType = 0x0103
+	ResXMLCharData       ResType = 0x0104
+
+	ResXMLResourceMap ResType = 0x0180
+
+	ResTablePackage  ResType = 0x0200
+	ResTableType     ResType = 0x0201
+	ResTableTypeSpec ResType = 0x0202
+)
+
+var (
+	btou16 = binary.LittleEndian.Uint16
+	btou32 = binary.LittleEndian.Uint32
+	putu16 = binary.LittleEndian.PutUint16
+	putu32 = binary.LittleEndian.PutUint32
+)
+
+// unmarshaler wraps BinaryUnmarshaler to provide byte size of decoded chunks.
+type unmarshaler interface {
+	encoding.BinaryUnmarshaler
+
+	// size returns the byte size unmarshalled after a call to
+	// UnmarshalBinary, or otherwise zero.
+	size() int
+}
+
+// chunkHeader appears at the front of every data chunk in a resource.
+// TODO look into removing this, it's not necessary for marshalling and
+// the information provided may possibly be given more simply. For unmarshal,
+// a simple function would do.
+type chunkHeader struct {
+	// Type of data that follows this header.
+	typ ResType
+
+	// Advance slice index by this value to find its associated data, if any.
+	headerByteSize uint16
+
+	// This is the header size plus the size of any data associated with the chunk.
+	// Advance slice index by this value to completely skip its contents, including
+	// any child chunks. If this value is the same as headerByteSize, there is
+	// no data associated with the chunk.
+	byteSize uint32
+}
+
+// size implements unmarshaler.
+func (hdr chunkHeader) size() int { return int(hdr.byteSize) }
+
+func (hdr *chunkHeader) UnmarshalBinary(bin []byte) error {
+	hdr.typ = ResType(btou16(bin))
+	if !hdr.typ.IsSupported() {
+		return fmt.Errorf("%s not supported", hdr.typ)
+	}
+	hdr.headerByteSize = btou16(bin[2:])
+	hdr.byteSize = btou32(bin[4:])
+	return nil
+}
+
+func (hdr chunkHeader) MarshalBinary() ([]byte, error) {
+	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)
+	putu32(bin[4:], hdr.byteSize)
+	return bin, nil
+}
+
+type XML struct {
+	chunkHeader
+
+	Pool *Pool
+	Map  *Map
+
+	Namespace *Namespace
+	Children  []*Element
+
+	// tmp field used when unmarshalling
+	stack []*Element
+}
+
+// TODO this is used strictly for querying in tests and dependent on
+// current XML.UnmarshalBinary implementation. Look into moving directly
+// into tests.
+var debugIndices = make(map[encoding.BinaryMarshaler]int)
+
+func (bx *XML) UnmarshalBinary(bin []byte) error {
+	buf := bin
+	if err := (&bx.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	buf = buf[8:]
+
+	// TODO this is tracked strictly for querying in tests; look into moving this
+	// functionality directly into tests if possible.
+	debugIndex := 8
+
+	for len(buf) > 0 {
+		t := ResType(btou16(buf))
+		k, err := bx.kind(t)
+		if err != nil {
+			return err
+		}
+		if err := k.UnmarshalBinary(buf); err != nil {
+			return err
+		}
+		debugIndices[k.(encoding.BinaryMarshaler)] = debugIndex
+		debugIndex += int(k.size())
+		buf = buf[k.size():]
+	}
+	return nil
+}
+
+func (bx *XML) kind(t ResType) (unmarshaler, error) {
+	switch t {
+	case ResStringPool:
+		if bx.Pool != nil {
+			return nil, fmt.Errorf("pool already exists")
+		}
+		bx.Pool = new(Pool)
+		return bx.Pool, nil
+	case ResXMLResourceMap:
+		if bx.Map != nil {
+			return nil, fmt.Errorf("resource map already exists")
+		}
+		bx.Map = new(Map)
+		return bx.Map, nil
+	case ResXMLStartNamespace:
+		if bx.Namespace != nil {
+			return nil, fmt.Errorf("namespace start already exists")
+		}
+		bx.Namespace = new(Namespace)
+		return bx.Namespace, nil
+	case ResXMLEndNamespace:
+		if bx.Namespace.end != nil {
+			return nil, fmt.Errorf("namespace end already exists")
+		}
+		bx.Namespace.end = new(Namespace)
+		return bx.Namespace.end, nil
+	case ResXMLStartElement:
+		el := new(Element)
+		if len(bx.stack) == 0 {
+			bx.Children = append(bx.Children, el)
+		} else {
+			n := len(bx.stack)
+			var p *Element
+			p, bx.stack = bx.stack[n-1], bx.stack[:n-1]
+			p.Children = append(p.Children, el)
+			bx.stack = append(bx.stack, p)
+		}
+		bx.stack = append(bx.stack, el)
+		return el, nil
+	case ResXMLEndElement:
+		n := len(bx.stack)
+		var el *Element
+		el, bx.stack = bx.stack[n-1], bx.stack[:n-1]
+		if el.end != nil {
+			return nil, fmt.Errorf("element end already exists")
+		}
+		el.end = new(ElementEnd)
+		return el.end, nil
+	case ResXMLCharData: // TODO
+		cdt := new(CharData)
+		el := bx.stack[len(bx.stack)-1]
+		if el.head == nil {
+			el.head = cdt
+		} else if el.tail == nil {
+			el.tail = cdt
+		} else {
+			return nil, fmt.Errorf("element head and tail already contain chardata")
+		}
+		return cdt, nil
+	default:
+		return nil, fmt.Errorf("unexpected type %s", t)
+	}
+}
+
+func (bx *XML) MarshalBinary() ([]byte, error) {
+	var (
+		bin, b []byte
+		err    error
+	)
+	b, err = bx.chunkHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	bin = append(bin, b...)
+
+	b, err = bx.Pool.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	bin = append(bin, b...)
+
+	b, err = bx.Map.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	bin = append(bin, b...)
+
+	b, err = bx.Namespace.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	bin = append(bin, b...)
+
+	for _, child := range bx.Children {
+		if err := marshalRecurse(child, &bin); err != nil {
+			return nil, err
+		}
+	}
+
+	b, err = bx.Namespace.end.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	bin = append(bin, b...)
+
+	return bin, nil
+}
+
+func marshalRecurse(el *Element, bin *[]byte) error {
+	b, err := el.MarshalBinary()
+	if err != nil {
+		return err
+	}
+	*bin = append(*bin, b...)
+
+	if el.head != nil {
+		b, err := el.head.MarshalBinary()
+		if err != nil {
+			return err
+		}
+		*bin = append(*bin, b...)
+	}
+
+	for _, child := range el.Children {
+		if err := marshalRecurse(child, bin); err != nil {
+			return err
+		}
+	}
+
+	b, err = el.end.MarshalBinary()
+	if err != nil {
+		return err
+	}
+	*bin = append(*bin, b...)
+
+	return nil
+}
+
+func (bx *XML) iterElements() <-chan *Element {
+	ch := make(chan *Element, 1)
+	go func() {
+		for _, el := range bx.Children {
+			iterElementsRecurse(el, ch)
+		}
+		close(ch)
+	}()
+	return ch
+}
+
+func iterElementsRecurse(el *Element, ch chan *Element) {
+	ch <- el
+	for _, e := range el.Children {
+		iterElementsRecurse(e, ch)
+	}
+}
diff --git a/internal/binres/binres_string.go b/internal/binres/binres_string.go
new file mode 100644
index 0000000..d1d08a8
--- /dev/null
+++ b/internal/binres/binres_string.go
@@ -0,0 +1,87 @@
+// generated by stringer -output binres_string.go -type ResType,DataType; DO NOT EDIT
+
+package binres
+
+import "fmt"
+
+const (
+	_ResType_name_0 = "ResNullResStringPoolResTableResXML"
+	_ResType_name_1 = "ResXMLStartNamespaceResXMLEndNamespaceResXMLStartElementResXMLEndElementResXMLCharData"
+	_ResType_name_2 = "ResXMLResourceMap"
+	_ResType_name_3 = "ResTablePackageResTableTypeResTableTypeSpec"
+)
+
+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}
+)
+
+func (i ResType) String() string {
+	switch {
+	case 0 <= i && i <= 3:
+		lo := uint8(0)
+		if i > 0 {
+			lo = _ResType_index_0[i-1]
+		}
+		return _ResType_name_0[lo:_ResType_index_0[i]]
+	case 256 <= i && i <= 260:
+		i -= 256
+		lo := uint8(0)
+		if i > 0 {
+			lo = _ResType_index_1[i-1]
+		}
+		return _ResType_name_1[lo:_ResType_index_1[i]]
+	case i == 384:
+		return _ResType_name_2
+	case 512 <= i && i <= 514:
+		i -= 512
+		lo := uint8(0)
+		if i > 0 {
+			lo = _ResType_index_3[i-1]
+		}
+		return _ResType_name_3[lo:_ResType_index_3[i]]
+	default:
+		return fmt.Sprintf("ResType(%d)", i)
+	}
+}
+
+const (
+	_DataType_name_0 = "DataNullDataReferenceDataAttributeDataStringDataFloatDataDimensionDataFractionDataDynamicReference"
+	_DataType_name_1 = "DataIntDecDataIntHexDataIntBool"
+	_DataType_name_2 = "DataIntColorARGB8DataIntColorRGB8DataIntColorARGB4DataIntColorRGB4"
+)
+
+var (
+	_DataType_index_0 = [...]uint8{8, 21, 34, 44, 53, 66, 78, 98}
+	_DataType_index_1 = [...]uint8{10, 20, 31}
+	_DataType_index_2 = [...]uint8{17, 33, 50, 66}
+)
+
+func (i DataType) String() string {
+	switch {
+	case 0 <= i && i <= 7:
+		lo := uint8(0)
+		if i > 0 {
+			lo = _DataType_index_0[i-1]
+		}
+		return _DataType_name_0[lo:_DataType_index_0[i]]
+	case 16 <= i && i <= 18:
+		i -= 16
+		lo := uint8(0)
+		if i > 0 {
+			lo = _DataType_index_1[i-1]
+		}
+		return _DataType_name_1[lo:_DataType_index_1[i]]
+	case 28 <= i && i <= 31:
+		i -= 28
+		lo := uint8(0)
+		if i > 0 {
+			lo = _DataType_index_2[i-1]
+		}
+		return _DataType_name_2[lo:_DataType_index_2[i]]
+	default:
+		return fmt.Sprintf("DataType(%d)", i)
+	}
+}
diff --git a/internal/binres/binres_test.go b/internal/binres/binres_test.go
new file mode 100644
index 0000000..739e1e0
--- /dev/null
+++ b/internal/binres/binres_test.go
@@ -0,0 +1,271 @@
+// 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"
+	"encoding"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"math"
+	"os"
+	"sort"
+	"strings"
+	"testing"
+)
+
+func printrecurse(t *testing.T, pl *Pool, el *Element, ws string) {
+	for _, attr := range el.attrs {
+		ns := ""
+		if attr.NS != math.MaxUint32 {
+			ns = pl.strings[int(attr.NS)]
+			nss := strings.Split(ns, "/")
+			ns = nss[len(nss)-1]
+		}
+
+		val := ""
+		if attr.RawValue != math.MaxUint32 {
+			val = pl.strings[int(attr.RawValue)]
+		} else {
+			switch attr.TypedValue.Type {
+			case DataIntDec:
+				val = fmt.Sprintf("%v", attr.TypedValue.Value)
+			case DataIntBool:
+				val = fmt.Sprintf("%v", attr.TypedValue.Value == 1)
+			default:
+				val = fmt.Sprintf("0x%08X", attr.TypedValue.Value)
+			}
+		}
+		dt := attr.TypedValue.Type
+
+		t.Logf("%s|attr:ns(%v) name(%s) val(%s) valtyp(%s)\n", ws, ns, pl.strings[int(attr.Name)], val, dt)
+	}
+	t.Log()
+	for _, e := range el.Children {
+		printrecurse(t, pl, e, ws+"        ")
+	}
+}
+
+func TestBootstrap(t *testing.T) {
+	bin, err := ioutil.ReadFile("testdata/bootstrap.bin")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	checkMarshal := func(res encoding.BinaryMarshaler, bsize int) {
+		b, err := res.MarshalBinary()
+		if err != nil {
+			t.Error(err)
+		}
+		idx := debugIndices[res]
+		a := bin[idx : idx+bsize]
+		if !bytes.Equal(a, b) {
+			x, y := len(a), len(b)
+			if x != y {
+				t.Errorf("%v: %T: byte length does not match, have %v, want %v", idx, res, y, x)
+			}
+			if x > y {
+				x, y = y, x
+			}
+			mismatch := false
+			for i := 0; i < x; i++ {
+				if mismatch = a[i] != b[i]; mismatch {
+					t.Errorf("%v: %T: first byte mismatch at %v of %v", idx, res, i, bsize)
+					break
+				}
+			}
+			if mismatch {
+				// print out a reasonable amount of data to help identify issues
+				truncate := x > 1300
+				if truncate {
+					x = 1300
+				}
+				t.Log("      HAVE               WANT")
+				for i := 0; i < x; i += 4 {
+					he, we := 4, 4
+					if i+he >= x {
+						he = x - i
+					}
+					if i+we >= y {
+						we = y - i
+					}
+					t.Logf("%3v | % X        % X\n", i, b[i:i+he], a[i:i+we])
+				}
+				if truncate {
+					t.Log("... output truncated.")
+				}
+			}
+		}
+	}
+
+	bxml := new(XML)
+	if err := bxml.UnmarshalBinary(bin); err != nil {
+		t.Fatal(err)
+	}
+
+	for i, x := range bxml.Pool.strings {
+		t.Logf("Pool(%v): %q\n", i, x)
+	}
+
+	for _, e := range bxml.Children {
+		printrecurse(t, bxml.Pool, e, "")
+	}
+
+	checkMarshal(&bxml.chunkHeader, int(bxml.headerByteSize))
+	checkMarshal(bxml.Pool, bxml.Pool.size())
+	checkMarshal(bxml.Map, bxml.Map.size())
+	checkMarshal(bxml.Namespace, bxml.Namespace.size())
+
+	for el := range bxml.iterElements() {
+		checkMarshal(el, el.size())
+		checkMarshal(el.end, el.end.size())
+	}
+
+	checkMarshal(bxml.Namespace.end, bxml.Namespace.end.size())
+	checkMarshal(bxml, bxml.size())
+}
+
+func retset(xs []string) []string {
+	m := make(map[string]struct{})
+	fo := xs[:0]
+	for _, x := range xs {
+		if x == "" {
+			continue
+		}
+		if _, ok := m[x]; !ok {
+			m[x] = struct{}{}
+			fo = append(fo, x)
+		}
+	}
+	return fo
+}
+
+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 {
+	if a[j].Name.Space == "" {
+		return a[i].Name.Space != ""
+	}
+	return false
+}
+
+// WIP approximation of first steps to be taken to encode manifest
+func TestEncode(t *testing.T) {
+	f, err := os.Open("testdata/bootstrap.xml")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var attrs []xml.Attr
+
+	dec := xml.NewDecoder(f)
+	for {
+		tkn, err := dec.Token()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return
+			// t.Fatal(err)
+		}
+		tkn = xml.CopyToken(tkn)
+
+		switch tkn := tkn.(type) {
+		case xml.StartElement:
+			attrs = append(attrs, tkn.Attr...)
+		default:
+			// t.Error("unhandled token type", tkn)
+		}
+	}
+
+	bvc := xml.Attr{
+		Name: xml.Name{
+			Space: "",
+			Local: "platformBuildVersionCode",
+		},
+		Value: "10",
+	}
+	bvn := xml.Attr{
+		Name: xml.Name{
+			Space: "",
+			Local: "platformBuildVersionName",
+		},
+		Value: "2.3.3",
+	}
+	attrs = append(attrs, bvc, bvn)
+
+	sort.Sort(ByNamespace(attrs))
+	var names, vals []string
+	for _, attr := range attrs {
+		if strings.HasSuffix(attr.Name.Space, "tools") {
+			continue
+		}
+		names = append(names, attr.Name.Local)
+		vals = append(vals, attr.Value)
+	}
+
+	var all []string
+	all = append(all, names...)
+	all = append(all, vals...)
+
+	// do not eliminate duplicates until the entire slice has been composed.
+	// consider <activity android:label="label" .../>
+	// all attribute names come first followed by values; in such a case, the value "label"
+	// would be a reference to the same "android:label" in the string pool which will occur
+	// within the beginning of the pool where other attr names are located.
+	pl := new(Pool)
+	for _, x := range retset(all) {
+		pl.strings = append(pl.strings, x)
+		// t.Logf("Pool(%v) %q\n", i, x)
+	}
+}
+
+func TestAndroidJar(t *testing.T) {
+	zr, err := zip.OpenReader("/home/daniel/local/android-sdk/platforms/android-10/android.jar")
+	if err != nil {
+		t.Fatal(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 {
+				t.Fatal(err)
+			}
+			_, 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/data.go b/internal/binres/data.go
new file mode 100644
index 0000000..483aa14
--- /dev/null
+++ b/internal/binres/data.go
@@ -0,0 +1,50 @@
+// 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 DataType uint8
+
+// explicitly defined for clarity and resolvability with apt source
+const (
+	DataNull             DataType = 0x00 // either 0 or 1 for resource undefined or empty
+	DataReference        DataType = 0x01 // ResTable_ref, a reference to another resource table entry
+	DataAttribute        DataType = 0x02 // attribute resource identifier
+	DataString           DataType = 0x03 // index into the containing resource table's global value string pool
+	DataFloat            DataType = 0x04 // single-precision floating point number
+	DataDimension        DataType = 0x05 // complex number encoding a dimension value, such as "100in"
+	DataFraction         DataType = 0x06 // complex number encoding a fraction of a container
+	DataDynamicReference DataType = 0x07 // dynamic ResTable_ref, which needs to be resolved before it can be used like a TYPE_REFERENCE.
+	DataIntDec           DataType = 0x10 // raw integer value of the form n..n
+	DataIntHex           DataType = 0x11 // raw integer value of the form 0xn..n
+	DataIntBool          DataType = 0x12 // either 0 or 1, for input "false" or "true"
+	DataIntColorARGB8    DataType = 0x1c // raw integer value of the form #aarrggbb
+	DataIntColorRGB8     DataType = 0x1d // raw integer value of the form #rrggbb
+	DataIntColorARGB4    DataType = 0x1e // raw integer value of the form #argb
+	DataIntColorRGB4     DataType = 0x1f // raw integer value of the form #rgb
+)
+
+type Data struct {
+	ByteSize uint16
+	Res0     uint8 // always 0, useful for debugging bad read offsets
+	Type     DataType
+	Value    uint32
+}
+
+func (d *Data) UnmarshalBinary(bin []byte) error {
+	d.ByteSize = btou16(bin)
+	d.Res0 = uint8(bin[2])
+	d.Type = DataType(bin[3])
+	d.Value = btou32(bin[4:])
+	return nil
+}
+
+func (d *Data) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 8)
+	putu16(bin, d.ByteSize)
+	bin[2] = byte(d.Res0)
+	bin[3] = byte(d.Type)
+	putu32(bin[4:], d.Value)
+	return bin, nil
+}
diff --git a/internal/binres/node.go b/internal/binres/node.go
new file mode 100644
index 0000000..81d4156
--- /dev/null
+++ b/internal/binres/node.go
@@ -0,0 +1,225 @@
+// 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
+
+// NodeHeader is header all xml node types have, providing additional
+// information regarding an xml node over binChunkHeader.
+type NodeHeader struct {
+	chunkHeader
+	LineNumber uint32  // line number in source file this element appears
+	Comment    PoolRef // optional xml comment associated with element, MaxUint32 if none
+}
+
+func (hdr *NodeHeader) UnmarshalBinary(bin []byte) error {
+	if err := (&hdr.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	hdr.LineNumber = btou32(bin[8:])
+	hdr.Comment = PoolRef(btou32(bin[12:]))
+	return nil
+}
+
+func (hdr *NodeHeader) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 16)
+	b, err := hdr.chunkHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	putu32(bin[8:], hdr.LineNumber)
+	putu32(bin[12:], uint32(hdr.Comment))
+	return bin, nil
+}
+
+type Namespace struct {
+	NodeHeader
+	prefix PoolRef
+	uri    PoolRef
+
+	end *Namespace // TODO don't let this type be recursive
+}
+
+func (ns *Namespace) UnmarshalBinary(bin []byte) error {
+	if err := (&ns.NodeHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	buf := bin[ns.headerByteSize:]
+	ns.prefix = PoolRef(btou32(buf))
+	ns.uri = PoolRef(btou32(buf[4:]))
+	return nil
+}
+
+func (ns *Namespace) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 24)
+	b, err := ns.NodeHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	putu32(bin[16:], uint32(ns.prefix))
+	putu32(bin[20:], uint32(ns.uri))
+	return bin, nil
+}
+
+type Element struct {
+	NodeHeader
+	NS             PoolRef
+	Name           PoolRef // name of node if element, otherwise chardata if CDATA
+	AttributeStart uint16  // byte offset where attrs start
+	AttributeSize  uint16  // byte size of attrs
+	AttributeCount uint16  // length of attrs
+	IdIndex        uint16  // Index (1-based) of the "id" attribute. 0 if none.
+	ClassIndex     uint16  // Index (1-based) of the "class" attribute. 0 if none.
+	StyleIndex     uint16  // Index (1-based) of the "style" attribute. 0 if none.
+
+	attrs    []*Attribute
+	Children []*Element
+	end      *ElementEnd
+
+	head, tail *CharData
+}
+
+func (el *Element) UnmarshalBinary(bin []byte) error {
+	if err := (&el.NodeHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	buf := bin[el.headerByteSize:] // 16
+	el.NS = PoolRef(btou32(buf))
+	el.Name = PoolRef(btou32(buf[4:]))
+	el.AttributeStart = btou16(buf[8:])
+	el.AttributeSize = btou16(buf[10:])
+	el.AttributeCount = btou16(buf[12:])
+	el.IdIndex = btou16(buf[14:])
+	el.ClassIndex = btou16(buf[16:])
+	el.StyleIndex = btou16(buf[18:])
+
+	buf = buf[el.AttributeStart:]
+	el.attrs = make([]*Attribute, int(el.AttributeCount))
+	for i := range el.attrs {
+		attr := new(Attribute)
+		if err := attr.UnmarshalBinary(buf); err != nil {
+			return err
+		}
+		el.attrs[i] = attr
+		buf = buf[el.AttributeSize:]
+	}
+
+	return nil
+}
+
+func (el *Element) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 16+20+len(el.attrs)*int(el.AttributeSize))
+	b, err := el.NodeHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	putu32(bin[16:], uint32(el.NS))
+	putu32(bin[20:], uint32(el.Name))
+	putu16(bin[24:], el.AttributeStart)
+	putu16(bin[26:], el.AttributeSize)
+	putu16(bin[28:], el.AttributeCount)
+	putu16(bin[30:], el.IdIndex)
+	putu16(bin[32:], el.ClassIndex)
+	putu16(bin[34:], el.StyleIndex)
+
+	buf := bin[36:]
+	for _, attr := range el.attrs {
+		b, err := attr.MarshalBinary()
+		if err != nil {
+			return nil, err
+		}
+		copy(buf, b)
+		buf = buf[int(el.AttributeSize):]
+	}
+
+	return bin, nil
+}
+
+// ElementEnd marks the end of an element node, either Element or CharData.
+type ElementEnd struct {
+	NodeHeader
+	NS   PoolRef
+	Name PoolRef // name of node if binElement, raw chardata if binCharData
+}
+
+func (el *ElementEnd) UnmarshalBinary(bin []byte) error {
+	(&el.NodeHeader).UnmarshalBinary(bin)
+	buf := bin[el.headerByteSize:]
+	el.NS = PoolRef(btou32(buf))
+	el.Name = PoolRef(btou32(buf[4:]))
+	return nil
+}
+
+func (el *ElementEnd) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 24)
+	b, err := el.NodeHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	putu32(bin[16:], uint32(el.NS))
+	putu32(bin[20:], uint32(el.Name))
+	return bin, nil
+}
+
+type Attribute struct {
+	NS         PoolRef
+	Name       PoolRef
+	RawValue   PoolRef // The original raw string value of this attribute.
+	TypedValue Data    // Processesd typed value of this attribute.
+}
+
+func (attr *Attribute) UnmarshalBinary(bin []byte) error {
+	attr.NS = PoolRef(btou32(bin))
+	attr.Name = PoolRef(btou32(bin[4:]))
+	attr.RawValue = PoolRef(btou32(bin[8:]))
+	return (&attr.TypedValue).UnmarshalBinary(bin[12:])
+}
+
+func (attr *Attribute) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 20)
+	putu32(bin, uint32(attr.NS))
+	putu32(bin[4:], uint32(attr.Name))
+	putu32(bin[8:], uint32(attr.RawValue))
+	b, err := attr.TypedValue.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin[12:], b)
+	return bin, nil
+}
+
+// CharData represents a CDATA node and includes ref to node's text value.
+type CharData struct {
+	NodeHeader
+	RawData   PoolRef // raw character data
+	TypedData Data    // typed value of character data
+}
+
+func (cdt *CharData) UnmarshalBinary(bin []byte) error {
+	if err := (&cdt.NodeHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	buf := bin[cdt.headerByteSize:]
+	cdt.RawData = PoolRef(btou32(buf))
+	return (&cdt.TypedData).UnmarshalBinary(buf[4:])
+}
+
+func (cdt *CharData) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 28)
+	b, err := cdt.NodeHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	putu32(bin[16:], uint32(cdt.RawData))
+	b, err = cdt.TypedData.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin[20:], b)
+	return bin, nil
+}
diff --git a/internal/binres/package.go b/internal/binres/package.go
new file mode 100644
index 0000000..8b967dd
--- /dev/null
+++ b/internal/binres/package.go
@@ -0,0 +1,101 @@
+// 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/pool.go b/internal/binres/pool.go
new file mode 100644
index 0000000..2e2d113
--- /dev/null
+++ b/internal/binres/pool.go
@@ -0,0 +1,276 @@
+// 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 (
+	"fmt"
+	"unicode/utf16"
+)
+
+const (
+	SortedFlag uint32 = 1 << 0
+	UTF8Flag          = 1 << 8
+)
+
+// PoolRef is the i'th string in a pool.
+type PoolRef uint32
+
+// Pool has the following structure marshalled:
+//
+// 	binChunkHeader
+//  StringCount  uint32 // Number of strings in this pool
+//  StyleCount   uint32 // Number of style spans in pool
+//  Flags        uint32 // SortedFlag, UTF8Flag
+//  StringsStart uint32 // Index of string data from header
+//  StylesStart  uint32 // Index of style data from header
+//
+//  StringIndices []uint32 // starting at zero
+//
+//  // UTF16 entries are concatenations of the following:
+//  // [2]byte uint16 string length, exclusive
+//  // [2]byte [optional] low word if high bit of length was set
+//  // [n]byte data
+//  // [2]byte 0x0000 terminator
+//  Strings []uint16
+type Pool struct {
+	chunkHeader
+
+	strings []string
+	styles  []*Span
+	flags   uint32 // SortedFlag, UTF8Flag
+}
+
+func (pl *Pool) IsSorted() bool { return pl.flags&SortedFlag == SortedFlag }
+func (pl *Pool) IsUTF8() bool   { return pl.flags&UTF8Flag == UTF8Flag }
+
+func (pl *Pool) UnmarshalBinary(bin []byte) error {
+	if err := (&pl.chunkHeader).UnmarshalBinary(bin); err != nil {
+		return err
+	}
+	if pl.typ != ResStringPool {
+		return fmt.Errorf("have type %s, want %s", pl.typ, ResStringPool)
+	}
+
+	nstrings := btou32(bin[8:])
+	pl.strings = make([]string, nstrings)
+
+	nstyles := btou32(bin[12:])
+	pl.styles = make([]*Span, nstyles)
+
+	pl.flags = btou32(bin[16:])
+
+	offstrings := btou32(bin[20:])
+	offstyle := btou32(bin[24:])
+
+	hdrlen := 28 // header size is always 28
+	if pl.IsUTF8() {
+		for i := range pl.strings {
+			ii := btou32(bin[hdrlen+i*4:]) // index of string index
+			r := int(offstrings + ii)      // read index of string
+
+			// for char and byte sizes below, if leading bit set,
+			// treat first as 7-bit high word and the next byte as low word.
+
+			// get char size of string
+			cn := uint8(bin[r])
+			rcn := int(cn)
+			r++
+			if cn&(1<<7) != 0 {
+				cn0 := int(cn ^ (1 << 7)) // high word
+				cn1 := int(bin[r])        // low word
+				rcn = cn0*256 + cn1
+				r++
+			}
+
+			// get byte size of string
+			// TODO(d) i've seen at least one case in android.jar resource table that has only
+			// highbit set, effectively making 7-bit highword zero. The reason for this is currently
+			// unknown but would make tests that unmarshal-marshal to match bytes impossible to pass.
+			// The values noted were high-word: 0 (after highbit unset), low-word: 141
+			// I don't recall character count but was around ?78?
+			// The case here may very well be that the size is treated like int8 triggering the use of
+			// two bytes to store size even though the implementation uses uint8.
+			n := uint8(bin[r])
+			r++
+			rn := int(n)
+			if n&(1<<7) != 0 {
+				n0 := int(n ^ (1 << 7)) // high word
+				n1 := int(bin[r])       // low word
+				rn = n0*(1<<8) + n1
+				r++
+			}
+
+			//
+			data := bin[r : r+rn]
+			if x := uint8(bin[r+rn]); x != 0 {
+				return fmt.Errorf("expected zero terminator, got %v for byte size %v char len %v", x, rn, rcn)
+			}
+			pl.strings[i] = string(data)
+		}
+	} else {
+		for i := range pl.strings {
+			ii := btou32(bin[hdrlen+i*4:]) // index of string index
+			r := int(offstrings + ii)      // read index of string
+			n := btou16(bin[r:])           // string length
+			rn := int(n)
+			r += 2
+
+			if n&(1<<15) != 0 { // TODO this is untested
+				n0 := int(n ^ (1 << 15)) // high word
+				n1 := int(btou16(bin[r:]))
+				rn = n0*(1<<16) + n1
+				r += 2
+			}
+
+			data := make([]uint16, int(rn))
+			for i := range data {
+				data[i] = btou16(bin[r+(2*i):])
+			}
+
+			r += int(n * 2)
+			if x := btou16(bin[r:]); x != 0 {
+				return fmt.Errorf("expected zero terminator, got 0x%04X\n%s", x)
+			}
+
+			pl.strings[i] = string(utf16.Decode(data))
+		}
+	}
+
+	// TODO
+	_ = offstyle
+	// styii := hdrlen + int(nstrings*4)
+	// for i := range pl.styles {
+	// ii := btou32(bin[styii+i*4:])
+	// r := int(offstyle + ii)
+	// spn := new(binSpan)
+	// spn.UnmarshalBinary(bin[r:])
+	// pl.styles[i] = spn
+	// }
+
+	return nil
+}
+
+func (pl *Pool) MarshalBinary() ([]byte, error) {
+	if pl.IsUTF8() {
+		return nil, fmt.Errorf("encode utf8 not supported")
+	}
+
+	var (
+		hdrlen = 28
+		// indices of string indices
+		iis    = make([]uint32, len(pl.strings))
+		iislen = len(iis) * 4
+		// utf16 encoded strings concatenated together
+		strs []uint16
+	)
+	for i, x := range pl.strings {
+		if len(x)>>16 > 0 {
+			panic(fmt.Errorf("string lengths over 1<<15 not yet supported, got len %d", len(x)))
+		}
+		p := utf16.Encode([]rune(x))
+		if len(p) == 0 {
+			strs = append(strs, 0x0000, 0x0000)
+		} else {
+			strs = append(strs, uint16(len(p))) // string length (implicitly includes zero terminator to follow)
+			strs = append(strs, p...)
+			strs = append(strs, 0) // zero terminated
+		}
+		// indices start at zero
+		if i+1 != len(iis) {
+			iis[i+1] = uint32(len(strs) * 2) // utf16 byte index
+		}
+	}
+
+	// check strings is 4-byte aligned, pad with zeros if not.
+	for x := (len(strs) * 2) % 4; x != 0; x -= 2 {
+		strs = append(strs, 0x0000)
+	}
+
+	strslen := len(strs) * 2
+
+	hdr := chunkHeader{
+		typ:            ResStringPool,
+		headerByteSize: 28,
+		byteSize:       uint32(28 + iislen + strslen),
+	}
+
+	bin := make([]byte, hdr.byteSize)
+
+	hdrbin, err := hdr.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, hdrbin)
+
+	putu32(bin[8:], uint32(len(pl.strings)))
+	putu32(bin[12:], uint32(len(pl.styles)))
+	putu32(bin[16:], pl.flags)
+	putu32(bin[20:], uint32(hdrlen+iislen))
+	putu32(bin[24:], 0) // index of styles start, is 0 when styles length is 0
+
+	buf := bin[28:]
+	for _, x := range iis {
+		putu32(buf, x)
+		buf = buf[4:]
+	}
+	for _, x := range strs {
+		putu16(buf, x)
+		buf = buf[2:]
+	}
+
+	if len(buf) != 0 {
+		panic(fmt.Errorf("failed to fill allocated buffer, %v bytes left over", len(buf)))
+	}
+
+	return bin, nil
+}
+
+type Span struct {
+	name PoolRef
+
+	firstChar, lastChar uint32
+}
+
+func (spn *Span) UnmarshalBinary(bin []byte) error {
+	const end = 0xFFFFFFFF
+	spn.name = PoolRef(btou32(bin))
+	if spn.name == end {
+		return nil
+	}
+	spn.firstChar = btou32(bin[4:])
+	spn.lastChar = btou32(bin[8:])
+	return nil
+}
+
+// Map contains a uint32 slice mapping strings in the string
+// pool back to resource identifiers. The i'th element of the slice
+// is also the same i'th element of the string pool.
+type Map struct {
+	chunkHeader
+	rs []uint32
+}
+
+func (m *Map) UnmarshalBinary(bin []byte) error {
+	(&m.chunkHeader).UnmarshalBinary(bin)
+	buf := bin[m.headerByteSize:m.byteSize]
+	m.rs = make([]uint32, len(buf)/4)
+	for i := range m.rs {
+		m.rs[i] = btou32(buf[i*4:])
+	}
+	return nil
+}
+
+func (m *Map) MarshalBinary() ([]byte, error) {
+	bin := make([]byte, 8+len(m.rs)*4)
+	b, err := m.chunkHeader.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	copy(bin, b)
+	for i, r := range m.rs {
+		putu32(bin[8+i*4:], r)
+	}
+	return bin, nil
+}
diff --git a/internal/binres/testdata/bootstrap.bin b/internal/binres/testdata/bootstrap.bin
new file mode 100644
index 0000000..ada309b
--- /dev/null
+++ b/internal/binres/testdata/bootstrap.bin
Binary files differ
diff --git a/internal/binres/testdata/bootstrap.xml b/internal/binres/testdata/bootstrap.xml
new file mode 100644
index 0000000..e50dbe6
--- /dev/null
+++ b/internal/binres/testdata/bootstrap.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2014 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.
+-->
+<manifest
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	package="com.zentus.balloon"
+	android:versionCode="42"
+	android:versionName=""
+	xmlns:tools="http://schemas.android.com/tools">
+
+	<uses-sdk android:minSdkVersion="9" />
+	<application
+			android:label="Balloon世界"
+			android:hasCode="false"
+			android:debuggable="true"
+			tools:strict="label">
+	<activity android:name="android.app.NativeActivity"
+		android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+		android:label="Balloon"
+		android:screenOrientation="portrait"
+		android:configChanges="orientation|keyboardHidden">
+		<meta-data android:name="android.app.lib_name" android:value="balloon" />
+		<intent-filter>
+			here is some text
+			<action android:name="android.intent.action.MAIN" />
+			<category android:name="android.intent.category.LAUNCHER" />
+		</intent-filter>
+	</activity>
+	</application>
+</manifest>
diff --git a/internal/binres/testdata/gen.sh b/internal/binres/testdata/gen.sh
new file mode 100755
index 0000000..b92daf2
--- /dev/null
+++ b/internal/binres/testdata/gen.sh
@@ -0,0 +1,15 @@
+#! /usr/bin/sh
+
+# version of build-tools tests run against
+AAPT=${ANDROID_HOME}/build-tools/23.0.1/aapt
+
+# minimum version of android api for resource identifiers supported
+APIJAR=${ANDROID_HOME}/platforms/android-10/android.jar
+
+for f in *.xml; do
+	cp "$f" AndroidManifest.xml
+	"$AAPT" p -M AndroidManifest.xml -I "$APIJAR" -F tmp.apk
+	unzip -qq -o tmp.apk
+	mv AndroidManifest.xml "${f:0:-3}bin"
+	rm tmp.apk
+done