1
0
Fork 0

Adding upstream version 0.0~git20250520.a1d9079+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 19:46:29 +02:00
parent 590ac7ff5f
commit 20149b7f3a
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
456 changed files with 70406 additions and 0 deletions

9
internal/binres/arsc.go Normal file

File diff suppressed because one or more lines are too long

963
internal/binres/binres.go Normal file
View file

@ -0,0 +1,963 @@
// 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 go run genarsc.go
//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/libs/androidfw/include/androidfw/ResourceTypes.h
// https://justanapplication.wordpress.com/2011/09/13/ (a series of articles, increment date)
package binres
import (
"encoding"
"encoding/binary"
"encoding/xml"
"fmt"
"io"
"sort"
"strconv"
"strings"
"unicode"
)
func errWrongType(have ResType, want ...ResType) error {
return fmt.Errorf("wrong resource type %s, want one of %v", have, want)
}
type ResType uint16
func (t ResType) IsSupported() bool {
return t != ResNull
}
// 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
ResTableLibrary ResType = 0x0203
ResTableOverlayable ResType = 0x0204
ResTableOverlayablePolicy ResType = 0x0205
ResTableStagedAlias ResType = 0x0206
)
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.
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:])
if len(bin) < int(hdr.byteSize) {
return fmt.Errorf("too few bytes to unmarshal chunk body, have %v, need at-least %v", len(bin), hdr.byteSize)
}
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 binary
stack []*Element
}
// RawValueByName returns the original raw string value of first matching element attribute, or error if not exists.
// Given <manifest package="VAL" ...> then RawValueByName("manifest", xml.Name{Local: "package"}) returns "VAL".
func (bx *XML) RawValueByName(elname string, attrname xml.Name) (string, error) {
elref, err := bx.Pool.RefByName(elname)
if err != nil {
return "", err
}
nref, err := bx.Pool.RefByName(attrname.Local)
if err != nil {
return "", err
}
nsref := PoolRef(NoEntry)
if attrname.Space != "" {
nsref, err = bx.Pool.RefByName(attrname.Space)
if err != nil {
return "", err
}
}
for el := range bx.iterElements() {
if el.Name == elref {
for _, attr := range el.attrs {
// TODO enforce TypedValue DataString constraint?
if nsref == attr.NS && nref == attr.Name {
return bx.Pool.strings[int(attr.RawValue)], nil
}
}
}
}
return "", fmt.Errorf("no matching element %q for attribute %+v found", elname, attrname)
}
const (
androidSchema = "http://schemas.android.com/apk/res/android"
toolsSchema = "http://schemas.android.com/tools"
)
// skipSynthesize is set true for tests to avoid synthesis of additional nodes and attributes.
var skipSynthesize bool
// UnmarshalXML decodes an AndroidManifest.xml document returning type XML
// containing decoded resources.
func UnmarshalXML(r io.Reader, withIcon bool) (*XML, error) {
tbl, err := OpenTable()
if err != nil {
return nil, err
}
lr := &lineReader{r: r}
dec := xml.NewDecoder(lr)
bx := new(XML)
// temporary pool to resolve real poolref later
pool := new(Pool)
type ltoken struct {
xml.Token
line int
}
var q []ltoken
for {
line := lr.line(dec.InputOffset())
tkn, err := dec.Token()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
tkn = xml.CopyToken(tkn)
switch tkn := tkn.(type) {
case xml.StartElement:
switch tkn.Name.Local {
default:
q = append(q, ltoken{tkn, line})
case "uses-sdk":
return nil, fmt.Errorf("manual declaration of uses-sdk in AndroidManifest.xml not supported")
case "manifest":
// synthesize additional attributes and nodes for use during encode.
tkn.Attr = append(tkn.Attr,
xml.Attr{
Name: xml.Name{
Space: "",
Local: "platformBuildVersionCode",
},
Value: "16",
},
xml.Attr{
Name: xml.Name{
Space: "",
Local: "platformBuildVersionName",
},
Value: "4.1.2-1425332",
})
q = append(q, ltoken{tkn, line})
if !skipSynthesize {
s := xml.StartElement{
Name: xml.Name{
Space: "",
Local: "uses-sdk",
},
Attr: []xml.Attr{
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "minSdkVersion",
},
Value: fmt.Sprintf("%v", MinSDK),
},
},
}
e := xml.EndElement{Name: xml.Name{Local: "uses-sdk"}}
q = append(q, ltoken{s, line}, ltoken{e, line})
}
case "application":
if !skipSynthesize {
for _, attr := range tkn.Attr {
if attr.Name.Space == androidSchema && attr.Name.Local == "icon" {
return nil, fmt.Errorf("manual declaration of android:icon in AndroidManifest.xml not supported")
}
}
if withIcon {
tkn.Attr = append(tkn.Attr,
xml.Attr{
Name: xml.Name{
Space: androidSchema,
Local: "icon",
},
Value: "@mipmap/icon",
})
}
}
q = append(q, ltoken{tkn, line})
}
default:
q = append(q, ltoken{tkn, line})
}
}
for _, ltkn := range q {
tkn, line := ltkn.Token, ltkn.line
switch tkn := tkn.(type) {
case xml.StartElement:
el := &Element{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: 0xFFFFFFFF,
},
NS: NoEntry,
Name: pool.ref(tkn.Name.Local),
}
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)
for _, attr := range tkn.Attr {
if (attr.Name.Space == "xmlns" && attr.Name.Local == "tools") || attr.Name.Space == toolsSchema {
continue // TODO can tbl be queried for schemas to determine validity instead?
}
if attr.Name.Space == "xmlns" && attr.Name.Local == "android" {
if bx.Namespace != nil {
return nil, fmt.Errorf("multiple declarations of xmlns:android encountered")
}
bx.Namespace = &Namespace{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
prefix: 0,
uri: 0,
}
continue
}
nattr := &Attribute{
NS: pool.ref(attr.Name.Space),
Name: pool.ref(attr.Name.Local),
RawValue: NoEntry,
}
el.attrs = append(el.attrs, nattr)
if attr.Name.Space == "" {
nattr.NS = NoEntry
// TODO it's unclear how to query these
switch attr.Name.Local {
case "platformBuildVersionCode":
nattr.TypedValue.Type = DataIntDec
i, err := strconv.Atoi(attr.Value)
if err != nil {
return nil, err
}
nattr.TypedValue.Value = uint32(i)
default: // "package", "platformBuildVersionName", and any invalid
nattr.RawValue = pool.ref(attr.Value)
nattr.TypedValue.Type = DataString
}
} else {
// get type spec and value data type
ref, err := tbl.RefByName("attr/" + attr.Name.Local)
if err != nil {
return nil, err
}
nt, err := ref.Resolve(tbl)
if err != nil {
return nil, err
}
if len(nt.values) == 0 {
panic("encountered empty values slice")
}
if len(nt.values) == 1 {
val := nt.values[0]
if val.data.Type != DataIntDec {
panic("TODO only know how to handle DataIntDec type here")
}
t := DataType(val.data.Value)
switch t {
case DataString, DataAttribute, DataType(0x3e):
// TODO identify 0x3e, in bootstrap.xml this is the native lib name
nattr.RawValue = pool.ref(attr.Value)
nattr.TypedValue.Type = DataString
nattr.TypedValue.Value = uint32(nattr.RawValue)
case DataIntBool, DataType(0x08):
nattr.TypedValue.Type = DataIntBool
switch attr.Value {
case "true":
nattr.TypedValue.Value = 0xFFFFFFFF
case "false":
nattr.TypedValue.Value = 0
default:
return nil, fmt.Errorf("invalid bool value %q", attr.Value)
}
case DataIntDec, DataFloat, DataFraction:
// TODO DataFraction needs it's own case statement. minSdkVersion identifies as DataFraction
// but has accepted input in the past such as android:minSdkVersion="L"
// Other use-cases for DataFraction are currently unknown as applicable to manifest generation
// but this provides minimum support for writing out minSdkVersion="15" correctly.
nattr.TypedValue.Type = DataIntDec
i, err := strconv.Atoi(attr.Value)
if err != nil {
return nil, err
}
nattr.TypedValue.Value = uint32(i)
case DataReference:
nattr.TypedValue.Type = DataReference
dref, err := tbl.RefByName(attr.Value)
if err != nil {
if strings.HasPrefix(attr.Value, "@mipmap") {
// firstDrawableId is a TableRef matching first entry of mipmap spec initialized by NewMipmapTable.
// 7f is default package, 02 is mipmap spec, 0000 is first entry; e.g. R.drawable.icon
// TODO resource table should generate ids as required.
const firstDrawableId = 0x7f020000
nattr.TypedValue.Value = firstDrawableId
continue
}
return nil, err
}
nattr.TypedValue.Value = uint32(dref)
default:
return nil, fmt.Errorf("unhandled data type %0#2x: %s", uint8(t), t)
}
} else {
// 0x01000000 is an unknown ref that doesn't point to anything, typically
// located at the start of entry value lists, peek at last value to determine type.
t := nt.values[len(nt.values)-1].data.Type
switch t {
case DataIntDec:
for _, val := range nt.values {
if val.name == 0x01000000 {
continue
}
nr, err := val.name.Resolve(tbl)
if err != nil {
return nil, err
}
if attr.Value == nr.key.Resolve(tbl.pkgs[0].keyPool) { // TODO hard-coded pkg ref
nattr.TypedValue = *val.data
break
}
}
case DataIntHex:
nattr.TypedValue.Type = t
for _, x := range strings.Split(attr.Value, "|") {
for _, val := range nt.values {
if val.name == 0x01000000 {
continue
}
nr, err := val.name.Resolve(tbl)
if err != nil {
return nil, err
}
if x == nr.key.Resolve(tbl.pkgs[0].keyPool) { // TODO hard-coded pkg ref
nattr.TypedValue.Value |= val.data.Value
break
}
}
}
default:
return nil, fmt.Errorf("unhandled data type for configuration %0#2x: %s", uint8(t), t)
}
}
}
}
case xml.CharData:
if s := poolTrim(string(tkn)); s != "" {
cdt := &CharData{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
RawData: pool.ref(s),
}
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")
}
}
case xml.EndElement:
if tkn.Name.Local == "manifest" {
bx.Namespace.end = &Namespace{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
prefix: 0,
uri: 0,
}
}
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 = &ElementEnd{
NodeHeader: NodeHeader{
LineNumber: uint32(line),
Comment: NoEntry,
},
NS: el.NS,
Name: el.Name,
}
case xml.Comment, xml.ProcInst:
// discard
default:
panic(fmt.Errorf("unhandled token type: %T %+v", tkn, tkn))
}
}
// pools appear to be sorted as follows:
// * attribute names prefixed with android:
// * "android", [schema-url], [empty-string]
// * for each node:
// * attribute names with no prefix
// * node name
// * attribute value if data type of name is DataString, DataAttribute, or 0x3e (an unknown)
bx.Pool = new(Pool)
var arecurse func(*Element)
arecurse = func(el *Element) {
for _, attr := range el.attrs {
if attr.NS == NoEntry {
continue
}
if attr.NS.Resolve(pool) == androidSchema {
bx.Pool.strings = append(bx.Pool.strings, attr.Name.Resolve(pool))
}
}
for _, child := range el.Children {
arecurse(child)
}
}
for _, el := range bx.Children {
arecurse(el)
}
// TODO encoding/xml does not enforce namespace prefix and manifest encoding in aapt
// appears to ignore all other prefixes. Inserting this manually is not strictly correct
// for the general case, but the effort to do otherwise currently offers nothing.
bx.Pool.strings = append(bx.Pool.strings, "android", androidSchema)
// there always appears to be an empty string located after schema, even if one is
// not present in manifest.
bx.Pool.strings = append(bx.Pool.strings, "")
var brecurse func(*Element)
brecurse = func(el *Element) {
for _, attr := range el.attrs {
if attr.NS == NoEntry {
bx.Pool.strings = append(bx.Pool.strings, attr.Name.Resolve(pool))
}
}
bx.Pool.strings = append(bx.Pool.strings, el.Name.Resolve(pool))
for _, attr := range el.attrs {
if attr.RawValue != NoEntry {
bx.Pool.strings = append(bx.Pool.strings, attr.RawValue.Resolve(pool))
} else if attr.NS == NoEntry {
bx.Pool.strings = append(bx.Pool.strings, fmt.Sprintf("%+v", attr.TypedValue.Value))
}
}
if el.head != nil {
bx.Pool.strings = append(bx.Pool.strings, el.head.RawData.Resolve(pool))
}
if el.tail != nil {
bx.Pool.strings = append(bx.Pool.strings, el.tail.RawData.Resolve(pool))
}
for _, child := range el.Children {
brecurse(child)
}
}
for _, el := range bx.Children {
brecurse(el)
}
// 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.
bx.Pool.strings = asSet(bx.Pool.strings)
// TODO consider cases of multiple declarations of the same attr name that should return error
// before ever reaching this point.
bx.Map = new(Map)
for _, s := range bx.Pool.strings {
ref, err := tbl.RefByName("attr/" + s)
if err != nil {
break // break after first non-ref as all strings after are also non-refs.
}
bx.Map.rs = append(bx.Map.rs, ref)
}
// resolve tmp pool refs to final pool refs
// TODO drop this in favor of sort directly on Table
var resolve func(el *Element)
resolve = func(el *Element) {
if el.NS != NoEntry {
el.NS = bx.Pool.ref(el.NS.Resolve(pool))
el.end.NS = el.NS
}
el.Name = bx.Pool.ref(el.Name.Resolve(pool))
el.end.Name = el.Name
for _, attr := range el.attrs {
if attr.NS != NoEntry {
attr.NS = bx.Pool.ref(attr.NS.Resolve(pool))
}
attr.Name = bx.Pool.ref(attr.Name.Resolve(pool))
if attr.RawValue != NoEntry {
attr.RawValue = bx.Pool.ref(attr.RawValue.Resolve(pool))
if attr.TypedValue.Type == DataString {
attr.TypedValue.Value = uint32(attr.RawValue)
}
}
}
for _, child := range el.Children {
resolve(child)
}
}
for _, el := range bx.Children {
resolve(el)
}
var asort func(*Element)
asort = func(el *Element) {
sort.Sort(byType(el.attrs))
sort.Sort(byNamespace(el.attrs))
sort.Sort(byName(el.attrs))
for _, child := range el.Children {
asort(child)
}
}
for _, el := range bx.Children {
asort(el)
}
for i, s := range bx.Pool.strings {
switch s {
case androidSchema:
bx.Namespace.uri = PoolRef(i)
bx.Namespace.end.uri = PoolRef(i)
case "android":
bx.Namespace.prefix = PoolRef(i)
bx.Namespace.end.prefix = PoolRef(i)
}
}
return bx, nil
}
// UnmarshalBinary decodes all resource chunks in buf returning any error encountered.
func (bx *XML) UnmarshalBinary(buf []byte) error {
if err := (&bx.chunkHeader).UnmarshalBinary(buf); err != nil {
return err
}
buf = buf[8:]
for len(buf) > 0 {
k, err := bx.unmarshalBinaryKind(buf)
if err != nil {
return err
}
buf = buf[k.size():]
}
return nil
}
// unmarshalBinaryKind decodes and stores the first resource chunk of bin.
// It returns the unmarshaler interface and any error encountered.
// If k.size() < len(bin), subsequent chunks can be decoded at bin[k.size():].
func (bx *XML) unmarshalBinaryKind(bin []byte) (k unmarshaler, err error) {
k, err = bx.kind(ResType(btou16(bin)))
if err != nil {
return nil, err
}
if err = k.UnmarshalBinary(bin); err != nil {
return nil, err
}
return k, 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 assure correctness
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) {
bx.typ = ResXML
bx.headerByteSize = 8
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...)
putu32(bin[4:], uint32(len(bin)))
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)
}
}
// asSet returns a set from a slice of strings.
func asSet(xs []string) []string {
m := make(map[string]bool)
fo := xs[:0]
for _, x := range xs {
if !m[x] {
m[x] = true
fo = append(fo, x)
}
}
return fo
}
// poolTrim trims all but immediately surrounding space.
// \n\t\tfoobar\n\t\t becomes \tfoobar\n
func poolTrim(s string) string {
var start, end int
for i, r := range s {
if !unicode.IsSpace(r) {
if i != 0 {
start = i - 1 // preserve preceding space
}
break
}
}
for i := len(s) - 1; i >= 0; i-- {
r := rune(s[i])
if !unicode.IsSpace(r) {
if i != len(s)-1 {
end = i + 2
}
break
}
}
if start == 0 && end == 0 {
return "" // every char was a space
}
return s[start:end]
}
// byNamespace sorts attributes based on string pool position of namespace.
// Given that "android" always proceeds "" in the pool, this results in the
// correct ordering of attributes.
type byNamespace []*Attribute
func (a byNamespace) Len() int { return len(a) }
func (a byNamespace) Less(i, j int) bool {
return a[i].NS < a[j].NS
}
func (a byNamespace) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// byType sorts attributes by the uint8 value of the type.
type byType []*Attribute
func (a byType) Len() int { return len(a) }
func (a byType) Less(i, j int) bool {
return a[i].TypedValue.Type < a[j].TypedValue.Type
}
func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// byName sorts attributes that have matching types based on string pool position of name.
type byName []*Attribute
func (a byName) Len() int { return len(a) }
func (a byName) Less(i, j int) bool {
return (a[i].TypedValue.Type == DataString || a[i].TypedValue.Type == DataIntDec) &&
(a[j].TypedValue.Type == DataString || a[j].TypedValue.Type == DataIntDec) &&
a[i].Name < a[j].Name
}
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type lineReader struct {
off int64
lines []int64
r io.Reader
}
func (r *lineReader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
for i := 0; i < n; i++ {
if p[i] == '\n' {
r.lines = append(r.lines, r.off+int64(i))
}
}
r.off += int64(n)
return n, err
}
func (r *lineReader) line(pos int64) int {
return sort.Search(len(r.lines), func(i int) bool {
return pos < r.lines[i]
}) + 1
}

