mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
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
174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
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)
|
|
}
|