// 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) } } }