View file

@ -0,0 +1,62 @@
// Code generated by "stringer -output binres_string.go -type ResType,DataType"; DO NOT EDIT.
package binres
import "strconv"
const (
_ResType_name_0 = "ResNullResStringPoolResTableResXML"
_ResType_name_1 = "ResXMLStartNamespaceResXMLEndNamespaceResXMLStartElementResXMLEndElementResXMLCharData"
_ResType_name_2 = "ResXMLResourceMap"
_ResType_name_3 = "ResTablePackageResTableTypeResTableTypeSpecResTableLibrary"
)
var (
_ResType_index_0 = [...]uint8{0, 7, 20, 28, 34}
_ResType_index_1 = [...]uint8{0, 20, 38, 56, 72, 86}
_ResType_index_3 = [...]uint8{0, 15, 27, 43, 58}
)
func (i ResType) String() string {
switch {
case 0 <= i && i <= 3:
return _ResType_name_0[_ResType_index_0[i]:_ResType_index_0[i+1]]
case 256 <= i && i <= 260:
i -= 256
return _ResType_name_1[_ResType_index_1[i]:_ResType_index_1[i+1]]
case i == 384:
return _ResType_name_2
case 512 <= i && i <= 515:
i -= 512
return _ResType_name_3[_ResType_index_3[i]:_ResType_index_3[i+1]]
default:
return "ResType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
const (
_DataType_name_0 = "DataNullDataReferenceDataAttributeDataStringDataFloatDataDimensionDataFractionDataDynamicReference"
_DataType_name_1 = "DataIntDecDataIntHexDataIntBool"
_DataType_name_2 = "DataIntColorARGB8DataIntColorRGB8DataIntColorARGB4DataIntColorRGB4"
)
var (
_DataType_index_0 = [...]uint8{0, 8, 21, 34, 44, 53, 66, 78, 98}
_DataType_index_1 = [...]uint8{0, 10, 20, 31}
_DataType_index_2 = [...]uint8{0, 17, 33, 50, 66}
)
func (i DataType) String() string {
switch {
case 0 <= i && i <= 7:
return _DataType_name_0[_DataType_index_0[i]:_DataType_index_0[i+1]]
case 16 <= i && i <= 18:
i -= 16
return _DataType_name_1[_DataType_index_1[i]:_DataType_index_1[i+1]]
case 28 <= i && i <= 31:
i -= 28
return _DataType_name_2[_DataType_index_2[i]:_DataType_index_2[i+1]]
default:
return "DataType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

View file

@ -0,0 +1,663 @@
// 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 (
"bytes"
"encoding"
"encoding/xml"
"errors"
"fmt"
"log"
"math"
"os"
"sort"
"strings"
"testing"
"golang.org/x/mobile/internal/sdkpath"
)
func init() {
skipSynthesize = true
}
func printrecurse(t *testing.T, pl *Pool, el *Element, ws string) {
t.Logf("%s+elem:ns(%v) name(%s)", ws, el.NS, el.Name.Resolve(pl))
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 != NoEntry {
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 == 0xFFFFFFFF)
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.Logf("\n")
for _, e := range el.Children {
printrecurse(t, pl, e, ws+" ")
}
}
func compareBytes(a, b []byte) error {
if bytes.Equal(a, b) {
return nil
}
buf := new(bytes.Buffer)
x, y := len(a), len(b)
if x != y {
fmt.Fprintf(buf, "byte length does not match, have %v, want %v\n", y, x)
}
if x > y {
x, y = y, x
}
mismatch := false
for i := 0; i < x; i++ {
if mismatch = a[i] != b[i]; mismatch {
fmt.Fprintf(buf, "first byte mismatch at %v\n", i)
break
}
}
if mismatch {
// print out a reasonable amount of data to help identify issues
truncate := x > 3300
if truncate {
x = 3300
}
buf.WriteString(" HAVE WANT\n")
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
}
notequal := ""
if !bytes.Equal(b[i:i+he], a[i:i+we]) {
notequal = "***"
}
fmt.Fprintf(buf, "%3v | % X % X %s\n", i, b[i:i+he], a[i:i+we], notequal)
}
if truncate {
fmt.Fprint(buf, "... output truncated.\n")
}
}
return errors.New(buf.String())
}
func TestBootstrap(t *testing.T) {
bin, err := os.ReadFile("testdata/bootstrap.bin")
if err != nil {
log.Fatal(err)
}
// unmarshal binary xml and store byte indices of decoded resources.
debugIndices := make(map[encoding.BinaryMarshaler]int)
trackUnmarshal := func(buf []byte) (*XML, error) {
bx := new(XML)
if err := (&bx.chunkHeader).UnmarshalBinary(buf); err != nil {
return nil, err
}
buf = buf[8:]
debugIndex := 8
for len(buf) > 0 {
k, err := bx.unmarshalBinaryKind(buf)
if err != nil {
return nil, err
}
debugIndices[k.(encoding.BinaryMarshaler)] = debugIndex
debugIndex += k.size()
buf = buf[k.size():]
}
return bx, nil
}
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, err := trackUnmarshal(bin)
if 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 TestEncode(t *testing.T) {
f, err := os.Open("testdata/bootstrap.xml")
if err != nil {
t.Fatal(err)
}
bx, err := UnmarshalXML(f, false)
if err != nil {
t.Fatal(err)
}
bin, err := os.ReadFile("testdata/bootstrap.bin")
if err != nil {
log.Fatal(err)
}
bxml := new(XML)
if err := bxml.UnmarshalBinary(bin); err != nil {
t.Fatal(err)
}
if err := compareStrings(t, bxml.Pool.strings, bx.Pool.strings); err != nil {
t.Error(err)
}
if err := compareUint32s(t, rtou(bxml.Map.rs), rtou(bx.Map.rs)); err != nil {
t.Error(err)
}
if err := compareNamespaces(bx.Namespace, bxml.Namespace); err != nil {
t.Error(err)
}
if err := compareElements(bx, bxml); err != nil {
t.Error(err)
}
// Current output byte-for-byte of pkg binres is close, but not exact, to output of aapt.
// The current exceptions to this are as follows:
// * sort order of certain attributes
// * typed value of minSdkVersion
// The below check will produce an error, listing differences in the byte output of each.
// have, err := bx.MarshalBinary()
// if err != nil {
// t.Fatal(err)
// }
// if err := compareBytes(bin, have); err != nil {
// t.Fatal(err)
// }
}
func TestRawValueByName(t *testing.T) {
f, err := os.Open("testdata/bootstrap.xml")
if err != nil {
t.Fatal(err)
}
bx, err := UnmarshalXML(f, false)
if err != nil {
t.Fatal(err)
}
pkgname, err := bx.RawValueByName("manifest", xml.Name{Local: "package"})
if want := "com.zentus.balloon"; err != nil || pkgname != want {
t.Fatalf("have (%q, %v), want (%q, nil)", pkgname, err, want)
}
}
type byAttrName []*Attribute
func (a byAttrName) Len() int { return len(a) }
func (a byAttrName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byAttrName) Less(i, j int) bool { return a[i].Name < a[j].Name }
func compareElements(have, want *XML) error {
h, w := have.iterElements(), want.iterElements()
buf := new(bytes.Buffer)
for {
a, b := <-h, <-w
if a == nil || b == nil {
break
}
if a.Name.Resolve(have.Pool) == "uses-sdk" {
a = <-h // discard uses-sdk token from tests since it's synthesized internally
}
if a.NS != b.NS ||
a.Name != b.Name {
return fmt.Errorf("elements don't match, have %+v, want %+v", a, b)
}
if a.end.NS != b.end.NS ||
a.end.Name != b.end.Name {
return fmt.Errorf("element ends don't match, have %+v, want %+v", a.end, b.end)
}
if len(a.attrs) != len(b.attrs) {
return fmt.Errorf("element attribute lengths don't match, have %v, want %v", len(a.attrs), len(b.attrs))
}
// discards order of aapt and binres as some sorting details of apt have eluded this package but do not
// affect final output from functioning correctly
sort.Sort(byAttrName(a.attrs))
sort.Sort(byAttrName(b.attrs))
for i, attr := range a.attrs {
bttr := b.attrs[i]
if attr.NS != bttr.NS ||
attr.Name != bttr.Name ||
attr.RawValue != bttr.RawValue ||
attr.TypedValue.Type != bttr.TypedValue.Type ||
attr.TypedValue.Value != bttr.TypedValue.Value {
// single exception to check for minSdkVersion which has peculiar output from aapt
// but following same format of all other like-types appears to work correctly.
// BUG(dskinner) this check is brittle as it will skip over any attribute in
// bootstrap.xml that has value == MinSDK.
if attr.TypedValue.Value == MinSDK {
continue
}
fmt.Fprintf(buf, "attrs don't match\nhave: %+v\nwant: %+v\n", attr, bttr)
}
}
if buf.Len() > 0 {
buf.WriteString("-------------\n")
}
}
if buf.Len() > 0 {
return errors.New(buf.String())
}
return nil
}
func compareNamespaces(have, want *Namespace) error {
if have == nil || want == nil ||
have.LineNumber != want.LineNumber ||
have.Comment != want.Comment ||
have.prefix != want.prefix ||
have.uri != want.uri {
return fmt.Errorf("namespaces don't match, have %+v, want %+v", have, want)
}
if have.end != nil || want.end != nil {
return compareNamespaces(have.end, want.end)
}
return nil
}
func rtou(a []TableRef) (b []uint32) {
for _, x := range a {
b = append(b, uint32(x))
}
return
}
func compareUint32s(t *testing.T, a, b []uint32) error {
var err error
if len(a) != len(b) {
err = fmt.Errorf("lengths do not match")
}
n := len(a)
if n < len(b) {
n = len(b)
}
var buf bytes.Buffer
buf.WriteString("a.Map.rs b.Map.rs\n")
for i := 0; i < n; i++ {
var c, d string
if i < len(a) {
c = fmt.Sprintf("%0#8x ", a[i])
} else {
c = "__________ "
}
if i < len(b) {
d = fmt.Sprintf("%0#8x ", b[i])
} else {
d = "__________ "
}
if err == nil && c != d {
err = fmt.Errorf("has missing/incorrect values")
}
buf.WriteString(c + " " + d + "\n")
}
if err != nil {
err = fmt.Errorf("%s\n%s", err, buf.String())
}
return err
}
func compareStrings(t *testing.T, a, b []string) error {
var err error
if len(a) != len(b) {
err = fmt.Errorf("lengths do not match")
}
buf := new(bytes.Buffer)
for i, x := range a {
v := "__"
for j, y := range b {
if x == y {
v = fmt.Sprintf("%2v", j)
break
}
}
if err == nil && v == "__" {
if !strings.HasPrefix(x, "4.1.") {
// as of the time of this writing, the current version of build tools being targeted
// reports 4.1.2-1425332.
//
// TODO this check has the potential to hide real errors but can be fixed once more
// of the xml document is unmarshalled and XML can be queried to assure this is related
// to platformBuildVersionName.
err = fmt.Errorf("has missing/incorrect values")
}
}
fmt.Fprintf(buf, "Pool(%2v, %s) %q\n", i, v, x)
}
contains := func(xs []string, a string) bool {
for _, x := range xs {
if x == a {
return true
}
}
return false
}
if err != nil {
buf.WriteString("\n## only in var a\n")
for i, x := range a {
if !contains(b, x) {
fmt.Fprintf(buf, "Pool(%2v) %q\n", i, x)
}
}
buf.WriteString("\n## only in var b\n")
for i, x := range b {
if !contains(a, x) {
fmt.Fprintf(buf, "Pool(%2v) %q\n", i, x)
}
}
}
if err != nil {
err = fmt.Errorf("%s\n%s", err, buf.String())
}
return err
}
func TestOpenTable(t *testing.T) {
if _, err := sdkpath.AndroidHome(); err != nil {
t.Skipf("Could not locate Android SDK: %v", err)
}
tbl, err := OpenTable()
if err != nil {
t.Fatal(err)
}
if len(tbl.pkgs) == 0 {
t.Fatal("failed to decode any resource packages")
}
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
}
}
}
}
}
func TestTableRefByName(t *testing.T) {
checkResources(t)
tbl, err := OpenSDKTable()
if err != nil {
t.Fatal(err)
}
if len(tbl.pkgs) == 0 {
t.Fatal("failed to decode any resource packages")
}
ref, err := tbl.RefByName("@android:style/Theme.NoTitleBar.Fullscreen")
if err != nil {
t.Fatal(err)
}
if want := uint32(0x01030007); uint32(ref) != want {
t.Fatalf("RefByName does not match expected result, have %0#8x, want %0#8x", ref, want)
}
}
func TestTableMarshal(t *testing.T) {
checkResources(t)
tbl, err := OpenSDKTable()
if err != nil {
t.Fatal(err)
}
bin, err := tbl.MarshalBinary()
if err != nil {
t.Fatal(err)
}
xtbl := new(Table)
if err := xtbl.UnmarshalBinary(bin); err != nil {
t.Fatal(err)
}
if len(tbl.pool.strings) != len(xtbl.pool.strings) {
t.Fatal("tbl.pool lengths don't match")
}
if len(tbl.pkgs) != len(xtbl.pkgs) {
t.Fatal("tbl.pkgs lengths don't match")
}
pkg, xpkg := tbl.pkgs[0], xtbl.pkgs[0]
if err := compareStrings(t, pkg.typePool.strings, xpkg.typePool.strings); err != nil {
t.Fatal(err)
}
if err := compareStrings(t, pkg.keyPool.strings, xpkg.keyPool.strings); err != nil {
t.Fatal(err)
}
if len(pkg.specs) != len(xpkg.specs) {
t.Fatal("pkg.specs lengths don't match")
}
for i, spec := range pkg.specs {
xspec := xpkg.specs[i]
if spec.id != xspec.id {
t.Fatal("spec.id doesn't match")
}
if spec.entryCount != xspec.entryCount {
t.Fatal("spec.entryCount doesn't match")
}
if len(spec.entries) != len(xspec.entries) {
t.Fatal("spec.entries lengths don't match")
}
for j, mask := range spec.entries {
xmask := xspec.entries[j]
if mask != xmask {
t.Fatal("entry mask doesn't match")
}
}
if len(spec.types) != len(xspec.types) {
t.Fatal("spec.types length don't match")
}
for j, typ := range spec.types {
xtyp := xspec.types[j]
if typ.id != xtyp.id {
t.Fatal("typ.id doesn't match")
}
if typ.entryCount != xtyp.entryCount {
t.Fatal("typ.entryCount doesn't match")
}
// Config size can differ after serialization due to the loss of extended fields
// during reserialization, but the fixed portions of the Type header must not change.
if uint32(typ.headerByteSize)-typ.config.size != uint32(xtyp.headerByteSize)-uint32(xtyp.config.size) {
t.Fatal("fixed size header portions don't match")
}
if len(typ.indices) != len(xtyp.indices) {
t.Fatal("typ.indices length don't match")
}
for k, index := range typ.indices {
xindex := xtyp.indices[k]
if index != xindex {
t.Errorf("type index doesn't match at %v, have %v, want %v", k, xindex, index)
}
}
if len(typ.entries) != len(xtyp.entries) {
t.Fatal("typ.entries lengths don't match")
}
for k, nt := range typ.entries {
xnt := xtyp.entries[k]
if nt == nil {
if xnt != nil {
t.Fatal("nt is nil but xnt is not")
}
continue
}
if nt.size != xnt.size {
t.Fatal("entry.size doesn't match")
}
if nt.flags != xnt.flags {
t.Fatal("entry.flags don't match")
}
if nt.key != xnt.key {
t.Fatal("entry.key doesn't match")
}
if nt.parent != xnt.parent {
t.Fatal("entry.parent doesn't match")
}
if nt.count != xnt.count {
t.Fatal("entry.count doesn't match")
}
for l, val := range nt.values {
xval := xnt.values[l]
if val.name != xval.name {
t.Fatal("value.name doesn't match")
}
}
}
}
}
}
func checkResources(t *testing.T) {
t.Helper()
if _, err := sdkpath.AndroidHome(); err != nil {
t.Skip("Could not locate Android SDK")
}
rscPath, err := apiResourcesPath()
if err != nil {
t.Skipf("failed to find resources: %v", err)
}
if _, err := os.Stat(rscPath); err != nil {
t.Skipf("failed to find resources: %v", err)
}
}
func BenchmarkTableRefByName(b *testing.B) {
if _, err := sdkpath.AndroidHome(); err != nil {
b.Fatal("Could not locate Android SDK")
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
tbl, err := OpenTable()
if err != nil {
b.Fatal(err)
}
_, err = tbl.RefByName("@android:style/Theme.NoTitleBar.Fullscreen")
if err != nil {
b.Fatal(err)
}
}
}

