| // 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/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 |
| ) |
| |
| 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: "15", |
| }, |
| xml.Attr{ |
| Name: xml.Name{ |
| Space: "", |
| Local: "platformBuildVersionName", |
| }, |
| Value: "4.0.4-1406430", |
| }) |
| |
| 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 preceeds "" 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 |
| } |