Skip to content

Commit

Permalink
Add documentation, make AttrType* enum public
Browse files Browse the repository at this point in the history
  • Loading branch information
Tasssadar committed Dec 6, 2017
1 parent 7be627b commit 73b29ed
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 45 deletions.
12 changes: 12 additions & 0 deletions apkparser.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package apkparser parses AndroidManifest.xml and resources.arsc from Android APKs.
package apkparser

import (
Expand All @@ -14,6 +15,11 @@ type apkParser struct {
resources *ResourceTable
}

// Parse APK's Manifest, including resolving refences to resource values.
// encoder expects an XML encoder instance, like Encoder from encoding/xml package.
//
// zipErr != nil means the APK couldn't be opened. The manifest will be parsed
// even when resourcesErr != nil, just without reference resolving.
func ParseApk(path string, encoder ManifestEncoder) (zipErr, resourcesErr, manifestErr error) {
zip, zipErr := OpenZip(path)
if zipErr != nil {
Expand All @@ -25,6 +31,12 @@ func ParseApk(path string, encoder ManifestEncoder) (zipErr, resourcesErr, manif
return
}

// Parse APK's Manifest, including resolving refences to resource values.
// encoder expects an XML encoder instance, like Encoder from encoding/xml package.
//
// Use this if you already opened the zip with OpenZip before. This method will not Close() the zip.
//
// The manifest will be parsed even when resourcesErr != nil, just without reference resolving.
func ParseApkWithZip(zip *ZipReader, encoder ManifestEncoder) (resourcesErr, manifestErr error) {
p := apkParser{
zip: zip,
Expand Down
3 changes: 2 additions & 1 deletion axml2xml/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// This is a tool to extract AndroidManifest.xml from apks and verify their signatures.
package main

import (
Expand All @@ -18,7 +19,7 @@ func main() {
isApk := flag.Bool("a", false, "The input file is an apk (default if INPUT is *.apk)")
isManifest := flag.Bool("m", false, "The input file is an AndroidManifest.xml (default)")
isResources := flag.Bool("r", false, "The input is resources.arsc file (default if INPUT is *.arsc)")
verifyApk := flag.Bool("v", false, "Verify the file if it is an APK.")
verifyApk := flag.Bool("v", false, "Verify the file signature if it is an APK.")
cpuProfile := flag.String("cpuprofile", "", "Write cpu profiling info")
fileListPath := flag.String("l", "", "Process file list")

Expand Down
31 changes: 17 additions & 14 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,26 @@ const (
attrIdxData = 4
attrValuesCount = 5

attrTypeNull = 0x00
attrTypeReference = 0x01
attrTypeAttribute = 0x02
attrTypeString = 0x03
attrTypeFloat = 0x04
attrTypeIntDec = 0x10
attrTypeIntHex = 0x11
attrTypeIntBool = 0x12

attrTypeIntColorArgb8 = 0x1c
attrTypeIntColorRgb8 = 0x1d
attrTypeIntColorArgb4 = 0x1e
attrTypeIntColorRgb4 = 0x1f

chunkHeaderSize = (2 + 2 + 4)
)

type AttrType uint8

const (
AttrTypeNull AttrType = 0x00
AttrTypeReference = 0x01
AttrTypeAttribute = 0x02
AttrTypeString = 0x03
AttrTypeFloat = 0x04
AttrTypeIntDec = 0x10
AttrTypeIntHex = 0x11
AttrTypeIntBool = 0x12
AttrTypeIntColorArgb8 = 0x1c
AttrTypeIntColorRgb8 = 0x1d
AttrTypeIntColorArgb4 = 0x1e
AttrTypeIntColorRgb4 = 0x1f
)

func parseChunkHeader(r io.Reader) (id, headerLen uint16, len uint32, err error) {
if err = binary.Read(r, binary.LittleEndian, &id); err != nil {
return
Expand Down
1 change: 1 addition & 0 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apkparser

import "encoding/xml"

// Encoder for writing the XML data. For example Encoder from encoding/xml matches this interface.
type ManifestEncoder interface {
EncodeToken(t xml.Token) error
Flush() error
Expand Down
13 changes: 7 additions & 6 deletions manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type manifestParseInfo struct {
res *ResourceTable
}

// Parse the AndroidManifest.xml binary format. The resources are optional and can be nil.
func ParseManifest(r io.Reader, enc ManifestEncoder, resources *ResourceTable) error {
x := manifestParseInfo{
encoder: enc,
Expand Down Expand Up @@ -221,19 +222,19 @@ func (x *manifestParseInfo) parseTagStart(r *io.LimitedReader) error {
}

switch attrData[attrIdxType] >> 24 {
case attrTypeString:
case AttrTypeString:
attr.Value, err = x.strings.get(attrData[attrIdxString])
if err != nil {
return fmt.Errorf("error decoding attrStringIdx: %s", err.Error())
}
case attrTypeIntBool:
case AttrTypeIntBool:
attr.Value = strconv.FormatBool(attrData[attrIdxData] != 0)
case attrTypeIntHex:
case AttrTypeIntHex:
attr.Value = fmt.Sprintf("0x%x", attrData[attrIdxData])
case attrTypeFloat:
case AttrTypeFloat:
val := (*float32)(unsafe.Pointer(&attrData[attrIdxData]))
attr.Value = fmt.Sprintf("%g", *val)
case attrTypeReference:
case AttrTypeReference:
if x.res != nil {
cfg := ConfigFirst
if attr.Name.Local == "icon" {
Expand All @@ -242,7 +243,7 @@ func (x *manifestParseInfo) parseTagStart(r *io.LimitedReader) error {

e, err := x.res.GetResourceEntryEx(attrData[attrIdxData], cfg)
if err == nil {
for i := 0; e.value.dataType == attrTypeReference && i < 5; i++ {
for i := 0; e.value.dataType == AttrTypeReference && i < 5; i++ {
lower, err := x.res.GetResourceEntryEx(e.value.data, cfg)
if err != nil {
break
Expand Down
59 changes: 41 additions & 18 deletions resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

var ErrUnknownResourceDataType = errors.New("Unknown resource data type")

// Contains parsed resources.arsc file.
type ResourceTable struct {
mainStrings stringTable
nextPackageId uint32
Expand Down Expand Up @@ -62,6 +63,7 @@ const (
tableEntryWeak = 0x0004
)

// Describes one resource entry, for example @drawable/icon in the original XML, in one particular config option.
type ResourceEntry struct {
size uint16
flags uint16
Expand All @@ -73,21 +75,27 @@ type ResourceEntry struct {
value ResourceValue
}

// Handle to the resource's actual value.
type ResourceValue struct {
dataType uint8
dataType AttrType
data uint32

globalStringTable *stringTable
convertedData interface{}
}

// Resource config option to pick from options - when @drawable/icon is referenced,
// use /res/drawable-xhdpi/icon.png or use /res/drawable-mdpi/icon.png?
//
// This is not fully implemented, so you can pick only first seen or last seen option.
type ResourceConfigOption int

const (
ConfigFirst ResourceConfigOption = iota
ConfigLast
ConfigFirst ResourceConfigOption = iota // Usually the smallest
ConfigLast // Usually the biggest
)

// Parses the resources.arsc file
func ParseResourceTable(r io.Reader) (*ResourceTable, error) {
res := ResourceTable{
nextPackageId: 2,
Expand Down Expand Up @@ -370,6 +378,7 @@ func (x *ResourceTable) parseType(r io.Reader, pkg *resourcePackage, group *pack
return nil
}

// Converts the resource id to readable name including the package name like "@drawable:com.example.app.icon".
func (x *ResourceTable) GetResourceName(resId uint32) (string, error) {
pkgId := (resId >> 24)
typ := ((resId >> 16) & 0xFF) - 1
Expand All @@ -388,10 +397,12 @@ func (x *ResourceTable) GetResourceName(resId uint32) (string, error) {
return fmt.Sprintf("@%s:%s.%s", entry.ResourceType, group.Name, entry.Key), nil
}

// Returns the resource entry for resId and the first configuration option it finds.
func (x *ResourceTable) GetResourceEntry(resId uint32) (*ResourceEntry, error) {
return x.GetResourceEntryEx(resId, ConfigFirst)
}

// Returns the resource entry for resId and config configuration option.
func (x *ResourceTable) GetResourceEntryEx(resId uint32, config ResourceConfigOption) (*ResourceEntry, error) {
pkgId := (resId >> 24)
typ := ((resId >> 16) & 0xFF) - 1
Expand Down Expand Up @@ -517,22 +528,31 @@ func (x *ResourceTable) parseEntry(r io.Reader, pkg *resourcePackage, typeId uin
return &res, nil
}

// Returns true if the resource entry is complex (for example arrays, string plural arrays...).
//
// Complex ResourceEntries are not yet supported.
func (e *ResourceEntry) IsComplex() bool {
return (e.flags & tableEntryComplex) != 0
}

// Returns the resource value handle
func (e *ResourceEntry) GetValue() *ResourceValue {
return &e.value
}

func (v *ResourceValue) Type() uint8 {
// Returns the resource data type
func (v *ResourceValue) Type() AttrType {
return v.dataType
}

// Returns the raw data of the resource
func (v *ResourceValue) RawData() uint32 {
return v.data
}

// Returns the data converted to their native type (e.g. AttrTypeString to string).
//
// Returns ErrUnknownResourceDataType if the type is not handled by this library
func (v *ResourceValue) Data() (interface{}, error) {
if v.convertedData != nil {
return v.convertedData, nil
Expand All @@ -541,13 +561,13 @@ func (v *ResourceValue) Data() (interface{}, error) {
var val interface{}
var err error
switch v.dataType {
case attrTypeNull:
case attrTypeString:
case AttrTypeNull:
case AttrTypeString:
val, err = v.globalStringTable.get(v.data)
case attrTypeIntDec, attrTypeIntHex, attrTypeIntBool,
attrTypeIntColorArgb8, attrTypeIntColorRgb8,
attrTypeIntColorArgb4, attrTypeIntColorRgb4,
attrTypeReference:
case AttrTypeIntDec, AttrTypeIntHex, AttrTypeIntBool,
AttrTypeIntColorArgb8, AttrTypeIntColorRgb8,
AttrTypeIntColorArgb4, AttrTypeIntColorRgb4,
AttrTypeReference:
val = v.data
default:
return nil, ErrUnknownResourceDataType
Expand All @@ -561,27 +581,30 @@ func (v *ResourceValue) Data() (interface{}, error) {
return val, nil
}

// Returns the data converted to a readable string, to the format it was likely in the original AndroidManifest.xml.
//
// Unknown data types are returned as the string from ErrUnknownResourceDataType.Error().
func (v *ResourceValue) String() string {
switch v.dataType {
case attrTypeNull:
case AttrTypeNull:
return "null"
case attrTypeIntHex:
case AttrTypeIntHex:
return fmt.Sprintf("0x%x", v.data)
case attrTypeIntBool:
case AttrTypeIntBool:
if v.data != 0 {
return "true"
} else {
return "false"
}
case attrTypeIntColorArgb8:
case AttrTypeIntColorArgb8:
return fmt.Sprintf("#%08x", v.data)
case attrTypeIntColorRgb8:
case AttrTypeIntColorRgb8:
return fmt.Sprintf("#%06x", v.data)
case attrTypeIntColorArgb4:
case AttrTypeIntColorArgb4:
return fmt.Sprintf("#%04x", v.data)
case attrTypeIntColorRgb4:
case AttrTypeIntColorRgb4:
return fmt.Sprintf("#%03x", v.data)
case attrTypeReference:
case AttrTypeReference:
return fmt.Sprintf("@%x", v.data)
default:
val, err := v.Data()
Expand Down
24 changes: 18 additions & 6 deletions zipreader.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ type zipReaderFileSubEntry struct {
method uint16
}

// This struct mimics of Reader from archive/zip. It's purpose is to handle
// even broken archives that Android can read, but archive/zip cannot.
type ZipReader struct {
File map[string]*ZipReaderFile

zipFile *os.File
}

// This struct mimics of File from archive/zip. The main difference is it can represent
// multiple actual entries in the ZIP file in case it has more than one with the same name.
type ZipReaderFile struct {
Name string
IsDir bool
Expand All @@ -29,12 +39,8 @@ type ZipReaderFile struct {
curEntry int
}

type ZipReader struct {
File map[string]*ZipReaderFile

zipFile *os.File
}

// Opens the file(s) for reading. After calling open, you should iterate through all possible entries that
// go by that Filename with for f.Next() { f.Read()... }
func (zr *ZipReaderFile) Open() error {
if zr.internalReader != nil {
return errors.New("File is already opened.")
Expand All @@ -52,6 +58,8 @@ func (zr *ZipReaderFile) Open() error {
return nil
}

// Reads data from current opened file. Returns io.EOF at the end of current file, but another file entry might exist.
// Use Next() to check for that.
func (zr *ZipReaderFile) Read(p []byte) (int, error) {
if zr.internalReader == nil {
if zr.curEntry == -1 && !zr.Next() {
Expand All @@ -77,6 +85,7 @@ func (zr *ZipReaderFile) Read(p []byte) (int, error) {
return zr.internalReader.Read(p)
}

// Moves this reader to the next file represented under it's Name. Returns false if there are no more to read.
func (zr *ZipReaderFile) Next() bool {
if len(zr.entries) == 0 && zr.internalReader != nil {
zr.curEntry++
Expand All @@ -97,6 +106,7 @@ func (zr *ZipReaderFile) Next() bool {
return true
}

// Closes this reader and all opened files.
func (zr *ZipReaderFile) Close() error {
if zr.internalReader != nil {
if zr.internalReader != zr.zipFile {
Expand All @@ -107,6 +117,7 @@ func (zr *ZipReaderFile) Close() error {
return nil
}

// Closes this ZIP archive and all it's ZipReaderFile entries.
func (zr *ZipReader) Close() error {
if zr.zipFile == nil {
return nil
Expand All @@ -121,6 +132,7 @@ func (zr *ZipReader) Close() error {
return err
}

// Attempts to open ZIP for reading.
func OpenZip(zippath string) (zr *ZipReader, err error) {
f, err := os.Open(zippath)
if err != nil {
Expand Down

0 comments on commit 73b29ed

Please sign in to comment.