View file

@ -0,0 +1,40 @@
// Copyright 2016 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:build ignore
// Genarsc generates stripped down version of android.jar resources used
// for validation of manifest entries.
//
// Requires the selected Android SDK to support the MinSDK platform version.
package main
import (
"fmt"
"log"
"os"
"strconv"
"golang.org/x/mobile/internal/binres"
)
const tmpl = `// Copyright 2016 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.
// Code generated by genarsc.go. DO NOT EDIT.
package binres
var arsc = []byte(%s)`
func main() {
arsc, err := binres.PackResources()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("arsc.go", []byte(fmt.Sprintf(tmpl, strconv.Quote(string(arsc)))), 0644); err != nil {
log.Fatal(err)
}
}

252
internal/binres/node.go Normal file
View file

@ -0,0 +1,252 @@
// 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) {
if ns.end == nil {
ns.typ = ResXMLEndNamespace
} else {
ns.typ = ResXMLStartNamespace
}
ns.headerByteSize = 16
ns.byteSize = 24
bin := make([]byte, ns.byteSize)
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(buf []byte) error {
if err := (&el.NodeHeader).UnmarshalBinary(buf); err != nil {
return err
}
buf = buf[el.headerByteSize:]
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) {
el.typ = ResXMLStartElement
el.headerByteSize = 16
el.AttributeSize = 20
el.AttributeStart = 20
el.AttributeCount = uint16(len(el.attrs))
el.IdIndex = 0
el.ClassIndex = 0
el.StyleIndex = 0
el.byteSize = uint32(el.headerByteSize) + uint32(el.AttributeStart) + uint32(len(el.attrs)*int(el.AttributeSize))
bin := make([]byte, el.byteSize)
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) {
el.typ = ResXMLEndElement
el.headerByteSize = 16
el.byteSize = 24
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) {
cdt.typ = ResXMLCharData
cdt.headerByteSize = 16
cdt.byteSize = 28
bin := make([]byte, cdt.byteSize)
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
}

293
internal/binres/pool.go Normal file
View file

@ -0,0 +1,293 @@
// 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
// Resolve returns the string entry of PoolRef in pl.
func (ref PoolRef) Resolve(pl *Pool) string {
return pl.strings[ref]
}
// Pool is a container for string and style span collections.
//
// Pool has the following structure marshalled:
//
// chunkHeader
// uint32 number of strings in this pool
// uint32 number of style spans in pool
// uint32 SortedFlag, UTF8Flag
// uint32 index of string data from header
// uint32 index of style data from header
// []uint32 string indices starting at zero
// []uint16 or []uint8 concatenation of string entries
//
// UTF-16 entries are as follows:
//
// uint16 string length, exclusive
// uint16 [optional] low word if high bit of length set
// [n]byte data
// uint16 0x0000 terminator
//
// UTF-8 entries are as follows:
//
// uint8 character length, exclusive
// uint8 [optional] low word if high bit of character length set
// uint8 byte length, exclusive
// uint8 [optional] low word if high bit of byte length set
// [n]byte data
// uint8 0x00 terminator
type Pool struct {
chunkHeader
strings []string
styles []*Span
flags uint32 // SortedFlag, UTF8Flag
}
// ref returns the PoolRef of s, inserting s if it doesn't exist.
func (pl *Pool) ref(s string) PoolRef {
for i, x := range pl.strings {
if s == x {
return PoolRef(i)
}
}
pl.strings = append(pl.strings, s)
return PoolRef(len(pl.strings) - 1)
}
// RefByName returns the PoolRef of s, or error if not exists.
func (pl *Pool) RefByName(s string) (PoolRef, error) {
for i, x := range pl.strings {
if s == x {
return PoolRef(i), nil
}
}
return 0, fmt.Errorf("PoolRef by name %q does not exist", s)
}
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)
}
pl.strings = make([]string, btou32(bin[8:]))
pl.styles = make([]*Span, btou32(bin[12:]))
pl.flags = btou32(bin[16:])
offstrings := btou32(bin[20:])
offstyle := btou32(bin[24:])
hdrlen := 28
if pl.IsUTF8() {
for i := range pl.strings {
offset := int(offstrings + btou32(bin[hdrlen+i*4:]))
// if leading bit set for nchars and nbytes,
// treat first byte as 7-bit high word and next as low word.
nchars := int(bin[offset])
offset++
if nchars&(1<<7) != 0 {
n0 := nchars ^ (1 << 7) // high word
n1 := int(bin[offset]) // low word
nchars = n0*(1<<8) + n1
offset++
}
// TODO(d) At least one case in android.jar (api-10) resource table has only
// highbit set, making 7-bit highword zero. The reason for this is currently
// unknown but would make tests that unmarshal-marshal to match bytes impossible.
// The values noted were high-word: 0 (after highbit unset), low-word: 141
// The size may be treated as an int8 triggering the use of two bytes to store size
// even though the implementation uses uint8.
nbytes := int(bin[offset])
offset++
if nbytes&(1<<7) != 0 {
n0 := nbytes ^ (1 << 7) // high word
n1 := int(bin[offset]) // low word
nbytes = n0*(1<<8) + n1
offset++
}
data := bin[offset : offset+nbytes]
if x := uint8(bin[offset+nbytes]); x != 0 {
return fmt.Errorf("expected zero terminator, got 0x%02X for nchars=%v nbytes=%v data=%q", x, nchars, nbytes, data)
}
pl.strings[i] = string(data)
}
} else {
for i := range pl.strings {
offset := int(offstrings + btou32(bin[hdrlen+i*4:])) // read index of string
// if leading bit set for nchars, treat first byte as 7-bit high word and next as low word.
nchars := int(btou16(bin[offset:]))
offset += 2
if nchars&(1<<15) != 0 { // TODO(d) this is untested
n0 := nchars ^ (1 << 15) // high word
n1 := int(btou16(bin[offset:])) // low word
nchars = n0*(1<<16) + n1
offset += 2
}
data := make([]uint16, nchars)
for i := range data {
data[i] = btou16(bin[offset+2*i:])
}
if x := btou16(bin[offset+nchars*2:]); x != 0 {
return fmt.Errorf("expected zero terminator, got 0x%04X for nchars=%v data=%q", x, nchars, data)
}
pl.strings[i] = string(utf16.Decode(data))
}
}
// TODO decode styles
_ = offstyle
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 []TableRef
}
func (m *Map) UnmarshalBinary(bin []byte) error {
(&m.chunkHeader).UnmarshalBinary(bin)
buf := bin[m.headerByteSize:m.byteSize]
m.rs = make([]TableRef, len(buf)/4)
for i := range m.rs {
m.rs[i] = TableRef(btou32(buf[i*4:]))
}
return nil
}
func (m *Map) MarshalBinary() ([]byte, error) {
m.typ = ResXMLResourceMap
m.headerByteSize = 8
m.byteSize = uint32(m.headerByteSize) + uint32(len(m.rs)*4)
bin := make([]byte, m.byteSize)
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:], uint32(r))
}
return bin, nil
}

