mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(android): read the application id out of an apk
parses the compiled AndroidManifest.xml rather than shelling out to aapt2, which lives in the versioned build-tools directory that hosts with only platform-tools never install. Claude-Session: https://claude.ai/code/session_012PVErdr3ZzyUASeVQDWsUc
This commit is contained in:
1 parent
8c0df6266b
commit
51d3504e9a
3 files changed
+401
No files matched your search
@@ -0,0 +1,228 @@
|
|||||||
|
package android
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"unicode/utf16"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
manifestEntry = "AndroidManifest.xml"
|
||||||
|
|
||||||
|
chunkStringPool = 0x0001
|
||||||
|
chunkStartElement = 0x0102
|
||||||
|
|
||||||
|
stringPoolUTF8 = 1 << 8
|
||||||
|
)
|
||||||
|
|
||||||
|
var errStringPoolRange = errors.New("string pool entry runs past the chunk")
|
||||||
|
|
||||||
|
// PackageName reads an APK's application id out of its compiled manifest,
|
||||||
|
// which carries the id the package manager will know the app by, suffixes and
|
||||||
|
// all. Parsing it here rather than shelling out to `aapt2 dump packagename`
|
||||||
|
// keeps the read working on the many hosts that install platform-tools for adb
|
||||||
|
// and never install the versioned build-tools directory aapt2 lives in.
|
||||||
|
func PackageName(apkPath string) (string, error) {
|
||||||
|
manifest, err := manifestFromAPK(apkPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
name, err := manifestPackage(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: %w", apkPath, err)
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestFromAPK(apkPath string) ([]byte, error) {
|
||||||
|
archive, err := zip.OpenReader(apkPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open %s: %w", apkPath, err)
|
||||||
|
}
|
||||||
|
defer archive.Close()
|
||||||
|
for _, file := range archive.File {
|
||||||
|
if file.Name != manifestEntry {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open %s in %s: %w", manifestEntry, apkPath, err)
|
||||||
|
}
|
||||||
|
defer entry.Close()
|
||||||
|
manifest, err := io.ReadAll(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read %s in %s: %w", manifestEntry, apkPath, err)
|
||||||
|
}
|
||||||
|
return manifest, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s holds no %s", apkPath, manifestEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manifestPackage walks the compiled manifest's chunks for the <manifest>
|
||||||
|
// element and reports its package attribute. The string pool always precedes
|
||||||
|
// the elements that index into it.
|
||||||
|
func manifestPackage(manifest []byte) (string, error) {
|
||||||
|
if len(manifest) < 8 {
|
||||||
|
return "", errors.New("truncated binary xml")
|
||||||
|
}
|
||||||
|
var pool []string
|
||||||
|
for offset := uint32(8); offset+8 <= uint32(len(manifest)); {
|
||||||
|
size := binary.LittleEndian.Uint32(manifest[offset+4:])
|
||||||
|
if size < 8 || uint64(offset)+uint64(size) > uint64(len(manifest)) {
|
||||||
|
return "", errors.New("truncated chunk in binary xml")
|
||||||
|
}
|
||||||
|
chunk := manifest[offset : offset+size]
|
||||||
|
switch binary.LittleEndian.Uint16(chunk) {
|
||||||
|
case chunkStringPool:
|
||||||
|
var err error
|
||||||
|
if pool, err = poolStrings(chunk); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
case chunkStartElement:
|
||||||
|
name, found, err := packageAttribute(chunk, pool)
|
||||||
|
if err != nil || found {
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += size
|
||||||
|
}
|
||||||
|
return "", errors.New("no <manifest> element in binary xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageAttribute reports the package attribute of a start-element chunk, and
|
||||||
|
// whether that chunk was the <manifest> element at all.
|
||||||
|
func packageAttribute(chunk []byte, pool []string) (string, bool, error) {
|
||||||
|
// The element's own fields start after the chunk header, line number and
|
||||||
|
// comment index, and the attribute offsets are relative to there.
|
||||||
|
const element = 16
|
||||||
|
if len(chunk) < element+20 {
|
||||||
|
return "", false, errors.New("truncated start-element chunk")
|
||||||
|
}
|
||||||
|
if poolString(pool, binary.LittleEndian.Uint32(chunk[element+4:])) != "manifest" {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
start := uint64(binary.LittleEndian.Uint16(chunk[element+8:]))
|
||||||
|
stride := uint64(binary.LittleEndian.Uint16(chunk[element+10:]))
|
||||||
|
count := uint64(binary.LittleEndian.Uint16(chunk[element+12:]))
|
||||||
|
if stride < 20 {
|
||||||
|
return "", false, fmt.Errorf("<manifest> attribute stride is %d bytes, want at least 20", stride)
|
||||||
|
}
|
||||||
|
for index := uint64(0); index < count; index++ {
|
||||||
|
at := element + start + index*stride
|
||||||
|
if at+20 > uint64(len(chunk)) {
|
||||||
|
return "", false, errors.New("<manifest> attribute runs past the chunk")
|
||||||
|
}
|
||||||
|
if poolString(pool, binary.LittleEndian.Uint32(chunk[at+4:])) != "package" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := poolString(pool, binary.LittleEndian.Uint32(chunk[at+8:]))
|
||||||
|
if name == "" {
|
||||||
|
// aapt2 leaves the raw value unset and keeps the string in the
|
||||||
|
// typed value's data word.
|
||||||
|
name = poolString(pool, binary.LittleEndian.Uint32(chunk[at+16:]))
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return "", false, errors.New("<manifest> package attribute is empty")
|
||||||
|
}
|
||||||
|
return name, true, nil
|
||||||
|
}
|
||||||
|
return "", false, errors.New("<manifest> has no package attribute")
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolString(pool []string, index uint32) string {
|
||||||
|
if uint64(index) >= uint64(len(pool)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return pool[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolStrings(chunk []byte) ([]string, error) {
|
||||||
|
const header = 28
|
||||||
|
if len(chunk) < header {
|
||||||
|
return nil, errors.New("truncated string pool")
|
||||||
|
}
|
||||||
|
count := uint64(binary.LittleEndian.Uint32(chunk[8:]))
|
||||||
|
flags := binary.LittleEndian.Uint32(chunk[16:])
|
||||||
|
start := uint64(binary.LittleEndian.Uint32(chunk[20:]))
|
||||||
|
if header+4*count > uint64(len(chunk)) {
|
||||||
|
return nil, errors.New("string pool offsets run past the chunk")
|
||||||
|
}
|
||||||
|
pool := make([]string, count)
|
||||||
|
for index := uint64(0); index < count; index++ {
|
||||||
|
at := start + uint64(binary.LittleEndian.Uint32(chunk[header+4*index:]))
|
||||||
|
value, err := poolString8Or16(chunk, at, flags&stringPoolUTF8 != 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pool[index] = value
|
||||||
|
}
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolString8Or16(chunk []byte, at uint64, utf8 bool) (string, error) {
|
||||||
|
if utf8 {
|
||||||
|
// The character count precedes the byte count; only the second governs
|
||||||
|
// how far the string reaches.
|
||||||
|
at, _, err := prefixLength8(chunk, at)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
at, length, err := prefixLength8(chunk, at)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if at+length > uint64(len(chunk)) {
|
||||||
|
return "", errStringPoolRange
|
||||||
|
}
|
||||||
|
return string(chunk[at : at+length]), nil
|
||||||
|
}
|
||||||
|
at, length, err := prefixLength16(chunk, at)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if at+2*length > uint64(len(chunk)) {
|
||||||
|
return "", errStringPoolRange
|
||||||
|
}
|
||||||
|
units := make([]uint16, length)
|
||||||
|
for index := range units {
|
||||||
|
units[index] = binary.LittleEndian.Uint16(chunk[at+2*uint64(index):])
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(units)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixLength8 reads a UTF-8 pool string's length prefix: one byte, or two
|
||||||
|
// when the high bit marks the value as wider. It returns the offset past the
|
||||||
|
// prefix.
|
||||||
|
func prefixLength8(chunk []byte, at uint64) (uint64, uint64, error) {
|
||||||
|
if at >= uint64(len(chunk)) {
|
||||||
|
return 0, 0, errStringPoolRange
|
||||||
|
}
|
||||||
|
length := uint64(chunk[at])
|
||||||
|
if length&0x80 == 0 {
|
||||||
|
return at + 1, length, nil
|
||||||
|
}
|
||||||
|
if at+1 >= uint64(len(chunk)) {
|
||||||
|
return 0, 0, errStringPoolRange
|
||||||
|
}
|
||||||
|
return at + 2, (length&0x7f)<<8 | uint64(chunk[at+1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixLength16 reads a UTF-16 pool string's length prefix, in units rather
|
||||||
|
// than bytes: one 16-bit word, or two when the high bit marks the value as
|
||||||
|
// wider.
|
||||||
|
func prefixLength16(chunk []byte, at uint64) (uint64, uint64, error) {
|
||||||
|
if at+2 > uint64(len(chunk)) {
|
||||||
|
return 0, 0, errStringPoolRange
|
||||||
|
}
|
||||||
|
length := uint64(binary.LittleEndian.Uint16(chunk[at:]))
|
||||||
|
if length&0x8000 == 0 {
|
||||||
|
return at + 2, length, nil
|
||||||
|
}
|
||||||
|
if at+4 > uint64(len(chunk)) {
|
||||||
|
return 0, 0, errStringPoolRange
|
||||||
|
}
|
||||||
|
return at + 4, (length&0x7fff)<<16 | uint64(binary.LittleEndian.Uint16(chunk[at+2:])), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package android
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// folioAPK is the folio debug APK stripped to the one entry the reader looks
|
||||||
|
// at, so the parse runs against a manifest aapt2 really produced.
|
||||||
|
const folioAPK = "testdata/folio.apk"
|
||||||
|
|
||||||
|
func TestPackageName_ReadsCompiledManifest(t *testing.T) {
|
||||||
|
name, err := PackageName(folioAPK)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if name != "app.folio" {
|
||||||
|
t.Fatalf("package name: got %q, want app.folio", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageName_UTF8StringPool(t *testing.T) {
|
||||||
|
name, err := PackageName(apkWithManifest(t, utf8PooledManifest("com.example.utf8")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if name != "com.example.utf8" {
|
||||||
|
t.Fatalf("package name: got %q, want com.example.utf8", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageName_NotAnAPK(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "app.apk")
|
||||||
|
if err := os.WriteFile(path, []byte("this is not a zip"), 0o600); err != nil {
|
||||||
|
t.Fatalf("write file: %v", err)
|
||||||
|
}
|
||||||
|
_, err := PackageName(path)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), path) {
|
||||||
|
t.Fatalf("expected an error naming %s, got %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageName_NoManifestEntry(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "app.apk")
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create apk: %v", err)
|
||||||
|
}
|
||||||
|
archive := zip.NewWriter(file)
|
||||||
|
if _, err := archive.Create("classes.dex"); err != nil {
|
||||||
|
t.Fatalf("write entry: %v", err)
|
||||||
|
}
|
||||||
|
if err := archive.Close(); err != nil {
|
||||||
|
t.Fatalf("close archive: %v", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
t.Fatalf("close apk: %v", err)
|
||||||
|
}
|
||||||
|
_, err = PackageName(path)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "AndroidManifest.xml") {
|
||||||
|
t.Fatalf("expected a missing-manifest error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageName_TruncatedManifest(t *testing.T) {
|
||||||
|
manifest, err := manifestFromAPK(folioAPK)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture manifest: %v", err)
|
||||||
|
}
|
||||||
|
for _, keep := range []int{4, 40, 400} {
|
||||||
|
if _, err := PackageName(apkWithManifest(t, manifest[:keep])); err == nil {
|
||||||
|
t.Fatalf("expected an error from a manifest cut to %d bytes", keep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apkWithManifest(t *testing.T, manifest []byte) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "app.apk")
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create apk: %v", err)
|
||||||
|
}
|
||||||
|
archive := zip.NewWriter(file)
|
||||||
|
entry, err := archive.Create(manifestEntry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create manifest entry: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := entry.Write(manifest); err != nil {
|
||||||
|
t.Fatalf("write manifest entry: %v", err)
|
||||||
|
}
|
||||||
|
if err := archive.Close(); err != nil {
|
||||||
|
t.Fatalf("close archive: %v", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
t.Fatalf("close apk: %v", err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// utf8PooledManifest encodes the smallest manifest that carries a package
|
||||||
|
// name, with the UTF-8 string pool that aapt2 does not emit and older builders
|
||||||
|
// do. Without it the UTF-8 decoding path is never exercised: the compiled
|
||||||
|
// manifests aapt2 writes all pool their strings as UTF-16.
|
||||||
|
func utf8PooledManifest(packageName string) []byte {
|
||||||
|
pool := []string{"manifest", "package", packageName}
|
||||||
|
var pooled bytes.Buffer
|
||||||
|
offsets := make([]uint32, len(pool))
|
||||||
|
for index, value := range pool {
|
||||||
|
offsets[index] = uint32(pooled.Len())
|
||||||
|
pooled.WriteByte(byte(len(value)))
|
||||||
|
pooled.WriteByte(byte(len(value)))
|
||||||
|
pooled.WriteString(value)
|
||||||
|
pooled.WriteByte(0)
|
||||||
|
}
|
||||||
|
stringsStart := uint32(28 + 4*len(pool))
|
||||||
|
|
||||||
|
var poolChunk bytes.Buffer
|
||||||
|
write16(&poolChunk, chunkStringPool)
|
||||||
|
write16(&poolChunk, 28)
|
||||||
|
write32(&poolChunk, stringsStart+uint32(pooled.Len()))
|
||||||
|
write32(&poolChunk, uint32(len(pool)))
|
||||||
|
write32(&poolChunk, 0)
|
||||||
|
write32(&poolChunk, stringPoolUTF8)
|
||||||
|
write32(&poolChunk, stringsStart)
|
||||||
|
write32(&poolChunk, 0)
|
||||||
|
for _, offset := range offsets {
|
||||||
|
write32(&poolChunk, offset)
|
||||||
|
}
|
||||||
|
poolChunk.Write(pooled.Bytes())
|
||||||
|
|
||||||
|
var element bytes.Buffer
|
||||||
|
write16(&element, chunkStartElement)
|
||||||
|
write16(&element, 16)
|
||||||
|
write32(&element, 36+20)
|
||||||
|
write32(&element, 1) // line number
|
||||||
|
write32(&element, 0xFFFFFFFF) // comment
|
||||||
|
write32(&element, 0xFFFFFFFF) // namespace
|
||||||
|
write32(&element, 0) // name: "manifest"
|
||||||
|
write16(&element, 20) // attributes start, past this header
|
||||||
|
write16(&element, 20) // attribute stride
|
||||||
|
write16(&element, 1) // attribute count
|
||||||
|
write16(&element, 0) // id index
|
||||||
|
write16(&element, 0) // class index
|
||||||
|
write16(&element, 0) // style index
|
||||||
|
write32(&element, 0xFFFFFFFF) // attribute namespace
|
||||||
|
write32(&element, 1) // attribute name: "package"
|
||||||
|
write32(&element, 2) // attribute raw value: the package name
|
||||||
|
write16(&element, 8) // typed value size
|
||||||
|
write16(&element, 3<<8) // res0, and TYPE_STRING
|
||||||
|
write32(&element, 2)
|
||||||
|
|
||||||
|
var manifest bytes.Buffer
|
||||||
|
write16(&manifest, 3)
|
||||||
|
write16(&manifest, 8)
|
||||||
|
write32(&manifest, uint32(8+poolChunk.Len()+element.Len()))
|
||||||
|
manifest.Write(poolChunk.Bytes())
|
||||||
|
manifest.Write(element.Bytes())
|
||||||
|
return manifest.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func write16(buffer *bytes.Buffer, value uint16) {
|
||||||
|
_ = binary.Write(buffer, binary.LittleEndian, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write32(buffer *bytes.Buffer, value uint32) {
|
||||||
|
_ = binary.Write(buffer, binary.LittleEndian, value)
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
Reference in new issue
Block a user