defmodule GitGud.ApkFixtures do
@moduledoc """
Synthetic but structurally real APK pieces: a `resources.arsc` and a
zip with the alignment padding a real one carries.
Both are built rather than lifted, for the reason `GitGud.RpmFixtures`
gives: the real article is 400 KB of resource table or 13 MB of APK,
and the parsers read a few hundred bytes of structure out of either.
A generated fixture also lets a test say "two densities and an XML"
and get exactly that, which no real APK offers on request.
The parsers were developed against the real thing — this repo's own
`org.neiam.gitgud.app`, release-built and resource-shrunk — and these
reproduce its shape: an icon whose resource id resolves to several
files, one per density, plus the adaptive-icon XML that must not win.
"""
@doc """
A compiled `AndroidManifest.xml` with `android:icon` and
`android:label` as resource references, which is how a real one
carries them — the checked-in fixture from this repo's debug build
declares neither, so resolving them can't be tested against it.
Takes the two ids so a test can point them at whatever
`resources_arsc/0` defines.
"""
def manifest(icon_ref, label_ref) do
strings = [
"package",
"versionCode",
"icon",
"label",
"manifest",
"application",
"org.neiam.gitgud.app"
]
pool = string_pool(strings)
manifest_element =
element(4, [{0, {:string, 6}}, {1, {:int, 7}}])
application_element =
element(5, [{2, {:reference, icon_ref}}, {3, {:reference, label_ref}}])
body = pool <> manifest_element <> application_element
<<0x0003::little-16, 8::little-16, byte_size(body) + 8::little-32>> <> body
end
@doc """
A zip shaped like an APK: a compiled manifest, a resource table, and
the icon at the two-character path R8 leaves behind.
"""
def resource_shrunk_apk(icon_ref, label_ref, png) do
entries = [
{~c"AndroidManifest.xml", manifest(icon_ref, label_ref)},
{~c"resources.arsc", resources_arsc(icon_ref, label_ref)},
{~c"res/hi.png", png},
{~c"res/lo.png", "a lower density, which should lose"},
{~c"res/ad.xml", "<adaptive-icon/>, which is not servable"}
]
{:ok, {_name, zip}} = :zip.create(~c"shrunk.apk", entries, [:memory])
zip
end
@doc """
A `resources.arsc` where:
* `0x7F010000` is a file reference with three values — two PNGs at
different densities and an XML at `anydpi`
* `0x7F020000` is a string, the app label
Paths are the two-character names R8 rewrites resources to, because
that is what the reader has to cope with.
"""
def resources_arsc(icon_id \\ 0x7F010000, label_id \\ 0x7F020000) do
# The global pool holds every value: file paths and strings alike.
strings = ["res/lo.png", "res/hi.png", "res/ad.xml", "GitGud"]
pool = string_pool(strings)
# The icon: one entry, three configurations.
{icon_type, icon_entry} = {type_id(icon_id), entry_index(icon_id)}
icon =
[
type_chunk(icon_type, 160, [{icon_entry, 0}]),
type_chunk(icon_type, 640, [{icon_entry, 1}]),
type_chunk(icon_type, 0xFFFE, [{icon_entry, 2}])
]
|> IO.iodata_to_binary()
# The label, in the default configuration.
label = type_chunk(type_id(label_id), 0, [{entry_index(label_id), 3}])
package = package_chunk(0x7F, [icon, label])
# The header is 8 bytes of chunk header plus the package count, and
# `size` covers the whole table including it.
body = <<1::little-32>> <> pool <> package
<<0x0002::little-16, 12::little-16, byte_size(body) + 8::little-32>> <> body
end
@doc """
A zip holding `name` stored (uncompressed) with one byte of alignment
padding in its local extra field — the shape `zipalign` produces and
`:zip.unzip/2` raises on.
"""
def aligned_zip(name, data) do
name = to_string(name)
crc = :erlang.crc32(data)
extra = <<0>>
local =
<<0x04034B50::little-32, 20::little-16, 0::little-16, 0::little-16, 0::little-16,
0::little-16, crc::little-32, byte_size(data)::little-32, byte_size(data)::little-32,
byte_size(name)::little-16, byte_size(extra)::little-16>> <> name <> extra <> data
central =
<<0x02014B50::little-32, 20::little-16, 20::little-16, 0::little-16, 0::little-16,
0::little-16, 0::little-16, crc::little-32, byte_size(data)::little-32,
byte_size(data)::little-32, byte_size(name)::little-16, 0::little-16, 0::little-16,
0::little-16, 0::little-16, 0::little-32, 0::little-32>> <> name
eocd =
<<0x06054B50::little-32, 0::little-16, 0::little-16, 1::little-16, 1::little-16,
byte_size(central)::little-32, byte_size(local)::little-32, 0::little-16>>
local <> central <> eocd
end
defp type_id(resource_id), do: Bitwise.band(Bitwise.bsr(resource_id, 16), 0xFF)
defp entry_index(resource_id), do: Bitwise.band(resource_id, 0xFFFF)
# ── chunk builders ───────────────────────────────────────────────────
# RES_XML_START_ELEMENT: the node header, the attribute extension,
# then fixed-size attributes.
defp element(name_index, attrs) do
# ResXMLTree_node: the chunk header, a line number and a comment
# index — 16 bytes, which is where the attribute extension starts.
header_size = 16
attr_size = 20
ext =
<<0xFFFFFFFF::little-32, name_index::little-32, 20::little-16, attr_size::little-16,
length(attrs)::little-16, 0::little-16, 0::little-16, 0::little-16>>
body = Enum.map_join(attrs, &attribute/1)
size = header_size + byte_size(ext) + byte_size(body)
<<0x0102::little-16, header_size::little-16, size::little-32, 0::little-32, 0::little-32>> <>
ext <> body
end
# ns, name, raw value, then the typed value: 0x03 string, 0x10 int,
# 0x01 reference.
defp attribute({name_index, {kind, value}}) do
{type, raw} =
case kind do
:string -> {0x03, value}
:int -> {0x10, 0xFFFFFFFF}
:reference -> {0x01, 0xFFFFFFFF}
end
<<0xFFFFFFFF::little-32, name_index::little-32, raw::little-32, 8::little-16, 0, type,
value::little-32>>
end
# A UTF-8 string pool: offsets, then each string as
# {utf16 length, byte length, bytes, NUL}.
defp string_pool(strings) do
{data, offsets} =
Enum.reduce(strings, {<<>>, []}, fn string, {data, offsets} ->
encoded = <<String.length(string), byte_size(string)>> <> string <> <<0>>
{data <> encoded, [byte_size(data) | offsets]}
end)
offsets = offsets |> Enum.reverse() |> Enum.map(&<<&1::little-32>>) |> IO.iodata_to_binary()
data = pad4(data)
header_size = 28
strings_start = header_size + byte_size(offsets)
size = strings_start + byte_size(data)
<<0x0001::little-16, header_size::little-16, size::little-32, length(strings)::little-32,
0::little-32, 0x100::little-32, strings_start::little-32, 0::little-32>> <> offsets <> data
end
# A package, carrying the empty type and key pools a real one has
# between its header and its type chunks.
defp package_chunk(id, type_chunks) do
header_size = 284
inner = string_pool([]) <> string_pool([]) <> IO.iodata_to_binary(type_chunks)
size = header_size + byte_size(inner)
<<0x0200::little-16, header_size::little-16, size::little-32, id::little-32,
0::size(256)-unit(8), 0::little-32, 0::little-32, 0::little-32, 0::little-32>> <> inner
end
# One type in one configuration: `{entry index, global pool index}`
# pairs, each stored as a simple string-typed value.
defp type_chunk(type_id, density, entries) do
config = <<28::little-32, 0::little-32, 0::little-32, 0, 0, density::little-16>> <> <<0::96>>
header_size = 20 + byte_size(config)
count = entries |> Enum.map(&elem(&1, 0)) |> Enum.max() |> Kernel.+(1)
{offsets, data} =
Enum.reduce(0..(count - 1), {<<>>, <<>>}, fn index, {offsets, data} ->
case List.keyfind(entries, index, 0) do
{^index, value} ->
entry =
<<8::little-16, 0::little-16, 0::little-32, 8::little-16, 0, 0x03,
value::little-32>>
{offsets <> <<byte_size(data)::little-32>>, data <> entry}
nil ->
{offsets <> <<0xFFFFFFFF::little-32>>, data}
end
end)
entries_start = header_size + byte_size(offsets)
size = entries_start + byte_size(data)
<<0x0201::little-16, header_size::little-16, size::little-32, type_id, 0, 0::little-16,
count::little-32, entries_start::little-32>> <> config <> offsets <> data
end
defp pad4(data) do
case rem(byte_size(data), 4) do
0 -> data
n -> data <> :binary.copy(<<0>>, 4 - n)
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.7 KiB · text
History
b299654