153
internal/binres/sdk.go Normal file
View file

@ -0,0 +1,153 @@
// Copyright 2016 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"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"golang.org/x/mobile/internal/sdkpath"
)
// MinSDK is the targeted sdk version for support by package binres.
const MinSDK = 16
func apiResources() ([]byte, error) {
apiResPath, err := apiResourcesPath()
if err != nil {
return nil, err
}
zr, err := zip.OpenReader(apiResPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(`%v; consider installing with "android update sdk --all --no-ui --filter android-%d"`, err, MinSDK)
}
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")
}
return buf.Bytes(), nil
}
func apiResourcesPath() (string, error) {
platformDir, err := sdkpath.AndroidAPIPath(MinSDK)
if err != nil {
return "", err
}
return filepath.Join(platformDir, "android.jar"), nil
}
// PackResources produces a stripped down gzip version of the resources.arsc from api jar.
func PackResources() ([]byte, error) {
tbl, err := OpenSDKTable()
if err != nil {
return nil, err
}
tbl.pool.strings = []string{} // should not be needed
pkg := tbl.pkgs[0]
// drop language string entries
for _, typ := range pkg.specs[3].types {
if typ.config.locale.language != 0 {
for j, nt := range typ.entries {
if nt == nil { // NoEntry
continue
}
pkg.keyPool.strings[nt.key] = ""
typ.indices[j] = NoEntry
typ.entries[j] = nil
}
}
}
// drop strings from pool for specs to be dropped
for _, spec := range pkg.specs[4:] {
for _, typ := range spec.types {
for _, nt := range typ.entries {
if nt == nil { // NoEntry
continue
}
// don't drop if there's a collision
var collision bool
for _, xspec := range pkg.specs[:4] {
for _, xtyp := range xspec.types {
for _, xnt := range xtyp.entries {
if xnt == nil {
continue
}
if collision = nt.key == xnt.key; collision {
break
}
}
}
}
if !collision {
pkg.keyPool.strings[nt.key] = ""
}
}
}
}
// entries are densely packed but probably safe to drop nil entries off the end
for _, spec := range pkg.specs[:4] {
for _, typ := range spec.types {
var last int
for i, nt := range typ.entries {
if nt != nil {
last = i
}
}
typ.entries = typ.entries[:last+1]
typ.indices = typ.indices[:last+1]
}
}
// keeping 0:attr, 1:id, 2:style, 3:string
pkg.typePool.strings = pkg.typePool.strings[:4]
pkg.specs = pkg.specs[:4]
bin, err := tbl.MarshalBinary()
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
zw := gzip.NewWriter(buf)
if _, err := zw.Write(bin); err != nil {
return nil, err
}
if err := zw.Flush(); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

833
internal/binres/table.go Normal file
View file

@ -0,0 +1,833 @@
// 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 (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"strings"
"unicode/utf16"
)
const NoEntry = 0xFFFFFFFF
// TableRef uniquely identifies entries within a resource table.
type TableRef uint32
// Resolve returns the Entry of TableRef in the given table.
//
// A TableRef is structured as follows:
//
// 0xpptteeee
// pp: package index
// tt: type spec index in package
// eeee: entry index in type spec
//
// The package and type spec values start at 1 for the first item,
// to help catch cases where they have not been supplied.
func (ref TableRef) Resolve(tbl *Table) (*Entry, error) {
pkg := tbl.pkgs[uint8(ref>>24)-1]
spec := pkg.specs[uint8(ref>>16)-1]
idx := uint16(ref)
for _, typ := range spec.types {
if idx < uint16(len(typ.entries)) {
nt := typ.entries[idx]
if nt == nil {
return nil, errors.New("nil entry match")
}
return nt, nil
}
}
return nil, errors.New("failed to resolve table reference")
}
// 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.
type Table struct {
chunkHeader
pool *Pool
pkgs []*Package
}
// NewMipmapTable returns a resource table initialized for a single xxxhdpi mipmap resource
// and the path to write resource data to.
func NewMipmapTable(pkgname string) (*Table, string) {
pkg := &Package{id: 127, name: pkgname, typePool: &Pool{}, keyPool: &Pool{}}
attr := pkg.typePool.ref("attr")
mipmap := pkg.typePool.ref("mipmap")
icon := pkg.keyPool.ref("icon")
nt := &Entry{values: []*Value{{data: &Data{Type: DataString}}}}
typ := &Type{id: 2, indices: []uint32{0}, entries: []*Entry{nt}}
typ.config.screenType.density = 640
typ.config.version.sdk = 4
pkg.specs = append(pkg.specs,
&TypeSpec{
id: uint8(attr) + 1, //1,
},
&TypeSpec{
id: uint8(mipmap) + 1, //2,
entryCount: 1,
entries: []uint32{uint32(icon)}, // {0}
types: []*Type{typ},
})
pkg.lastPublicType = uint32(len(pkg.typePool.strings)) // 2
pkg.lastPublicKey = uint32(len(pkg.keyPool.strings)) // 1
name := "res/mipmap-xxxhdpi-v4/icon.png"
tbl := &Table{pool: &Pool{}, pkgs: []*Package{pkg}}
tbl.pool.ref(name)
return tbl, name
}
// OpenSDKTable decodes resources.arsc from sdk platform jar.
func OpenSDKTable() (*Table, error) {
bin, err := apiResources()
if err != nil {
return nil, err
}
tbl := new(Table)
if err := tbl.UnmarshalBinary(bin); err != nil {
return nil, err
}
return tbl, nil
}
// OpenTable decodes the prepacked resources.arsc for the supported sdk platform.
func OpenTable() (*Table, error) {
zr, err := gzip.NewReader(bytes.NewReader(arsc))
if err != nil {
return nil, fmt.Errorf("gzip: %v", err)
}
defer zr.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, zr); err != nil {
return nil, fmt.Errorf("io: %v", err)
}
tbl := new(Table)
if err := tbl.UnmarshalBinary(buf.Bytes()); err != nil {
return nil, err
}
return tbl, nil
}
// SpecByName parses the spec name from an entry string if necessary and returns
// the Package and TypeSpec associated with that name along with their respective
// indices.
//
// For example:
//
// tbl.SpecByName("@android:style/Theme.NoTitleBar")
// tbl.SpecByName("style")
//
// Both locate the spec by name "style".
func (tbl *Table) SpecByName(name string) (int, *Package, int, *TypeSpec, error) {
n := strings.TrimPrefix(name, "@android:")
n = strings.Split(n, "/")[0]
for pp, pkg := range tbl.pkgs {
for tt, spec := range pkg.specs {
if n == pkg.typePool.strings[spec.id-1] {
return pp, pkg, tt, spec, nil
}
}
}
return 0, nil, 0, nil, fmt.Errorf("spec by name not found: %q", name)
}
// RefByName returns the TableRef by a given name. The ref may be used to resolve the
// associated Entry and is used for the generation of binary manifest files.
func (tbl *Table) RefByName(name string) (TableRef, error) {
pp, pkg, tt, spec, err := tbl.SpecByName(name)
if err != nil {
return 0, err
}
q := strings.Split(name, "/")
if len(q) != 2 {
return 0, fmt.Errorf("invalid entry format, missing forward-slash: %q", name)
}
n := q[1]
for _, t := range spec.types {
for eeee, nt := range t.entries {
if nt == nil { // NoEntry
continue
}
if n == pkg.keyPool.strings[nt.key] {
return TableRef(uint32(eeee) | uint32(tt+1)<<16 | uint32(pp+1)<<24), nil
}
}
}
return 0, fmt.Errorf("failed to find table ref by %q", name)
}
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
}
func (tbl *Table) MarshalBinary() ([]byte, error) {
bin := make([]byte, 12)
putu16(bin, uint16(ResTable))
putu16(bin[2:], 12)
putu32(bin[8:], uint32(len(tbl.pkgs)))
if tbl.pool.IsUTF8() {
tbl.pool.flags ^= UTF8Flag
defer func() {
tbl.pool.flags |= UTF8Flag
}()
}
b, err := tbl.pool.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
for _, pkg := range tbl.pkgs {
b, err = pkg.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
putu32(bin[4:], uint32(len(bin)))
return bin, 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
aliases []*StagedAlias
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:pkg.byteSize]
for len(buf) > 0 {
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:]
case ResTableStagedAlias:
alias := new(StagedAlias)
if err := alias.UnmarshalBinary(buf); err != nil {
return err
}
pkg.aliases = append(pkg.aliases, alias)
buf = buf[alias.byteSize:]
default:
return errWrongType(t, ResTableTypeSpec, ResTableType, ResTableStagedAlias)
}
}
return nil
}
func (pkg *Package) MarshalBinary() ([]byte, error) {
// Package header size is determined by C++ struct ResTable_package
// see frameworks/base/include/ResourceTypes.h
bin := make([]byte, 288)
putu16(bin, uint16(ResTablePackage))
putu16(bin[2:], 288)
putu32(bin[8:], pkg.id)
p := utf16.Encode([]rune(pkg.name))
for i, x := range p {
putu16(bin[12+i*2:], x)
}
if pkg.typePool != nil {
if pkg.typePool.IsUTF8() {
pkg.typePool.flags ^= UTF8Flag
defer func() {
pkg.typePool.flags |= UTF8Flag
}()
}
b, err := pkg.typePool.MarshalBinary()
if err != nil {
return nil, err
}
putu32(bin[268:], uint32(len(bin)))
putu32(bin[272:], pkg.lastPublicType)
bin = append(bin, b...)
}
if pkg.keyPool != nil {
if pkg.keyPool.IsUTF8() {
pkg.keyPool.flags ^= UTF8Flag
defer func() {
pkg.keyPool.flags |= UTF8Flag
}()
}
b, err := pkg.keyPool.MarshalBinary()
if err != nil {
return nil, err
}
putu32(bin[276:], uint32(len(bin)))
putu32(bin[280:], pkg.lastPublicKey)
bin = append(bin, b...)
}
for _, alias := range pkg.aliases {
b, err := alias.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
for _, spec := range pkg.specs {
b, err := spec.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
putu32(bin[4:], uint32(len(bin)))
return bin, 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 // entry configuration masks
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
}
func (spec *TypeSpec) MarshalBinary() ([]byte, error) {
bin := make([]byte, 16+len(spec.entries)*4)
putu16(bin, uint16(ResTableTypeSpec))
putu16(bin[2:], 16)
putu32(bin[4:], uint32(len(bin)))
bin[8] = byte(spec.id)
// [9] = 0
// [10:12] = 0
putu32(bin[12:], uint32(len(spec.entries)))
for i, x := range spec.entries {
putu32(bin[16+i*4:], x)
}
for _, typ := range spec.types {
b, err := typ.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
return bin, nil
}
// Type provides a collection of entries for a specific device configuration.
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
// configuration this collection of entries is designed for
config struct {
size uint32
imsi struct {
mcc uint16 // mobile country code
mnc uint16 // mobile network code
}
locale struct {
language uint16
country uint16
}
screenType struct {
orientation uint8
touchscreen uint8
density uint16
}
input struct {
keyboard uint8
navigation uint8
inputFlags uint8
inputPad0 uint8
}
screenSize struct {
width uint16
height uint16
}
version struct {
sdk uint16
minor uint16 // always 0
}
screenConfig struct {
layout uint8
uiMode uint8
smallestWidthDP uint16
}
screenSizeDP struct {
width uint16
height uint16
}
}
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")
}
typ.config.size = btou32(bin[20:])
typ.config.imsi.mcc = btou16(bin[24:])
typ.config.imsi.mnc = btou16(bin[26:])
typ.config.locale.language = btou16(bin[28:])
typ.config.locale.country = btou16(bin[30:])
typ.config.screenType.orientation = uint8(bin[32])
typ.config.screenType.touchscreen = uint8(bin[33])
typ.config.screenType.density = btou16(bin[34:])
typ.config.input.keyboard = uint8(bin[36])
typ.config.input.navigation = uint8(bin[37])
typ.config.input.inputFlags = uint8(bin[38])
typ.config.input.inputPad0 = uint8(bin[39])
typ.config.screenSize.width = btou16(bin[40:])
typ.config.screenSize.height = btou16(bin[42:])
typ.config.version.sdk = btou16(bin[44:])
typ.config.version.minor = btou16(bin[46:])
typ.config.screenConfig.layout = uint8(bin[48])
typ.config.screenConfig.uiMode = uint8(bin[49])
typ.config.screenConfig.smallestWidthDP = btou16(bin[50:])
typ.config.screenSizeDP.width = btou16(bin[52:])
typ.config.screenSizeDP.height = btou16(bin[54:])
// fmt.Println("language/country:", u16tos(typ.config.locale.language), u16tos(typ.config.locale.country))
buf := bin[typ.headerByteSize:typ.entriesStart]
if len(buf) != 4*int(typ.entryCount) {
return fmt.Errorf("index buffer len[%v] doesn't match entryCount[%v]", len(buf), typ.entryCount)
}
typ.indices = make([]uint32, typ.entryCount)
for i := range typ.indices {
typ.indices[i] = btou32(buf[i*4:])
}
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
}
func (typ *Type) MarshalBinary() ([]byte, error) {
bin := make([]byte, 56+len(typ.entries)*4)
putu16(bin, uint16(ResTableType))
putu16(bin[2:], 56)
bin[8] = byte(typ.id)
// [9] = 0
// [10:12] = 0
putu32(bin[12:], uint32(len(typ.entries)))
putu32(bin[16:], uint32(56+len(typ.entries)*4))
// assure typ.config.size is always written as 36; extended configuration beyond supported
// API level is not supported by this marshal implementation but will be forward-compatible.
putu32(bin[20:], 36)
putu16(bin[24:], typ.config.imsi.mcc)
putu16(bin[26:], typ.config.imsi.mnc)
putu16(bin[28:], typ.config.locale.language)
putu16(bin[30:], typ.config.locale.country)
bin[32] = typ.config.screenType.orientation
bin[33] = typ.config.screenType.touchscreen
putu16(bin[34:], typ.config.screenType.density)
bin[36] = typ.config.input.keyboard
bin[37] = typ.config.input.navigation
bin[38] = typ.config.input.inputFlags
bin[39] = typ.config.input.inputPad0
putu16(bin[40:], typ.config.screenSize.width)
putu16(bin[42:], typ.config.screenSize.height)
putu16(bin[44:], typ.config.version.sdk)
putu16(bin[46:], typ.config.version.minor)
bin[48] = typ.config.screenConfig.layout
bin[49] = typ.config.screenConfig.uiMode
putu16(bin[50:], typ.config.screenConfig.smallestWidthDP)
putu16(bin[52:], typ.config.screenSizeDP.width)
putu16(bin[54:], typ.config.screenSizeDP.height)
var ntbin []byte
for i, nt := range typ.entries {
if nt == nil { // NoEntry
putu32(bin[56+i*4:], NoEntry)
continue
}
putu32(bin[56+i*4:], uint32(len(ntbin)))
b, err := nt.MarshalBinary()
if err != nil {
return nil, err
}
ntbin = append(ntbin, b...)
}
bin = append(bin, ntbin...)
putu32(bin[4:], uint32(len(bin)))
return bin, nil
}
type StagedAliasEntry struct {
stagedID uint32
finalizedID uint32
}
func (ae *StagedAliasEntry) MarshalBinary() ([]byte, error) {
bin := make([]byte, 8)
putu32(bin, ae.stagedID)
putu32(bin[4:], ae.finalizedID)
return bin, nil
}
func (ae *StagedAliasEntry) UnmarshalBinary(bin []byte) error {
ae.stagedID = btou32(bin)
ae.finalizedID = btou32(bin[4:])
return nil
}
type StagedAlias struct {
chunkHeader
count uint32
entries []StagedAliasEntry
}
func (a *StagedAlias) UnmarshalBinary(bin []byte) error {
if err := (&a.chunkHeader).UnmarshalBinary(bin); err != nil {
return err
}
if a.typ != ResTableStagedAlias {
return errWrongType(a.typ, ResTableStagedAlias)
}
a.count = btou32(bin[8:])
a.entries = make([]StagedAliasEntry, a.count)
for i := range a.entries {
if err := a.entries[i].UnmarshalBinary(bin[12+i*8:]); err != nil {
return err
}
}
return nil
}
func (a *StagedAlias) MarshalBinary() ([]byte, error) {
chunkHeaderBin, err := a.chunkHeader.MarshalBinary()
if err != nil {
return nil, err
}
countBin := make([]byte, 4)
putu32(countBin, a.count)
bin := append(chunkHeaderBin, countBin...)
for _, entry := range a.entries {
entryBin, err := entry.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, entryBin...)
}
return bin, 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
// only filled if this is a map entry; when size is 16
parent TableRef // id of parent mapping or zero if none
count uint32 // name and value pairs that follow for FlagComplex
values []*Value
}
func (nt *Entry) UnmarshalBinary(bin []byte) error {
nt.size = btou16(bin)
nt.flags = btou16(bin[2:])
nt.key = PoolRef(btou32(bin[4:]))
if nt.size == 16 {
nt.parent = TableRef(btou32(bin[8:]))
nt.count = btou32(bin[12:])
nt.values = make([]*Value, nt.count)
for i := range nt.values {
val := &Value{}
if err := val.UnmarshalBinary(bin[16+i*12:]); err != nil {
return err
}
nt.values[i] = val
}
} else {
data := &Data{}
if err := data.UnmarshalBinary(bin[8:]); err != nil {
return err
}
// TODO boxing data not strictly correct as binary repr isn't of Value.
nt.values = append(nt.values, &Value{0, data})
}
return nil
}
func (nt *Entry) MarshalBinary() ([]byte, error) {
bin := make([]byte, 8)
sz := nt.size
if sz == 0 {
sz = 8
}
putu16(bin, sz)
putu16(bin[2:], nt.flags)
putu32(bin[4:], uint32(nt.key))
if sz == 16 {
bin = append(bin, make([]byte, 8+len(nt.values)*12)...)
putu32(bin[8:], uint32(nt.parent))
putu32(bin[12:], uint32(len(nt.values)))
for i, val := range nt.values {
b, err := val.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[16+i*12:], b)
}
} else {
b, err := nt.values[0].data.MarshalBinary()
if err != nil {
return nil, err
}
bin = append(bin, b...)
}
return bin, nil
}
type Value struct {
name TableRef
data *Data
}
func (val *Value) UnmarshalBinary(bin []byte) error {
val.name = TableRef(btou32(bin))
val.data = &Data{}
return val.data.UnmarshalBinary(bin[4:])
}
func (val *Value) MarshalBinary() ([]byte, error) {
bin := make([]byte, 12)
putu32(bin, uint32(val.name))
b, err := val.data.MarshalBinary()
if err != nil {
return nil, err
}
copy(bin[4:], b)
return bin, nil
}
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, 8)
bin[2] = byte(d.Res0)
bin[3] = byte(d.Type)
putu32(bin[4:], d.Value)
return bin, nil
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

BIN
internal/binres/testdata/bootstrap.arsc vendored Normal file

Binary file not shown.

BIN
internal/binres/testdata/bootstrap.bin vendored Normal file

Binary file not shown.

38
internal/binres/testdata/bootstrap.xml vendored Normal file
View file

@ -0,0 +1,38 @@
<?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-permission android:name="android.permission.INTERNET" />
<application
android:label="Balloon世界"
android:allowBackup="true"
android:hasCode="false"
android:icon="@mipmap/icon"
foo="bar"
android:debuggable="true"
baz="bar"
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>

20
internal/binres/testdata/gen.sh vendored Executable file
View file

@ -0,0 +1,20 @@
#! /usr/bin/env bash
# version of build-tools tests run against
AAPT=${ANDROID_HOME:-${HOME}/Android/Sdk}/build-tools/32.0.0/aapt
# minimum version of android api for resource identifiers supported
APIJAR=${ANDROID_HOME:-${HOME}/Android/Sdk}/platforms/android-16/android.jar
for f in *.xml; do
RES=""
if [ -d "${f:0:-4}-res" ]; then
RES="-S ${f:0:-4}-res"
fi
cp "$f" AndroidManifest.xml
"$AAPT" p -M AndroidManifest.xml $RES -I "$APIJAR" -F tmp.apk
unzip -qq -o tmp.apk AndroidManifest.xml resources.arsc
mv AndroidManifest.xml "${f:0:-3}bin"
mv resources.arsc "${f:0:-3}arsc"
rm tmp.apk
done

248
internal/importers/ast.go Normal file
View file

@ -0,0 +1,248 @@
// Copyright 2016 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.
// The importers package uses go/ast to analyze Go packages or Go files
// and collect references to types whose package has a package prefix.
// It is used by the language specific importers to determine the set of
// wrapper types to be generated.
//
// # For example, in the Go file
//
// package javaprogram
//
// import "Java/java/lang"
//
// func F() {
//
// o := lang.Object.New()
// ...
//
// }
//
// the java importer uses this package to determine that the "java/lang"
// package and the wrapper interface, lang.Object, needs to be generated.
// After calling AnalyzeFile or AnalyzePackages, the References result
// contains the reference to lang.Object and the names set will contain
// "New".
package importers
import (
"errors"
"go/ast"
"go/token"
"path"
"sort"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
)
// References is the result of analyzing a Go file or set of Go packages.
//
// # For example, the Go file
//
// package pkg
//
// import "Prefix/some/Package"
//
// var A = Package.Identifier
//
// Will result in a single PkgRef with the "some/Package" package and
// the Identifier name. The Names set will contain the single name,
// "Identifier".
type References struct {
// The list of references to identifiers in packages that are
// identified by a package prefix.
Refs []PkgRef
// The list of names used in at least one selector expression.
// Useful as a conservative upper bound on the set of identifiers
// referenced from a set of packages.
Names map[string]struct{}
// Embedders is a list of struct types with prefixed types
// embedded.
Embedders []Struct
}
// Struct is a representation of a struct type with embedded
// types.
type Struct struct {
Name string
Pkg string
PkgPath string
Refs []PkgRef
}
// PkgRef is a reference to an identifier in a package.
type PkgRef struct {
Name string
Pkg string
}
type refsSaver struct {
pkgPrefix string
*References
refMap map[PkgRef]struct{}
insideStruct bool
}
// AnalyzeFile scans the provided file for references to packages with the given
// package prefix. The list of unique (package, identifier) pairs is returned
func AnalyzeFile(file *ast.File, pkgPrefix string) (*References, error) {
visitor := newRefsSaver(pkgPrefix)
fset := token.NewFileSet()
files := map[string]*ast.File{file.Name.Name: file}
// Ignore errors (from unknown packages)
pkg, _ := ast.NewPackage(fset, files, visitor.importer(), nil)
ast.Walk(visitor, pkg)
visitor.findEmbeddingStructs("", pkg)
return visitor.References, nil
}
// AnalyzePackages scans the provided packages for references to packages with the given
// package prefix. The list of unique (package, identifier) pairs is returned
func AnalyzePackages(pkgs []*packages.Package, pkgPrefix string) (*References, error) {
visitor := newRefsSaver(pkgPrefix)
imp := visitor.importer()
fset := token.NewFileSet()
for _, pkg := range pkgs {
files := make(map[string]*ast.File)
for i, name := range pkg.GoFiles {
files[name] = pkg.Syntax[i]
}
// Ignore errors (from unknown packages)
astpkg, _ := ast.NewPackage(fset, files, imp, nil)
ast.Walk(visitor, astpkg)
visitor.findEmbeddingStructs(pkg.PkgPath, astpkg)
}
return visitor.References, nil
}
// findEmbeddingStructs finds all top level declarations embedding a prefixed type.
//
// For example:
//
// import "Prefix/some/Package"
//
// type T struct {
//
// Package.Class
//
// }
func (v *refsSaver) findEmbeddingStructs(pkgpath string, pkg *ast.Package) {
var names []string
for _, obj := range pkg.Scope.Objects {
if obj.Kind != ast.Typ || !ast.IsExported(obj.Name) {
continue
}
names = append(names, obj.Name)
}
sort.Strings(names)
for _, name := range names {
obj := pkg.Scope.Objects[name]
t, ok := obj.Decl.(*ast.TypeSpec).Type.(*ast.StructType)
if !ok {
continue
}
var refs []PkgRef
for _, f := range t.Fields.List {
sel, ok := f.Type.(*ast.SelectorExpr)
if !ok {
continue
}
ref, ok := v.addRef(sel)
if !ok {
continue
}
if len(f.Names) > 0 && !f.Names[0].IsExported() {
continue
}
refs = append(refs, ref)
}
if len(refs) > 0 {
v.Embedders = append(v.Embedders, Struct{
Name: obj.Name,
Pkg: pkg.Name,
PkgPath: pkgpath,
Refs: refs,
})
}
}
}
func newRefsSaver(pkgPrefix string) *refsSaver {
s := &refsSaver{
pkgPrefix: pkgPrefix,
refMap: make(map[PkgRef]struct{}),
References: &References{},
}
s.Names = make(map[string]struct{})
return s
}
func (v *refsSaver) importer() ast.Importer {
return func(imports map[string]*ast.Object, pkgPath string) (*ast.Object, error) {
if pkg, exists := imports[pkgPath]; exists {
return pkg, nil
}
if !strings.HasPrefix(pkgPath, v.pkgPrefix) {
return nil, errors.New("ignored")
}
pkg := ast.NewObj(ast.Pkg, path.Base(pkgPath))
imports[pkgPath] = pkg
return pkg, nil
}
}
func (v *refsSaver) addRef(sel *ast.SelectorExpr) (PkgRef, bool) {
x, ok := sel.X.(*ast.Ident)
if !ok || x.Obj == nil {
return PkgRef{}, false
}
imp, ok := x.Obj.Decl.(*ast.ImportSpec)
if !ok {
return PkgRef{}, false
}
pkgPath, err := strconv.Unquote(imp.Path.Value)
if err != nil {
return PkgRef{}, false
}
if !strings.HasPrefix(pkgPath, v.pkgPrefix) {
return PkgRef{}, false
}
pkgPath = pkgPath[len(v.pkgPrefix):]
ref := PkgRef{Pkg: pkgPath, Name: sel.Sel.Name}
if _, exists := v.refMap[ref]; !exists {
v.refMap[ref] = struct{}{}
v.Refs = append(v.Refs, ref)
}
return ref, true
}
func (v *refsSaver) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case *ast.StructType:
// Use a copy of refsSaver that only accepts exported fields. It refers
// to the original refsSaver for collecting references.
v2 := *v
v2.insideStruct = true
return &v2
case *ast.Field:
if v.insideStruct && len(n.Names) == 1 && !n.Names[0].IsExported() {
return nil
}
case *ast.SelectorExpr:
v.Names[n.Sel.Name] = struct{}{}
if _, ok := v.addRef(n); ok {
return nil
}
case *ast.FuncDecl:
if n.Recv != nil { // Methods
v.Names[n.Name.Name] = struct{}{}
}
}
return v
}

View file

@ -0,0 +1,69 @@
// Copyright 2016 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 importers
import (
"go/parser"
"go/token"
"reflect"
"testing"
)
func TestAnalyzer(t *testing.T) {
file := `package ast_test
import "Prefix/some/pkg/Name"
import "Prefix/some/pkg/Name2"
const c = Name.Constant
type T struct {
Name.Type
unexported Name.Type2
}
func f() {
Name2.Func().Func().Func()
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "ast_test.go", file, parser.AllErrors)
if err != nil {
t.Fatal(err)
}
refs, err := AnalyzeFile(f, "Prefix/")
if err != nil {
t.Fatal(err)
}
exps := []PkgRef{
{Pkg: "some/pkg/Name", Name: "Constant"},
{Pkg: "some/pkg/Name", Name: "Type"},
{Pkg: "some/pkg/Name2", Name: "Func"},
{Pkg: "some/pkg/Name", Name: "Type2"},
}
if len(refs.Refs) != len(exps) {
t.Fatalf("expected %d references; got %d", len(exps), len(refs.Refs))
}
for i, exp := range exps {
if got := refs.Refs[i]; exp != got {
t.Errorf("expected ref %v; got %v", exp, got)
}
}
if _, exists := refs.Names["Constant"]; !exists {
t.Errorf("expected \"Constant\" in the names set")
}
if len(refs.Embedders) != 1 {
t.Fatalf("expected 1 struct; got %d", len(refs.Embedders))
}
s := refs.Embedders[0]
exp := Struct{
Name: "T",
Pkg: "ast_test",
Refs: []PkgRef{{Pkg: "some/pkg/Name", Name: "Type"}},
}
if !reflect.DeepEqual(exp, s) {
t.Errorf("expected struct %v; got %v", exp, s)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,206 @@
// Copyright 2016 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 java
import (
"reflect"
"testing"
"golang.org/x/mobile/internal/importers"
)
func TestImport(t *testing.T) {
if !IsAvailable() {
t.Skipf("javap not available")
}
tests := []struct {
ref importers.PkgRef
name string
methods []*FuncSet
}{
{
ref: importers.PkgRef{Pkg: "java/lang/Object", Name: "equals"},
name: "java.lang.Object",
methods: []*FuncSet{
&FuncSet{
Name: "equals",
GoName: "Equals",
CommonSig: CommonSig{
Params: []*Type{&Type{Kind: Object, Class: "java.lang.Object"}}, Ret: &Type{Kind: Boolean}, HasRet: true,
},
Funcs: []*Func{&Func{FuncSig: FuncSig{Name: "equals", Desc: "(Ljava/lang/Object;)Z"}, ArgDesc: "Ljava/lang/Object;", JNIName: "equals", Public: true, Params: []*Type{&Type{Kind: Object, Class: "java.lang.Object"}}, Ret: &Type{Kind: Boolean}}},
},
},
},
{
ref: importers.PkgRef{Pkg: "java/lang/Runnable", Name: "run"},
name: "java.lang.Runnable",
methods: []*FuncSet{
&FuncSet{
Name: "run",
GoName: "Run",
CommonSig: CommonSig{},
Funcs: []*Func{&Func{FuncSig: FuncSig{Name: "run", Desc: "()V"}, ArgDesc: "", JNIName: "run", Public: true, Abstract: true}},
},
},
},
}
toString := &FuncSet{
Name: "toString",
GoName: "ToString",
CommonSig: CommonSig{
Ret: &Type{Kind: String}, HasRet: true,
},
Funcs: []*Func{&Func{FuncSig: FuncSig{Name: "toString", Desc: "()Ljava/lang/String;"}, ArgDesc: "", JNIName: "toString", Public: true, Ret: &Type{Kind: String}}},
}
for _, test := range tests {
refs := &importers.References{
Refs: []importers.PkgRef{test.ref},
Names: make(map[string]struct{}),
}
for _, m := range test.methods {
refs.Names[m.GoName] = struct{}{}
}
classes, err := (&Importer{}).Import(refs)
if err != nil {
t.Fatal(err)
}
if len(classes) != 1 {
t.Fatalf("got %d classes, expected 1", len(classes))
}
cls := classes[0]
if cls.Name != test.name {
t.Errorf("got class name %s, expected %s", cls.Name, test.name)
}
methods := test.methods
if !cls.Interface {
methods = append(methods, toString)
}
loop:
for _, exp := range methods {
for _, got := range cls.AllMethods {
if reflect.DeepEqual(exp, got) {
continue loop
}
}
t.Errorf("failed to find method: %+v", exp)
}
}
}
func testClsMap() map[string]*Class {
//
// A--
// / \ \
// B C \
// \ / \ \
// D E F
//
return map[string]*Class{
"A": &Class{},
"B": &Class{
Supers: []string{"A"},
},
"C": &Class{
Supers: []string{"A"},
},
"D": &Class{
Supers: []string{"B", "C"},
},
"E": &Class{
Supers: []string{"C"},
},
"F": &Class{
Supers: []string{"A"},
},
}
}
func TestCommonTypes(t *testing.T) {
clsMap := testClsMap()
tests := [][3]*Type{
{nil, nil, nil},
{&Type{Kind: Int}, nil, nil},
{&Type{Kind: Int}, &Type{Kind: Float}, nil},
{&Type{Kind: Int}, &Type{Kind: Int}, &Type{Kind: Int}},
{&Type{Kind: Object, Class: "D"}, &Type{Kind: Object, Class: "D"}, &Type{Kind: Object, Class: "D"}},
{&Type{Kind: Object, Class: "D"}, &Type{Kind: Object, Class: "E"}, &Type{Kind: Object, Class: "C"}},
{&Type{Kind: Object, Class: "D"}, &Type{Kind: Object, Class: "F"}, &Type{Kind: Object, Class: "A"}},
{&Type{Kind: Object, Class: "B"}, &Type{Kind: Object, Class: "E"}, &Type{Kind: Object, Class: "A"}},
}
for _, test := range tests {
t1, t2, exp := test[0], test[1], test[2]
got := commonType(clsMap, t1, t2)
if !reflect.DeepEqual(got, exp) {
t.Errorf("commonType(%+v, %+v) = %+v, expected %+v", t1, t2, got, exp)
}
}
}
func TestCommonSig(t *testing.T) {
tests := []struct {
Sigs []CommonSig
CommonSig
}{
{
Sigs: []CommonSig{
CommonSig{}, // f()
},
CommonSig: CommonSig{}, // f()
},
{
Sigs: []CommonSig{
CommonSig{Throws: true, HasRet: true, Ret: &Type{Kind: Int}}, // int f() throws
},
// int f() throws
CommonSig: CommonSig{Throws: true, HasRet: true, Ret: &Type{Kind: Int}},
},
{
Sigs: []CommonSig{
CommonSig{}, // f()
CommonSig{Params: []*Type{&Type{Kind: Int}}}, // f(int)
},
CommonSig: CommonSig{ // f(int...)
Variadic: true,
Params: []*Type{&Type{Kind: Int}},
},
},
{
Sigs: []CommonSig{
CommonSig{Params: []*Type{&Type{Kind: Int}}}, // f(int)
CommonSig{Params: []*Type{&Type{Kind: Float}}}, // f(float)
},
CommonSig: CommonSig{ // f(interface{})
Params: []*Type{nil},
},
},
{
Sigs: []CommonSig{
CommonSig{Params: []*Type{&Type{Kind: Int}}}, // f(int)
CommonSig{Params: []*Type{&Type{Kind: Int}, &Type{Kind: Int}}}, // f(int, int)
},
CommonSig: CommonSig{ // f(int, int...)
Variadic: true,
Params: []*Type{&Type{Kind: Int}, &Type{Kind: Int}},
},
},
{
Sigs: []CommonSig{
CommonSig{Params: []*Type{&Type{Kind: Object, Class: "A"}}}, // f(A)
CommonSig{Params: []*Type{&Type{Kind: Object, Class: "B"}}}, // f(B)
},
CommonSig: CommonSig{ // f(A)
Params: []*Type{&Type{Kind: Object, Class: "A"}},
},
},
}
clsMap := testClsMap()
for _, test := range tests {
got := combineSigs(clsMap, test.Sigs...)
if !reflect.DeepEqual(got, test.CommonSig) {
t.Errorf("commonSig(%+v) = %+v, expected %+v", test.Sigs, got, test.CommonSig)
}
}
}

View file

@ -0,0 +1,868 @@
// Copyright 2016 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.
// The objc package takes the result of an AST traversal by the
// importers package and uses the clang command to dump the type
// information for the referenced ObjC classes and protocols.
//
// It is the of go/types for ObjC types and is used by the bind
// package to generate Go wrappers for ObjC API on iOS.
package objc
import (
"bufio"
"bytes"
"fmt"
"os/exec"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/mobile/internal/importers"
)
type parser struct {
sdkPath string
sc *bufio.Scanner
decl string
indent int
last string
// Current module as parsed from the AST tree.
module string
}
type TypeKind int
// Named represents ObjC classes and protocols.
type Named struct {
Name string
GoName string
Module string
Funcs []*Func
Methods []*Func
AllMethods []*Func
Supers []Super
// For deduplication of function or method
// declarations.
funcMap map[string]struct{}
Protocol bool
// Generated is true if the type is wrapper of a
// generated Go struct.
Generated bool
}
// Super denotes a super class or protocol.
type Super struct {
Name string
Protocol bool
}
// Func is a ObjC method, static functions as well as
// instance methods.
type Func struct {
Sig string
GoName string
Params []*Param
Ret *Type
Static bool
// Method whose name start with "init"
Constructor bool
}
type Param struct {
Name string
Type *Type
}
type Type struct {
Kind TypeKind
// For Interface and Protocol types.
Name string
// For 'id' types.
instanceType bool
// The declared type raw from the AST.
Decl string
// Set if the type is a pointer to its kind. For classes
// Indirect is true if the type is a double pointer, e.g.
// NSObject **.
Indirect bool
}
const (
Unknown TypeKind = iota
Protocol
Class
String
Data
Int
Uint
Short
Ushort
Bool
Char
Uchar
Float
Double
)
// Import returns descriptors for a list of references to
// ObjC protocols and classes.
//
// The type information is parsed from the output of clang -cc1
// -ast-dump.
func Import(refs *importers.References) ([]*Named, error) {
var modules []string
modMap := make(map[string]struct{})
typeNames := make(map[string][]string)
typeSet := make(map[string]struct{})
genMods := make(map[string]struct{})
for _, emb := range refs.Embedders {
genMods[initialUpper(emb.Pkg)] = struct{}{}
}
for _, ref := range refs.Refs {
var module, name string
if idx := strings.Index(ref.Pkg, "/"); idx != -1 {
// ref is a static method reference.
module = ref.Pkg[:idx]
name = ref.Pkg[idx+1:]
} else {
// ref is a type name.
module = ref.Pkg
name = ref.Name
}
if _, exists := typeSet[name]; !exists {
typeNames[module] = append(typeNames[module], name)
typeSet[name] = struct{}{}
}
if _, exists := modMap[module]; !exists {
// Include the module only if it is generated.
if _, exists := genMods[module]; !exists {
modMap[module] = struct{}{}
modules = append(modules, module)
}
}
}
sdkPathOut, err := exec.Command("xcrun", "--sdk", "iphonesimulator", "--show-sdk-path").CombinedOutput()
if err != nil {
return nil, err
}
sdkPath := strings.TrimSpace(string(sdkPathOut))
var allTypes []*Named
typeMap := make(map[string]*Named)
for _, module := range modules {
types, err := importModule(string(sdkPath), module, typeNames[module], typeMap)
if err != nil {
return nil, fmt.Errorf("%s: %v", module, err)
}
allTypes = append(allTypes, types...)
}
// Embedders refer to every exported Go struct that will have its class
// generated. Allow Go code to reverse bind to those classes by synthesizing
// their descriptors.
for _, emb := range refs.Embedders {
module := initialUpper(emb.Pkg)
named := &Named{
Name: module + emb.Name,
GoName: emb.Name,
Module: module,
Generated: true,
}
for _, ref := range emb.Refs {
t, exists := typeMap[ref.Name]
if !exists {
return nil, fmt.Errorf("type not found: %q", ref.Name)
}
named.Supers = append(named.Supers, Super{
Name: t.Name,
Protocol: t.Protocol,
})
}
typeMap[emb.Name] = named
allTypes = append(allTypes, named)
}
initTypes(allTypes, refs, typeMap)
// Include implicit types that are used in parameter or return values.
newTypes := allTypes
for len(newTypes) > 0 {
var impTypes []*Named
for _, t := range newTypes {
for _, funcs := range [][]*Func{t.Funcs, t.AllMethods} {
for _, f := range funcs {
types := implicitFuncTypes(f)
for _, name := range types {
if _, exists := typeSet[name]; exists {
continue
}
typeSet[name] = struct{}{}
t, exists := typeMap[name]
if !exists {
return nil, fmt.Errorf("implicit type %q not found", name)
}
impTypes = append(impTypes, t)
}
}
}
}
initTypes(impTypes, refs, typeMap)
allTypes = append(allTypes, impTypes...)
newTypes = impTypes
}
return allTypes, nil
}
func implicitFuncTypes(f *Func) []string {
var types []string
if rt := f.Ret; rt != nil && !rt.instanceType && (rt.Kind == Class || rt.Kind == Protocol) {
types = append(types, rt.Name)
}
for _, p := range f.Params {
if t := p.Type; !t.instanceType && (t.Kind == Class || t.Kind == Protocol) {
types = append(types, t.Name)
}
}
return types
}
func initTypes(types []*Named, refs *importers.References, typeMap map[string]*Named) {
for _, t := range types {
fillAllMethods(t, typeMap)
}
// Move constructors to functions. They are represented in Go
// as functions.
for _, t := range types {
var methods []*Func
for _, f := range t.AllMethods {
if f.Constructor {
f.Static = true
t.Funcs = append(t.Funcs, f)
} else {
methods = append(methods, f)
}
}
t.AllMethods = methods
}
for _, t := range types {
mangleMethodNames(t.AllMethods)
mangleMethodNames(t.Funcs)
}
filterReferences(types, refs, typeMap)
for _, t := range types {
resolveInstanceTypes(t, t.Funcs)
resolveInstanceTypes(t, t.AllMethods)
}
}
func filterReferences(types []*Named, refs *importers.References, typeMap map[string]*Named) {
refFuncs := make(map[[2]string]struct{})
for _, ref := range refs.Refs {
if sep := strings.Index(ref.Pkg, "/"); sep != -1 {
pkgName := ref.Pkg[sep+1:]
n := typeMap[pkgName]
if n == nil {
continue
}
refFuncs[[...]string{pkgName, ref.Name}] = struct{}{}
}
}
for _, t := range types {
var filtered []*Func
for _, f := range t.Funcs {
if _, exists := refFuncs[[...]string{t.GoName, f.GoName}]; exists {
filtered = append(filtered, f)
}
}
t.Funcs = filtered
filtered = nil
for _, m := range t.Methods {
if _, exists := refs.Names[m.GoName]; exists {
filtered = append(filtered, m)
}
}
t.Methods = filtered
filtered = nil
for _, m := range t.AllMethods {
if _, exists := refs.Names[m.GoName]; exists {
filtered = append(filtered, m)
}
}
t.AllMethods = filtered
}
}
// mangleMethodNames assigns unique Go names to ObjC methods. If a method name is unique
// within the same method list, its name is used with its first letter in upper case.
// Multiple methods with the same name have their full signature appended, with : removed.
func mangleMethodNames(allFuncs []*Func) {
goName := func(n string, constructor bool) string {
if constructor {
n = "new" + n[len("init"):]
}
return initialUpper(n)
}
overloads := make(map[string][]*Func)
for i, f := range allFuncs {
// Copy function so each class can have its own
// name mangling.
f := *f
allFuncs[i] = &f
f.GoName = goName(f.Sig, f.Constructor)
if colon := strings.Index(f.GoName, ":"); colon != -1 {
f.GoName = f.GoName[:colon]
}
overloads[f.GoName] = append(overloads[f.GoName], &f)
}
fallbacks := make(map[string][]*Func)
for _, funcs := range overloads {
if len(funcs) == 1 {
continue
}
for _, f := range funcs {
sig := f.Sig
if strings.HasSuffix(sig, ":") {
sig = sig[:len(sig)-1]
}
sigElems := strings.Split(f.Sig, ":")
for i := 0; i < len(sigElems); i++ {
sigElems[i] = initialUpper(sigElems[i])
}
name := strings.Join(sigElems, "")
f.GoName = goName(name, f.Constructor)
fallbacks[f.GoName] = append(fallbacks[f.GoName], f)
}
}
for _, funcs := range fallbacks {
if len(funcs) == 1 {
continue
}
for _, f := range funcs {
name := strings.Replace(f.Sig, ":", "_", -1)
f.GoName = goName(name, f.Constructor)
}
}
}
func resolveInstanceType(n *Named, t *Type) *Type {
if !t.instanceType || t.Kind != Protocol {
return t
}
// Copy and update the type name for instancetype types
ct := *t
ct.instanceType = false
ct.Decl = n.Name + " *"
if n.Name == "NSString" {
ct.Kind = String
ct.Name = ""
} else {
ct.Kind = Class
ct.Name = n.Name
}
return &ct
}
func resolveInstanceTypes(n *Named, funcs []*Func) {
for _, f := range funcs {
for _, p := range f.Params {
p.Type = resolveInstanceType(n, p.Type)
}
if f.Ret != nil {
f.Ret = resolveInstanceType(n, f.Ret)
}
}
}
func fillAllMethods(n *Named, typeMap map[string]*Named) {
if len(n.AllMethods) > 0 {
return
}
if len(n.Supers) == 0 {
n.AllMethods = n.Methods
return
}
for _, sup := range n.Supers {
super := lookup(sup.Name, sup.Protocol, typeMap)
fillAllMethods(super, typeMap)
}
methods := make(map[string]struct{})
for _, sup := range n.Supers {
super := lookup(sup.Name, sup.Protocol, typeMap)
for _, f := range super.AllMethods {
if _, exists := methods[f.Sig]; !exists {
methods[f.Sig] = struct{}{}
n.AllMethods = append(n.AllMethods, f)
}
}
}
for _, f := range n.Methods {
if _, exists := methods[f.Sig]; !exists {
n.AllMethods = append(n.AllMethods, f)
}
}
}
const (
frameworksPath = "/System/Library/Frameworks/"
)
// importModule parses ObjC type information with clang -cc1 -ast-dump.
//
// TODO: Use module.map files to precisely model the @import Module.Identifier
// directive. For now, importModules assumes the single umbrella header
// file Module.framework/Headers/Module.h contains every declaration.
func importModule(sdkPath, module string, identifiers []string, typeMap map[string]*Named) ([]*Named, error) {
hFile := fmt.Sprintf(sdkPath+frameworksPath+"%s.framework/Headers/%[1]s.h", module)
clang := exec.Command("xcrun", "--sdk", "iphonesimulator", "clang", "-cc1", "-triple", "x86_64-apple-ios8.0.0-simulator", "-isysroot", sdkPath, "-ast-dump", "-fblocks", "-fobjc-arc", "-x", "objective-c", hFile)
out, err := clang.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("clang failed to parse module: %v: %s", err, out)
}
p := &parser{
sdkPath: sdkPath,
sc: bufio.NewScanner(bytes.NewBuffer(out)),
}
if err := p.parseModule(module, typeMap); err != nil {
return nil, err
}
var types []*Named
for _, ident := range identifiers {
named, exists := typeMap[ident]
if !exists {
return nil, fmt.Errorf("no such type: %s", ident)
}
types = append(types, named)
}
return types, nil
}
func (p *parser) scanLine() bool {
for {
l := p.last
if l == "" {
if !p.sc.Scan() {
return false
}
l = p.sc.Text()
} else {
p.last = ""
}
indent := (strings.Index(l, "-") + 1) / 2
switch {
case indent > p.indent:
// Skip
case indent < p.indent:
p.indent--
p.last = l
return false
case indent == p.indent:
p.decl = l[p.indent*2:]
return true
}
}
}
func (p *parser) parseModule(module string, typeMap map[string]*Named) (err error) {
defer func() {
if rerr := recover(); rerr != nil {
err = rerr.(error)
}
}()
if !p.scanLine() {
return nil
}
// A header file AST starts with
//
// TranslationUnitDecl 0x103833ad0 <<invalid sloc>> <invalid sloc>
if w := p.scanWord(); w != "TranslationUnitDecl" {
return fmt.Errorf("unexpected AST root: %q", w)
}
p.indent++
for {
if !p.scanLine() {
break
}
switch w := p.scanWord(); w {
case "ObjCCategoryDecl":
// ObjCCategoryDecl 0x103d9bdb8 <line:48:1, line:63:2> line:48:12 NSDateCreation
// |-ObjCInterface 0x103d9a788 'NSDate'
// Skip the node address, the source code range, position.
p.scanWord()
p.parseLocation()
catName := p.scanWord()
p.indent++
if !p.scanLine() {
return fmt.Errorf("no interface for category %s", catName)
}
if w := p.scanWord(); w != "ObjCInterface" {
return fmt.Errorf("unexpected declaaration %s for category %s", w, catName)
}
p.scanWord()
clsName := p.scanWord()
clsName = clsName[1 : len(clsName)-1]
named := lookup(clsName, false, typeMap)
if named == nil {
return fmt.Errorf("category %s references unknown class %s", catName, clsName)
}
p.parseInterface(named)
case "ObjCInterfaceDecl", "ObjCProtocolDecl":
// ObjCProtocolDecl 0x104116450 <line:15:1, line:47:2> line:15:11 NSObject
// or
// ObjCInterfaceDecl 0x1041ca480 <line:17:29, line:64:2> line:17:40 UIResponder
prot := w == "ObjCProtocolDecl"
// Skip the node address, the source code range, position.
p.scanWord()
if strings.HasPrefix(p.decl, "prev ") {
p.scanWord()
p.scanWord()
}
p.parseLocation()
if strings.HasPrefix(p.decl, "implicit ") {
p.scanWord()
}
name := p.decl
named := p.lookupOrCreate(name, prot, typeMap)
p.indent++
p.parseInterface(named)
default:
}
}
return nil
}
func lookup(name string, prot bool, typeMap map[string]*Named) *Named {
var mangled string
if prot {
mangled = name + "P"
} else {
mangled = name + "C"
}
if n := typeMap[mangled]; n != nil {
return n
}
return typeMap[name]
}
// lookupOrCreate looks up the type name in the type map. If it doesn't exist, it creates
// and returns a new type. If it does exist, it returns the existing type. If there are both
// a class and a protocol with the same name, their type names are mangled by prefixing
// 'C' or 'P' and then re-inserted into the type map.
func (p *parser) lookupOrCreate(name string, prot bool, typeMap map[string]*Named) *Named {
mangled := name + "C"
otherMangled := name + "P"
if prot {
mangled, otherMangled = otherMangled, mangled
}
named, exists := typeMap[mangled]
if exists {
return named
}
named, exists = typeMap[name]
if exists {
if named.Protocol == prot {
return named
}
// Both a class and a protocol exists with the same name.
delete(typeMap, name)
named.GoName = otherMangled
typeMap[otherMangled] = named
named = &Named{
GoName: mangled,
}
} else {
named = &Named{
GoName: name,
}
}
named.Name = name
named.Protocol = prot
named.funcMap = make(map[string]struct{})
named.Module = p.module
typeMap[named.GoName] = named
return named
}
func (p *parser) parseInterface(n *Named) {
for {
more := p.scanLine()
if !more {
break
}
switch w := p.scanWord(); w {
case "super":
if w := p.scanWord(); w != "ObjCInterface" {
panic(fmt.Errorf("unknown super type: %s", w))
}
// Skip node address.
p.scanWord()
super := p.scanWord()
// Remove single quotes
super = super[1 : len(super)-1]
n.Supers = append(n.Supers, Super{super, false})
case "ObjCProtocol":
p.scanWord()
super := p.scanWord()
super = super[1 : len(super)-1]
n.Supers = append(n.Supers, Super{super, true})
case "ObjCMethodDecl":
f := p.parseMethod()
if f == nil {
continue
}
var key string
if f.Static {
key = "+" + f.Sig
} else {
key = "-" + f.Sig
}
if _, exists := n.funcMap[key]; !exists {
n.funcMap[key] = struct{}{}
if f.Static {
n.Funcs = append(n.Funcs, f)
} else {
n.Methods = append(n.Methods, f)
}
}
}
}
}
func (p *parser) parseMethod() *Func {
// ObjCMethodDecl 0x103bdfb80 <line:17:1, col:27> col:1 - isEqual: 'BOOL':'_Bool'
// Skip the address, range, position.
p.scanWord()
p.parseLocation()
if strings.HasPrefix(p.decl, "implicit") {
p.scanWord()
}
f := new(Func)
switch w := p.scanWord(); w {
case "+":
f.Static = true
case "-":
f.Static = false
default:
panic(fmt.Errorf("unknown method type for %q", w))
}
f.Sig = p.scanWord()
if f.Sig == "dealloc" {
// ARC forbids dealloc
return nil
}
if strings.HasPrefix(f.Sig, "init") {
f.Constructor = true
}
f.Ret = p.parseType()
p.indent++
for {
more := p.scanLine()
if !more {
break
}
switch p.scanWord() {
case "UnavailableAttr":
p.indent--
return nil
case "ParmVarDecl":
f.Params = append(f.Params, p.parseParameter())
}
}
return f
}
func (p *parser) parseParameter() *Param {
// ParmVarDecl 0x1041caca8 <col:70, col:80> col:80 event 'UIEvent * _Nullable':'UIEvent *'
// Skip address, source range, position.
p.scanWord()
p.parseLocation()
return &Param{Name: p.scanWord(), Type: p.parseType()}
}
func (p *parser) parseType() *Type {
// NSUInteger':'unsigned long'
s := strings.SplitN(p.decl, ":", 2)
decl := s[0]
var canon string
if len(s) == 2 {
canon = s[1]
} else {
canon = decl
}
// unquote the type
canon = canon[1 : len(canon)-1]
if canon == "void" {
return nil
}
decl = decl[1 : len(decl)-1]
instancetype := strings.HasPrefix(decl, "instancetype")
// Strip modifiers
mods := []string{"__strong", "__unsafe_unretained", "const", "__strong", "_Nonnull", "_Nullable", "__autoreleasing"}
for _, mod := range mods {
if idx := strings.Index(canon, mod); idx != -1 {
canon = canon[:idx] + canon[idx+len(mod):]
}
if idx := strings.Index(decl, mod); idx != -1 {
decl = decl[:idx] + decl[idx+len(mod):]
}
}
canon = strings.TrimSpace(canon)
decl = strings.TrimSpace(decl)
t := &Type{
Decl: decl,
instanceType: instancetype,
}
switch canon {
case "int", "long", "long long":
t.Kind = Int
case "unsigned int", "unsigned long", "unsigned long long":
t.Kind = Uint
case "short":
t.Kind = Short
case "unsigned short":
t.Kind = Ushort
case "char":
t.Kind = Char
case "unsigned char":
t.Kind = Uchar
case "float":
t.Kind = Float
case "double":
t.Kind = Double
case "_Bool":
t.Kind = Bool
case "NSString *":
t.Kind = String
case "NSData *":
t.Kind = Data
default:
switch {
case strings.HasPrefix(canon, "enum"):
t.Kind = Int
case strings.HasPrefix(canon, "id"):
_, gen := p.splitGeneric(canon)
t.Kind = Protocol
t.Name = gen
default:
if ind := strings.Count(canon, "*"); 1 <= ind && ind <= 2 {
space := strings.Index(canon, " ")
name := canon[:space]
name, _ = p.splitGeneric(name)
t.Kind = Class
t.Name = name
t.Indirect = ind > 1
}
}
}
return t
}
func (p *parser) splitGeneric(decl string) (string, string) {
// NSArray<KeyType>
if br := strings.Index(decl, "<"); br != -1 {
return decl[:br], decl[br+1 : len(decl)-1]
} else {
return decl, ""
}
}
func (p *parser) parseSrcPos() {
const invPref = "<invalid sloc>"
if strings.HasPrefix(p.decl, invPref) {
p.decl = p.decl[len(invPref):]
return
}
var loc string
const scrPref = "<scratch space>"
if strings.HasPrefix(p.decl, scrPref) {
// <scratch space>:130:1
p.decl = p.decl[len(scrPref):]
loc = "line" + p.scanWord()
} else {
// line:17:2, col:18 or, a file location:
// /.../UIKit.framework/Headers/UISelectionFeedbackGenerator.h:16:1
loc = p.scanWord()
}
locs := strings.SplitN(loc, ":", 2)
if len(locs) != 2 && len(locs) != 3 {
panic(fmt.Errorf("invalid source position: %q", loc))
}
switch loc := locs[0]; loc {
case "line", "col":
default:
if !strings.HasPrefix(loc, p.sdkPath) {
panic(fmt.Errorf("invalid source position: %q", loc))
}
loc = loc[len(p.sdkPath):]
switch {
case strings.HasPrefix(loc, "/usr/include/objc/"):
p.module = "Foundation"
case strings.HasPrefix(loc, frameworksPath):
loc = loc[len(frameworksPath):]
i := strings.Index(loc, ".framework")
if i == -1 {
panic(fmt.Errorf("invalid source position: %q", loc))
}
p.module = loc[:i]
// Some types are declared in CoreFoundation.framework
// even though they belong in Foundation in Objective-C.
if p.module == "CoreFoundation" {
p.module = "Foundation"
}
default:
}
}
}
func (p *parser) parseLocation() {
// Source ranges are on the form: <line:17:29, line:64:2>.
if !strings.HasPrefix(p.decl, "<") {
panic(fmt.Errorf("1no source range first in %s", p.decl))
}
p.decl = p.decl[1:]
p.parseSrcPos()
if strings.HasPrefix(p.decl, ", ") {
p.decl = p.decl[2:]
p.parseSrcPos()
}
if !strings.HasPrefix(p.decl, "> ") {
panic(fmt.Errorf("no source range first in %s", p.decl))
}
p.decl = p.decl[2:]
p.parseSrcPos()
}
func (p *parser) scanWord() string {
i := 0
loop:
for ; i < len(p.decl); i++ {
switch p.decl[i] {
case ' ', '>', ',':
break loop
}
}
w := p.decl[:i]
p.decl = p.decl[i:]
for len(p.decl) > 0 && p.decl[0] == ' ' {
p.decl = p.decl[1:]
}
return w
}
func initialUpper(s string) string {
if s == "" {
return ""
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToUpper(r)) + s[n:]
}
func (t *Named) ObjcType() string {
if t.Protocol {
return fmt.Sprintf("id<%s> _Nullable", t.Name)
} else {
return t.Name + " * _Nullable"
}
}

View file

@ -0,0 +1,83 @@
// Copyright 2016 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 objc
import (
"reflect"
"runtime"
"testing"
"golang.org/x/mobile/internal/importers"
)
func TestImport(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skipf("can only parse objc types on darwin")
}
tests := []struct {
ref importers.PkgRef
name string
methods []*Func
}{
{
ref: importers.PkgRef{Pkg: "Foundation/NSObjectP", Name: "Hash"},
name: "NSObject",
methods: []*Func{
&Func{Sig: "hash", GoName: "Hash", Ret: &Type{Kind: Uint, Decl: "NSUInteger"}},
},
},
{
ref: importers.PkgRef{Pkg: "Foundation/NSString", Name: "StringWithContentsOfFileEncodingError"},
name: "NSString",
methods: []*Func{
&Func{
Sig: "stringWithContentsOfFile:encoding:error:",
GoName: "StringWithContentsOfFileEncodingError",
Params: []*Param{
&Param{Name: "path", Type: &Type{Kind: String, Decl: "NSString *"}},
&Param{Name: "enc", Type: &Type{Kind: Uint, Decl: "NSStringEncoding"}},
&Param{Name: "error", Type: &Type{Kind: Class, Name: "NSError", Decl: "NSError * * _Nullable", Indirect: true}},
},
Ret: &Type{Kind: 3, Decl: "NSString *"},
Static: true,
},
},
},
}
for _, test := range tests {
refs := &importers.References{
Refs: []importers.PkgRef{test.ref},
Names: make(map[string]struct{}),
}
for _, m := range test.methods {
refs.Names[m.GoName] = struct{}{}
}
types, err := Import(refs)
if err != nil {
t.Fatal(err)
}
if len(types) == 0 {
t.Fatalf("got no types, expected at least 1")
}
n := types[0]
if n.Name != test.name {
t.Errorf("got class name %s, expected %s", n.Name, test.name)
}
loop:
for _, exp := range test.methods {
for _, got := range n.AllMethods {
if reflect.DeepEqual(exp, got) {
continue loop
}
}
for _, got := range n.Funcs {
if reflect.DeepEqual(exp, got) {
continue loop
}
}
t.Errorf("failed to find method: %+v", exp)
}
}
}

View file

@ -0,0 +1,124 @@
// 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 mobileinit
/*
#include <jni.h>
#include <stdlib.h>
static char* lockJNI(JavaVM *vm, uintptr_t* envp, int* attachedp) {
JNIEnv* env;
if (vm == NULL) {
return "no current JVM";
}
*attachedp = 0;
switch ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6)) {
case JNI_OK:
break;
case JNI_EDETACHED:
if ((*vm)->AttachCurrentThread(vm, &env, 0) != 0) {
return "cannot attach to JVM";
}
*attachedp = 1;
break;
case JNI_EVERSION:
return "bad JNI version";
default:
return "unknown JNI error from GetEnv";
}
*envp = (uintptr_t)env;
return NULL;
}
static char* checkException(uintptr_t jnienv) {
jthrowable exc;
JNIEnv* env = (JNIEnv*)jnienv;
if (!(*env)->ExceptionCheck(env)) {
return NULL;
}
exc = (*env)->ExceptionOccurred(env);
(*env)->ExceptionClear(env);
jclass clazz = (*env)->FindClass(env, "java/lang/Throwable");
jmethodID toString = (*env)->GetMethodID(env, clazz, "toString", "()Ljava/lang/String;");
jobject msgStr = (*env)->CallObjectMethod(env, exc, toString);
return (char*)(*env)->GetStringUTFChars(env, msgStr, 0);
}
static void unlockJNI(JavaVM *vm) {
(*vm)->DetachCurrentThread(vm);
}
*/
import "C"
import (
"errors"
"runtime"
"unsafe"
)
// currentVM is stored to initialize other cgo packages.
//
// As all the Go packages in a program form a single shared library,
// there can only be one JNI_OnLoad function for initialization. In
// OpenJDK there is JNI_GetCreatedJavaVMs, but this is not available
// on android.
var currentVM *C.JavaVM
// currentCtx is Android's android.context.Context. May be NULL.
var currentCtx C.jobject
// SetCurrentContext populates the global Context object with the specified
// current JavaVM instance (vm) and android.context.Context object (ctx).
// The android.context.Context object must be a global reference.
func SetCurrentContext(vm unsafe.Pointer, ctx uintptr) {
currentVM = (*C.JavaVM)(vm)
currentCtx = (C.jobject)(ctx)
}
// RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv.
//
// RunOnJVM blocks until the call to fn is complete. Any Java
// exception or failure to attach to the JVM is returned as an error.
//
// The function fn takes vm, the current JavaVM*,
// env, the current JNIEnv*, and
// ctx, a jobject representing the global android.context.Context.
func RunOnJVM(fn func(vm, env, ctx uintptr) error) error {
errch := make(chan error)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
env := C.uintptr_t(0)
attached := C.int(0)
if errStr := C.lockJNI(currentVM, &env, &attached); errStr != nil {
errch <- errors.New(C.GoString(errStr))
return
}
if attached != 0 {
defer C.unlockJNI(currentVM)
}
vm := uintptr(unsafe.Pointer(currentVM))
if err := fn(vm, uintptr(env), uintptr(currentCtx)); err != nil {
errch <- err
return
}
if exc := C.checkException(env); exc != nil {
errch <- errors.New(C.GoString(exc))
C.free(unsafe.Pointer(exc))
return
}
errch <- nil
}()
return <-errch
}

View file

@ -0,0 +1,11 @@
// 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 mobileinit contains common initialization logic for mobile platforms
// that is relevant to both all-Go apps and gobind-based apps.
//
// Long-term, some code in this package should consider moving into Go stdlib.
package mobileinit
import "C"

View file

@ -0,0 +1,93 @@
// 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.
package mobileinit
/*
To view the log output run:
adb logcat GoLog:I *:S
*/
// Android redirects stdout and stderr to /dev/null.
// As these are common debugging utilities in Go,
// we redirect them to logcat.
//
// Unfortunately, logcat is line oriented, so we must buffer.
/*
#cgo LDFLAGS: -landroid -llog
#include <android/log.h>
#include <stdlib.h>
#include <string.h>
*/
import "C"
import (
"bufio"
"log"
"os"
"syscall"
"unsafe"
)
var (
ctag = C.CString("GoLog")
// Store the writer end of the redirected stderr and stdout
// so that they are not garbage collected and closed.
stderr, stdout *os.File
)
type infoWriter struct{}
func (infoWriter) Write(p []byte) (n int, err error) {
cstr := C.CString(string(p))
C.__android_log_write(C.ANDROID_LOG_INFO, ctag, cstr)
C.free(unsafe.Pointer(cstr))
return len(p), nil
}
func lineLog(f *os.File, priority C.int) {
const logSize = 1024 // matches android/log.h.
r := bufio.NewReaderSize(f, logSize)
for {
line, _, err := r.ReadLine()
str := string(line)
if err != nil {
str += " " + err.Error()
}
cstr := C.CString(str)
C.__android_log_write(priority, ctag, cstr)
C.free(unsafe.Pointer(cstr))
if err != nil {
break
}
}
}
func init() {
log.SetOutput(infoWriter{})
// android logcat includes all of log.LstdFlags
log.SetFlags(log.Flags() &^ log.LstdFlags)
r, w, err := os.Pipe()
if err != nil {
panic(err)
}
stderr = w
if err := syscall.Dup3(int(w.Fd()), int(os.Stderr.Fd()), 0); err != nil {
panic(err)
}
go lineLog(r, C.ANDROID_LOG_ERROR)
r, w, err = os.Pipe()
if err != nil {
panic(err)
}
stdout = w
if err := syscall.Dup3(int(w.Fd()), int(os.Stdout.Fd()), 0); err != nil {
panic(err)
}
go lineLog(r, C.ANDROID_LOG_INFO)
}

View file

@ -0,0 +1,41 @@
// 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 mobileinit
import (
"io"
"log"
"os"
"unsafe"
)
/*
#include <stdlib.h>
#include <os/log.h>
os_log_t create_os_log() {
return os_log_create("org.golang.mobile", "os_log");
}
void os_log_wrap(os_log_t log, const char *str) {
os_log(log, "%s", str);
}
*/
import "C"
type osWriter struct {
w C.os_log_t
}
func (o osWriter) Write(p []byte) (n int, err error) {
cstr := C.CString(string(p))
C.os_log_wrap(o.w, cstr)
C.free(unsafe.Pointer(cstr))
return len(p), nil
}
func init() {
log.SetOutput(io.MultiWriter(os.Stderr, osWriter{C.create_os_log()}))
}

View file

@ -0,0 +1,89 @@
// Copyright 2022 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 sdkpath provides functions for locating the Android SDK.
// These functions respect the ANDROID_HOME environment variable, and
// otherwise use the default SDK location.
package sdkpath
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
// AndroidHome returns the absolute path of the selected Android SDK,
// if one can be found.
func AndroidHome() (string, error) {
androidHome := os.Getenv("ANDROID_HOME")
if androidHome == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
switch runtime.GOOS {
case "windows":
// See https://android.googlesource.com/platform/tools/adt/idea/+/85b4bfb7a10ad858a30ffa4003085b54f9424087/native/installer/win/setup_android_studio.nsi#100
androidHome = filepath.Join(home, "AppData", "Local", "Android", "sdk")
case "darwin":
// See https://android.googlesource.com/platform/tools/asuite/+/67e0cd9604379e9663df57f16a318d76423c0aa8/aidegen/lib/ide_util.py#88
androidHome = filepath.Join(home, "Library", "Android", "sdk")
default: // Linux, BSDs, etc.
// See LINUX_ANDROID_SDK_PATH in ide_util.py above.
androidHome = filepath.Join(home, "Android", "Sdk")
}
}
if info, err := os.Stat(androidHome); err != nil {
return "", fmt.Errorf("%w; Android SDK was not found at %s", err, androidHome)
} else if !info.IsDir() {
return "", fmt.Errorf("%s is not a directory", androidHome)
}
return androidHome, nil
}
// AndroidAPIPath returns an android SDK platform directory within the configured SDK.
// If there are multiple platforms that satisfy the minimum version requirement,
// AndroidAPIPath returns the latest one among them.
func AndroidAPIPath(api int) (string, error) {
sdk, err := AndroidHome()
if err != nil {
return "", err
}
sdkDir, err := os.Open(filepath.Join(sdk, "platforms"))
if err != nil {
return "", fmt.Errorf("failed to find android SDK platform: %w", err)
}
defer sdkDir.Close()
fis, err := sdkDir.Readdir(-1)
if err != nil {
return "", fmt.Errorf("failed to find android SDK platform (API level: %d): %w", api, err)
}
var apiPath string
var apiVer int
for _, fi := range fis {
name := fi.Name()
if !strings.HasPrefix(name, "android-") {
continue
}
n, err := strconv.Atoi(name[len("android-"):])
if err != nil || n < api {
continue
}
p := filepath.Join(sdkDir.Name(), name)
_, err = os.Stat(filepath.Join(p, "android.jar"))
if err == nil && apiVer < n {
apiPath = p
apiVer = n
}
}
if apiVer == 0 {
return "", fmt.Errorf("failed to find android SDK platform (API level: %d) in %s",
api, sdkDir.Name())
}
return apiPath, nil
}