This commit is contained in:
relikd
2025-11-25 22:46:14 +01:00
commit a28636a7ca
28 changed files with 2380 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 relikd
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

23
Package.swift Normal file
View File

@@ -0,0 +1,23 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "AndroidXML",
products: [
.library(
name: "AndroidXML",
targets: ["AndroidXML"]
),
],
targets: [
.target(
name: "AndroidXML"
),
.testTarget(
name: "AndroidXMLTests",
dependencies: ["AndroidXML"]
),
],
)

90
README.md Normal file
View File

@@ -0,0 +1,90 @@
# AndroidXML
Pure Swift library for parsing binary XML files such as found in Android apk files.
## About
Written from scratch and closely following the original C++ source code ([ResourceTypes.h], [ResourceTypes.cpp]).
Names have been adapted to a more Swiftly syntax.
Import of `Foundation` is optional which should makes the library cross-platform compatible (though I have not tested other than macOS).
Since the AXML format is not documented anywhere apart from the source code you cannot rely on the structure to work forever.
Especially, pay attention to other binary XML formats which have [been spotted](https://www.cclsolutionsgroup.com/post/android-abx-binary-xml) on Android.
I do not think ABX is currently used for `.apk` files, only system files.
However, If that should change I would need to make some adjustments to this library.
## Example Usage
### Automatic XML String Conversion
```swift
let parser = try AndroidXML(path: "some/AndroidManifest.xml").parseXml()
print(try parser.xmlString())
```
### Manual XML Parser
```swift
let xml = try! AndroidXML(data: [...]).parseXml()
try xml.iterElements({ startTag, attributes in
print("<\(startTag)>")
for (name, val) in attributes {
print("\t", name, "=", val.resolve(xml.stringPool))
}
}) { endTag in
print("</\(endTag)>")
}
```
### Resource Table Lookup
```swift
let lookupId = 0x7F100000
let xml = try! AndroidXML(path: "some/resources.arsc").parseTable()
let res = try! xml.getResource(TblTableRef(lookupId))!
let pkgPool = res.package.stringPool(for: .Keys)!
for x in res.entries {
let val = x.entry.value!
print(pkgPool.getString(x.entry.key), val.resolve(xml.stringPool), x.config.screenType.density)
}
```
### Manual Table Parsing
```swift
let xml = try AndroidXML(path: "some/resources.arsc").parseTable()
for pkg in try xml.getPackages() {
let pool = pkg.stringPool(for: .Keys)!
try pkg.iterTypes { spec, types in
//print("----- \(spec.id) ------")
for type in types {
try type.iterValues { idx, entry in
if entry.isComplex {
//print("::.::.complex", pool.getString(entry.key), entry.valueMap!.parent.asHex)
for x in entry.valueMap!.entries {
let _ = x.name.asHex
let _ = x.value.resolve(xml.stringPool)
}
} else {
let _ = pool.getString(entry.key)
let _ = entry.value!.resolve(xml.stringPool)
}
}
}
}
}
```
## Developer Note
There are a few places I could not properly test becaues none of my test files triggered the condition.
For example, handling dynamic attributes and dynamic references.
I've added some `assert` (ignored in release builds) to trigger as soon as a specific condition is met.
If you encounter a file, which triggers such a condition, reach out so I can finalize the library.
[ResourceTypes.h]: https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
[ResourceTypes.cpp]: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/ResourceTypes.cpp

View File

@@ -0,0 +1,61 @@
#if canImport(Foundation)
import Foundation
#endif
// see:
// https://android.googlesource.com/platform/frameworks/base/+/master/libs/androidfw/include/androidfw/ResourceTypes.h
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/ResourceTypes.cpp
// or:
// https://github.com/aosp-mirror/platform_frameworks_base/blob/master/libs/androidfw/include/androidfw/ResourceTypes.h
// https://github.com/aosp-mirror/platform_frameworks_base/blob/master/libs/androidfw/ResourceTypes.cpp
public struct AndroidXML {
private let _bytes: RawBytes
/// Document type. Either `.XmlTree` or `.Table
public let type: DocumentType
public enum DocumentType {
case XmlTree
case Table
}
#if canImport(Foundation)
/// Convenience init for `init(_: URL)` using `URL(fileURLWithPath: path)`
public init(path: String) throws {
try self.init(url: URL(fileURLWithPath: path))
}
/// Convenience init for `init(_: RawBytes)` using `Data(contentsOf: url)`
public init(url: URL) throws {
try self.init(data: RawBytes(try Data(contentsOf: url)))
}
#endif
/// Throws exception if `bytes` does not start with `.XmlTree` or `.Table` header.
public init(data: RawBytes) throws {
// TODO: check if other headers are possible
let firstChunk = try? ChunkHeader(0, data)
switch firstChunk?.type {
case .XmlTree: type = .XmlTree
case .Table: type = .Table
default:
throw AXMLError("Unsupported binary XML format. Document must start with .XmlTree or .Table header.")
}
guard firstChunk!.chunkSize == data.count else {
throw AXMLError("Data length != XML-defined data length")
}
_bytes = data
}
// MARK: - Public methods
/// `parseXml()` is only valid for `.XmlTree` documents
public func parseXml() -> Xml_Parser {
precondition(type == .XmlTree, "parseXml() is only valid for .XmlTree documents")
return try! Xml_Parser(_bytes)
}
/// `parseTable()` is only valid for `.Table` documents
public func parseTable() -> Tbl_Parser {
precondition(type == .Table, "parseTable() is only valid for .Table documents")
return try! Tbl_Parser(_bytes)
}
}

View File

@@ -0,0 +1,48 @@
/// Index offset over all data (subscript access on `Data`)
public typealias Index = Int
struct AXMLError: Error {
let description: String
init(_ description: String) {
self.description = description
}
}
#if canImport(Foundation)
import Foundation
extension AXMLError: LocalizedError {
var errorDescription: String? { description }
}
#endif
// MARK: - ChunkType
/// These are standard types that are shared between multiple specific resource types.
enum ChunkType: UInt16 {
case Null = 0x0000
case StringPool = 0x0001
case Table = 0x0002
case XmlTree = 0x0003
// Chunk types of XML
case XmlStartNamespace = 0x0100
case XmlEndNamespace = 0x0101
case XmlStartElement = 0x0102
case XmlEndElement = 0x0103
case XmlCDATA = 0x0104
/// This contains a `UInt32` array mapping strings in the string pool back to resource identifiers.
/// It is optional.
case XmlResourceMap = 0x0180
// Chunk types of TABLE
case TblPackage = 0x0200
case TblType = 0x0201
case TblTypeSpec = 0x0202
case TblLibrary = 0x0203
case TblOverlayable = 0x0204
case TblOverlayablePolicy = 0x0205
case TblStagedAlias = 0x0206
}

View File

@@ -0,0 +1,81 @@
/**
* Header that appears at the front of every data chunk in a resource.
*/
/// Header size: `8 Bytes`
struct ChunkHeader {
// INTERNAL USAGE. No correspondence in data:
private let _offset: Index
internal let _bytes: RawBytes
/// Type identifier for this chunk. The meaning of this value depends on the containing chunk.
let type: ChunkType // UInt16
/// Size of the chunk header (in bytes).
/// Adding this value to the address of the chunk allows you to find its associated data (if any).
let headerSize: UInt16
/// Total size of this chunk (in bytes).
/// This is the chunkSize plus the size of any data associated with the chunk.
/// Adding this value to the chunk allows you to completely skip its contents (including any child chunks).
/// If this value is the same as chunkSize, there is no data associated with the chunk.
let chunkSize: UInt32
init(_ idx: Index, _ bytes: RawBytes, expect: ChunkType? = nil) throws {
let rawType = bytes.int16(idx)
guard let typ = ChunkType(rawValue: rawType) else {
throw AXMLError("Unexpected chunk type \(rawType)")
}
if let expect, expect != typ {
throw AXMLError("Expected chunk type \(expect) but got \(typ)")
}
_offset = idx
_bytes = bytes
type = typ
headerSize = bytes.int16(idx + 2)
chunkSize = bytes.int32(idx + 4)
}
/// Go over all child elements by iterating over them one-by-one
func iterChildren(filter: ((ChunkHeader) -> Bool)? = nil, _ block: (_ abort: inout Bool, _ chunk: ChunkHeader) throws -> Void) throws {
var nextIdx = index(.startOfData)
let lastIdx = index(.afterChunk)
while nextIdx < lastIdx {
let child = try ChunkHeader(nextIdx, _bytes)
nextIdx = child.index(.afterChunk)
guard filter?(child) ?? true else {
continue
}
var abort = false
try block(&abort, child)
if abort {
break
}
}
}
/// Collect all children with `iterChildren` and return full list
func children(filter: ((ChunkHeader) -> Bool)? = nil) throws -> [ChunkHeader] {
var result: [ChunkHeader] = []
try iterChildren(filter: filter) { result.append($1) }
return result
}
/// Convert relative offset to absolute offset
func index(_ relative: RelativeIndex) -> Index {
switch relative {
case .startOfChunk: _offset
case .startOfHeader: _offset + 8 // chunk header length
case .startOfData: _offset + Index(headerSize)
case .afterChunk: _offset + Index(chunkSize)
}
}
enum RelativeIndex {
case startOfChunk
case startOfHeader
case startOfData
case afterChunk
}
func byteReader(at offset: RelativeIndex, skip: Index = 0) -> RawBytes.Reader {
.init(data: _bytes, index: index(offset) + skip)
}
}

View File

@@ -0,0 +1,57 @@
extension UnsignedInteger {
/// `Foundation` independent alternative to `String(format: "%x")`. Default: upper-case digits
func hex(_ padding: Int = 1, upper: Bool = true) -> String {
let rv = String(self, radix: 16, uppercase: upper)
return rv.count >= padding
? rv
: String(repeating: "0", count: padding - rv.count) + rv
}
}
public struct ColorComponents {
let raw: UInt32
public let a: UInt8, r: UInt8, g: UInt8, b: UInt8
init(_ c: UInt32) {
raw = c
a = UInt8((c & 0xFF000000) >> 24)
r = UInt8((c & 0xFF0000) >> 16)
g = UInt8((c & 0xFF00) >> 8)
b = UInt8((c & 0xFF))
}
/// alpha value as float
public var fa: Float { Float(a) / 255 }
/// red value as float
public var fr: Float { Float(r) / 255 }
/// green value as float
public var fg: Float { Float(g) / 255 }
/// blue value as float
public var fb: Float { Float(b) / 255 }
/// String with format `#AARRGGBB`
public func argb8() -> String { "#" + raw.hex(8) }
/// String with format `#RRGGBBAA`
public func rgba8() -> String { "#" + (raw & 0xFFFFFF).hex(6) + a.hex(2) }
/// String with format `#RRGGBB`
public func rgb8() -> String { "#" + (raw & 0xFFFFFF).hex(6) }
/// String with format `#ARGB`
public func argb4() -> String { "#" + (a & 0xF).hex(1) + short.hex(3) }
/// String with format `#RGBA`
public func rgba4() -> String { "#" + short.hex(3) + (a & 0xF).hex(1) }
/// String with format `#RGB`
public func rgb4() -> String { "#" + short.hex(3) }
/// If color can be represented as 3-letter string, prefer that over the 6-letter version.
/// Alpha value is always ignored, even if set to transparent values.
public func rgbAuto() -> String {
(r >> 4 == r & 0xF) && (g >> 4 == g & 0xF) && (b >> 4 == b & 0xF)
? rgb4() : rgb8()
}
// MARK: - Private methods
private var short: UInt16 {
UInt16((r & 0xF)) << 8 | UInt16((g & 0xF0) | (b & 0xF))
}
}

View File

@@ -0,0 +1,135 @@
// MARK: - RawBytes
#if canImport(Foundation)
import Foundation
public typealias RawBytes = Data
#else
public typealias RawBytes = [UInt8]
#endif
extension RawBytes {
/// Read `1 Byte` integer
@inlinable func int8(_ idx: Index) -> UInt8 {
self[idx]
}
/// Read and convert a `2 Bytes` little-endian integer
@inlinable func int16(_ idx: Index) -> UInt16 {
UInt16(self[idx + 1]) << 8 | UInt16(self[idx])
}
/// Read and convert a `4 Bytes` little-endian integer
@inlinable func int32(_ idx: Index) -> UInt32 {
UInt32(self[idx + 3]) << 24 | UInt32(self[idx + 2]) << 16 | UInt32(self[idx + 1]) << 8 | UInt32(self[idx])
}
/// Big-endian version of `int32()`
@inlinable func int32be(_ idx: Index) -> UInt32 {
UInt32(self[idx]) << 24 | UInt32(self[idx + 1]) << 16 | UInt32(self[idx + 2]) << 8 | UInt32(self[idx + 3])
}
/// Decode chunks of data with UTF16-LE encoding. `len` uses `UInt16` characters (* 2 Bytes)
@inlinable func utf16(_ start: Index, fixedLen len: Int) -> String {
let chars = (0 ..< len).map { i in int16(start + i * 2) }
return String(decoding: chars, as: UTF16.self)
// String(data: self[start ..< start + len * 2], encoding: .utf16LittleEndian)!
}
/// Decode chunks of data with UTF8 encoding. `len` uses `UInt8` characters (1 Byte)
@inlinable func utf8(_ start: Index, fixedLen len: Int) -> String {
String(decoding: self[start ..< start + len], as: UTF8.self)
// String(data: self[start ..< start + len], encoding: .utf8)!
}
/// Search for first `\0` character. Upto `maxLen` UInt16 characters. Fallback to `maxLen`.
@inlinable func utf16(_ start: Index, maxLen: Int) -> String {
self.utf16(start, fixedLen: (0..<maxLen).first {
self[start + $0 * 2] == 0 && self[start + $0 * 2 + 1] == 0
} ?? maxLen)
}
/// Search for first `\0` character. Upto `maxLen` UInt8 characters. Fallback to `maxLen`.
@inlinable func utf8(_ start: Index, maxLen: Int) -> String {
self.utf8(start, fixedLen: (0..<maxLen).first {
self[start + $0] == 0
} ?? maxLen)
}
/**
* Strings in UTF-16 format have length indicated by a length encoded in the stored data.
* It is either 1 or 2 characters of length data.
* This allows a maximum length of `0x7FFFFFF` (2147483647 bytes),
* but if you're storing that much data in a string, you're abusing them.
*
* If the high bit is set, then there are two characters or 4 bytes of length data encoded.
* In that case, drop the high bit of the first character and add it together with the next character.
*/
@inline(__always) private func lenStr16(_ idx: Index) -> (Index, Int) {
let len = Int(int16(idx))
if len & 0x8000 != 0 { // uses two chars for size
return (idx + 4, (len & 0x7FFF) << 16 + Int(int16(idx + 2)))
}
return (idx + 2, len)
}
/**
* Strings in UTF-8 format have length indicated by a length encoded in the stored data.
* It is either 1 or 2 characters of length data.
* This allows a maximum length of `0x7FFF` (32767 bytes),
* but you should consider storing text in another way if you're using that much data in a single string.
*
* If the high bit is set, then there are two characters or 2 bytes of length data encoded.
* In that case, drop the high bit of the first character and add it together with the next character.
*/
@inline(__always) private func lenStr8(_ idx: Index) -> (Index, Int) {
let len = Int(int8(idx))
if len & 0x80 != 0 { // uses two chars for size
return (idx + 2, (len & 0x7F) << 8 + Int(int8(idx + 1)))
}
return (idx + 1, len)
}
/// Read an UTF16 string which has its length encoded just before the string.
func stringUTF16(_ idx: Index) -> String {
let (start, len) = lenStr16(idx)
// validate string
precondition(int16(start + len * 2) == 0, "String does not end with \0 (offset: \(start))")
return self.utf16(start, fixedLen: len)
}
/// Read an UTF8 string which has its length encoded just before the string.
func stringUTF8(_ idx: Index) -> String {
let (next, _) = lenStr8(idx) // length of string when converted to UTF16
let (start, len) = lenStr8(next) // length of raw UTF8 string
// validate string
precondition(int8(start + len) == 0, "String does not end with \0 (offset: \(idx))")
return self.utf8(start, fixedLen: len)
}
/// Keeps an index while progressing over the data
struct Reader {
let data: RawBytes
var index: Index
/// Reads `1 Byte` integer
mutating func read8() -> UInt8 { data.int8(incr(1)) }
/// Reads `2 Bytes` little-endian integer
mutating func read16() -> UInt16 { data.int16(incr(2)) }
/// Reads `4 Bytes` little-endian integer
mutating func read32() -> UInt32 { data.int32(incr(4)) }
/// Reads `4 Bytes` big-endian integer
mutating func read32_be() -> UInt32 { data.int32be(incr(4)) }
/// Reads `N Bytes` of `UTF8` data, terminated by `\0` (1 Byte)
mutating func readStr8(len: Int) -> String { data.utf8(incr(len), maxLen: len) }
/// Reads `2*N Bytes` of `UTF16` data, terminated by `\0` (2 Bytes)
mutating func readStr16(len: Int) -> String { data.utf16(incr(len * 2), maxLen: len) }
/// return current index, then increment
@discardableResult
mutating func incr(_ len: Index) -> Index {
let rv = index
index += len
return rv
}
}
}

View File

@@ -0,0 +1,198 @@
// MARK: - DataType
/// Type of the data value.
public enum DataType : UInt8 {
/// `data` is either `0` or `1`, specifying this resource is either undefined or empty, respectively.
case Null = 0x00
/// `data` holds a `TblTableRef`, a reference to another resource table entry.
case Reference = 0x01
/// `data` holds an attribute resource identifier.
case Attribute = 0x02
/// `data` holds an index into the containing resource table's global value string pool.
case String = 0x03
/// `data` holds a single-precision floating point number.
case Float = 0x04
/// `data` holds a complex number encoding a dimension value, such as `100in`.
case Dimension = 0x05
/// `data` holds a complex number encoding a fraction of a container.
case Fraction = 0x06
/// `data` holds a dynamic `TblTableRef`, which needs to be resolved before it can be used like a `.Reference`.
case DynamicReference = 0x07
/// `data` holds an attribute resource identifier, which needs to be resolved before it can be used like a `.Attribute`.
case DynamicAttribute = 0x08
/// `data` is a raw integer value of the form `n..n`.
case IntDec = 0x10
/// `data` is a raw integer value of the form `0xn..n`.
case IntHex = 0x11
/// `data` is either `0` or `1`, for input `false` or `true` respectively.
case IntBoolean = 0x12
/// `data` is a raw integer value of the form `#aarrggbb`.
case IntColorARGB8 = 0x1c
/// `data` is a raw integer value of the form `#rrggbb`.
case IntColorRGB8 = 0x1d
/// `data` is a raw integer value of the form `#argb`.
case IntColorARGB4 = 0x1e
/// `data` is a raw integer value of the form `#rgb`.
case IntColorRGB4 = 0x1f
var isColor: Bool {
self == .IntColorARGB8 || self == .IntColorRGB8 || self == .IntColorARGB4 || self == .IntColorRGB4
}
}
// MARK: - ResValue
/**
* Representation of a value in a resource, supplying type information.
*/
/// Size: `8 Bytes`
public struct ResValue {
/// Number of bytes in this structure.
let size: UInt16
/// Always set to 0.
let res0: UInt8
/// Type of the data value.
public let dataType: DataType // UInt8
/// The data for this item, as interpreted according to dataType.
let data: UInt32
init(type: DataType, data rawData: UInt32) {
size = 8
res0 = 0
dataType = type
data = rawData
}
init(_ br: inout RawBytes.Reader) throws {
size = br.read16()
res0 = br.read8()
let rawType: UInt8 = br.read8()
guard let typ = DataType(rawValue: rawType) else {
throw AXMLError("Unknown ResValue data-type \(rawType) (offset: \(br.index - 1))")
}
dataType = typ
data = br.read32()
assert(dataType != .DynamicAttribute, "TODO: .DynamicAttribute data type")
assert(dataType != .DynamicReference, "TODO: .DynamicReference data type")
}
/// Convenience getter for `DataType.String`
public var asStringRef: StringPoolRef { data }
/// Convenience getter for `DataType.Reference` and `DataType.Attribute`
public var asTableRef: TblTableRef { TblTableRef(data) }
/// Convenience getter for `DataType.Float`
public var asFloat: Float { Float(bitPattern: data) }
/// Convenience getter for `DataType.Dimension`
public var asDimension: ComplexType { ComplexType(data, isFraction: false) }
/// Convenience getter for `DataType.Fraction`
public var asFraction: ComplexType { ComplexType(data, isFraction: true) }
/// Convenience getter for `DataType.IntDec`
public var asIntDec: Int { Int(Int32(truncatingIfNeeded: data)) }
/// Convenience getter for `DataType.IntHex` with format `00AA22CC`
public var asIntHex: String { data.hex(8) }
/// Convenience getter for `DataType.IntBoolean` (value `> 0`)
public var asBoolean: Bool { data > 0 }
/// Convenience getter for `DataType.IntColor*`
public var asColor: ColorComponents { ColorComponents(data) }
// see: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/ResourceTypes.cpp#7637
/// Use meta data to display value according to type.
/// `StringPool` only required for type `.String`
public func resolve(_ pool: StringPool?) -> String {
switch dataType {
case .Null:
// switch data {
// case 0: return "(null)" // not defined
// case 1: return "(null empty)" // explicitly defined as empty
// default: return String(format: "(null) 0x%08x\n", data) // should never happen
// }
return ""
case .DynamicReference:
// TODO: can we treat them as their non-dynamic counterpart?
fallthrough
case .Reference: return ((data >> 24) & 0xFF == 1 ? "@android:" : "@") + data.hex(8)
case .DynamicAttribute:
fallthrough
case .Attribute: return ((data >> 24) & 0xFF == 1 ? "?android:" : "?") + data.hex(8)
case .String: return pool!.getString(data)
case .Float: return "\(asFloat)"
case .Dimension: return asDimension.asString()
case .Fraction: return asFraction.asString()
case .IntDec: return "\(asIntDec)"
case .IntHex: return "0x" + data.hex(8)
case .IntBoolean: return data > 0 ? "true" : "false"
case .IntColorARGB8: return asColor.argb8()
case .IntColorRGB8: return asColor.rgb8()
case .IntColorARGB4: return asColor.argb4()
case .IntColorRGB4: return asColor.rgb4()
}
}
}
// MARK: - ComplexType
/**
* Structure of complex data values (`DataType.Dimension` and `DataType.Fraction`)
*
* `MANTISSA_MASK = 0xFFFFFF00`
*
* Where the actual value is.
* This gives us 23 bits of precision.
* The top bit is the sign.
*
* `RADIX_MASK = 0x00000030`
*
* Where the radix information is, telling where the decimal place appears in the mantissa.
* This give us 4 possible fixed point representations as defined below.
*
* `UNIT_MASK = 0x0000000F`
*
* Where the unit type information is.
* This gives us 16 possible types, as defined below.
*/
public struct ComplexType {
public let value: Float
public let unit: String
init(_ complex: UInt32, isFraction: Bool) {
// UNIT
var scale: Float = 1.0
if (isFraction) {
scale = 100
switch (complex & 0xF) {
case 0: unit = "%" // A basic fraction of the overall size.
case 1: unit = "%p" // A fraction of the parent size.
default: unit = ""
}
} else {
switch (complex & 0xF) {
case 0: unit = "px" // raw pixels
case 1: unit = "dp" // Device Independent Pixels
case 2: unit = "sp" // Scaled device independent Pixels
case 3: unit = "pt" // points
case 4: unit = "in" // inches
case 5: unit = "mm" // millimeters
default: unit = ""
}
}
// VALUE
let neg = (complex & 0x80000000) != 0
let mantissa = Float((complex & 0x7FFFFF00) >> 8) - (neg ? Float(0x7FFFFF) : 0)
let val = mantissa / (radix[Int(complex & 0x30) >> 4] / scale)
// for `.Fraction`, round to nearest integral number (if equivalent)
let isIntegral = isFraction && abs(val.rounded() - val) < 1e-05
value = isIntegral ? val.rounded() : val
}
public func asString() -> String {
value.rounded() == value ? "\(Int(value))\(unit)" : "\(value)\(unit)"
}
}
// 23p0 - The mantissa is an integral number -- i.e., 0xnnnnnn.0
// 16p7 - The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn
// 8p15 - The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn
// 0p23 - The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn
private let radix: [Float] = [1, 0x80, 0x8000, 0x800000]

View File

@@ -0,0 +1,98 @@
/// Index into the string pool table (`UInt32`-offset from the indices immediately after `StringPool`)
/// at which to find the location of the string data in the pool.
public typealias StringPoolRef = UInt32
/**
* Definition for a pool of strings.
*
* The data of this chunk is an array of `UInt32` providing indices into the pool, relative to `stringsStart`.
* At `stringsStart` are all of the UTF-16 strings concatenated together;
* each starts with a `UInt16`of the string's length and each ends with a `0x0000` terminator.
* If a string is `> 32767` characters, the high bit of the length is set meaning to take
* those 15 bits as a high word and it will be followed by another `UInt16`containing the low word.
*
* If `styleCount` is not zero, then immediately following the array of `UInt32` indices into the
* string table is another array of indices into a style table starting at `stylesStart`.
* Each entry in the style table is an array of `StringPoolSpan` structures.
*/
/// Header size: `ChunkHeader` (8B) + `20 Bytes`
public struct StringPool {
let header: ChunkHeader
/// Number of strings in this pool (number of `UInt32` indices that follow in the data).
public let stringCount: UInt32
/// Number of style span arrays in the pool (number of `UInt32` indices follow the string indices).
public let styleCount: UInt32
/// Flags.
let flags: Flags // UInt32
struct Flags: OptionSet {
let rawValue: UInt32
/// If set, the string index is sorted by the string values (based on `strcmp16()`).
static let Sorted = Flags(rawValue: 1 << 0)
/// String pool is encoded in UTF-8
static let Utf8 = Flags(rawValue: 1 << 8)
}
/// Index from header of the string data.
let stringsStart: UInt32
/// Index from header of the style data. (will be `0` if `styleCount == 0`)
let stylesStart: UInt32
// then:
// string indices (stringCount * UInt32)
// style indices (styleCount * UInt32)
// strings (variable size)
// styles (styleCount * 16) followed by 0xFFFFFFFF 0xFFFFFFFF
public var isSorted: Bool { flags.contains(.Sorted) }
// INTERNAL USAGE. No correspondence in data:
private let _offset: Indices
struct Indices {
let indicesString: Index
let indicesStyle: Index
let dataString: Index
let dataStyle: Index
}
init(_ idx: Index, _ bytes: RawBytes) throws {
self.init(try ChunkHeader(idx, bytes, expect: .StringPool))
}
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfHeader)
stringCount = br.read32()
styleCount = br.read32()
flags = Flags(rawValue: br.read32())
stringsStart = br.read32()
stylesStart = br.read32()
// internal
let idxChunk = header.index(.startOfChunk)
let idxData = header.index(.startOfData)
_offset = Indices(
indicesString: idxData,
indicesStyle: idxData + Index(stringCount) * 4,
dataString: idxChunk + Index(stringsStart),
dataStyle: idxChunk + Index(stylesStart))
}
// MARK: - Public methods
/// Retrieve string for given `StringPoolRef` index
public func getString(_ idx: StringPoolRef) -> String {
precondition(idx < stringCount, "Index out of bounds for string-pool string[\(idx) > \(stringCount)]")
let offset = header._bytes.int32(_offset.indicesString + Index(idx) * 4)
let dataOffset = _offset.dataString + Index(offset)
return flags.contains(.Utf8)
? header._bytes.stringUTF8(dataOffset)
: header._bytes.stringUTF16(dataOffset)
}
/// Retrieve styles for given `StringPoolRef` index
public func getStyle(_ idx: StringPoolRef) -> StringPoolSpan {
precondition(idx < styleCount, "Index out of bounds for string-pool style[\(idx) > \(styleCount)]")
let offset = header._bytes.int32(_offset.indicesStyle + Index(idx) * 4)
let dataOffset = _offset.dataStyle + Index(offset)
return StringPoolSpan(dataOffset, header._bytes)
}
}

View File

@@ -0,0 +1,22 @@
/**
* This structure defines a span of style information associated with a string in the pool.
*/
/// Size: `12 Bytes`
public struct StringPoolSpan {
/// This is the name of the span -- that is, the name of the XML tag that defined it.
/// The special value END (`0xFFFFFFFF`) indicates the end of an array of spans.
public let name: StringPoolRef // UInt32
/// The range of characters in the string that this span applies to.
public let firstChar: UInt32
public let lastChar: UInt32
// then:
// fixed 0xFFFFFFFF ?
init(_ idx: Index, _ bytes: RawBytes) {
name = bytes.int32(idx)
firstChar = bytes.int32(idx + 4)
lastChar = bytes.int32(idx + 8)
// _ff = bytes.int32(idx + 12)
}
}

View File

@@ -0,0 +1,91 @@
/**
* This is the beginning of information about an entry in the resource table.
*
* It holds the reference to the name of this entry, and is immediately followed by one of:
* * A `ResValue` structure, if `Flags.Complex` is -not- set.
* * An array of `ResTable_map` structures, if `Flags.Complex` is set.
* These supply a set of name/value mappings of data.
* * If `Flags.Compact` is set, this entry is a compact entry for simple values only
*/
/// Size: `8 Bytes`
public struct TblEntry {
// Note: `_size` and `_key` are private because `Flags` determine their meaning
/// Number of bytes in this structure.
var size: UInt16 { flags.contains(.Compact) ? 8 : _size }
/// Reference into `TblPackage::keyStrings` identifying this entry.
public var key: StringPoolRef { flags.contains(.Compact) ? StringPoolRef(_size) : _key }
// ACTUAL data entry:
/// Number of bytes in this structure.
private let _size: UInt16 // used as `key` if Flags.Compact
/// Flags.
let flags: Flags // UInt16
struct Flags: OptionSet {
let rawValue: UInt16
/// If set, this is a complex entry, holding a set of name/value mappings.
/// It is followed by an array of `ResTable_map` structures.
static let Complex = Flags(rawValue: 0x0001)
/// If set, this resource has been declared public, so libraries are allowed to reference it.
static let Public = Flags(rawValue: 0x0002)
/// If set, this is a weak resource and may be overriden by strong resources of the same name/type.
/// This is only useful during linking with other resource tables.
static let Weak = Flags(rawValue: 0x0004)
/// If set, this is a compact entry with data type and value directly encoded in the this entry.
/// See `ResTable_entry::compact`
static let Compact = Flags(rawValue: 0x0008)
}
/// Reference into `TblPackage::keyStrings` identifying this entry.
private let _key: StringPoolRef // UInt32 // used as `data` if Flags.Compact
// Interpreted data value
/// Only set if `Flags.Complex` is `false`
public let value: ResValue? // 8 Bytes
/// Only set if `Flags.Complex` is `true`
public let valueMap: TblEntryMap?
/// `true` if `Flags.Complex` is set
public var isComplex: Bool { flags.contains(.Complex) }
init(_ br: inout RawBytes.Reader) throws {
_size = br.read16()
flags = Flags(rawValue: br.read16())
_key = br.read32()
assert(!flags.contains(.Compact), "TODO: TblEntry with Flags.Compact")
/*
* Always verify the memory associated with this entry and its value
* before calling `value()` or `map_entry()`
*/
if flags.contains(.Compact) {
let rawType = UInt8((flags.rawValue & 0xFF00) >> 8)
guard let typ = DataType(rawValue: rawType) else {
throw AXMLError("Unknown ResValue data-type \(rawType)")
}
value = ResValue(type: typ, data: _key)
valueMap = nil
} else if flags.contains(.Complex) {
value = nil
valueMap = try TblEntryMap(&br)
} else {
value = try ResValue(&br)
valueMap = nil
}
}
}
/**
* A compact entry is indicated by `Flags.Compact`, with flags at the same offset as a normal entry.
* This is only for simple data values where
*
* - size for entry or value can be inferred (both being 8 bytes).
* - key index is encoded in 16-bit
* - dataType is encoded as the higher 8-bit of flags
* - data is encoded directly in this entry
*/
// UNUSED because case handled directly in `TblEntry`
//struct TblEntryCompact {
// let key: UInt16
// let flags: UInt16
// let data: UInt32
//}

View File

@@ -0,0 +1,98 @@
/**
* Extended form of a `TblEntry` for map entries, defining a parent map resource from which to inherit values.
*/
/// Header size: `8 Bytes`
public struct TblEntryMap {
/// Resource identifier of the parent mapping, or `0` if there is none.
/// This is always treated as a `TYPE_DYNAMIC_REFERENCE`.
public let parent: TblTableRef // UInt32
/// Number of name/value pairs that follow for `Flags.Complex`.
public let count: UInt32
/// Entries immediatelly following this structure
public let entries: [TblEntryMapItem]
init(_ br: inout RawBytes.Reader) throws {
parent = TblTableRef(br.read32())
count = br.read32()
entries = try (0..<count).map { _ in try TblEntryMapItem(&br) }
}
}
/**
* A single name/value mapping that is part of a complex resource
* entry.
*/
/// Header size: `12 Bytes`
public struct TblEntryMapItem {
/// The resource identifier defining this mapping's name.
/// For attribute resources, `name` can be one of the following special resource types to supply
/// meta-data about the attribute; for all other resource types it must be an attribute resource.
public let name: TblTableRef // UInt32
/// This mapping's value.
public let value: ResValue // 8 Bytes
init(_ br: inout RawBytes.Reader) throws {
name = TblTableRef(br.read32())
value = try ResValue(&br)
}
// TODO: How to treat dynamic `01010000` and `01030000` references?
// TODO: These aren't used anywhere (yet). Probably needed for complex-map-type?
/// Special values for `name` when defining attribute resources.
public enum Attr: UInt32 {
/// This entry holds the attribute's type code.
case type = 0x01000000
/// For integral attributes, this is the minimum value it can hold.
case Min = 0x01000001
/// For integral attributes, this is the maximum value it can hold.
case Max = 0x01000002
/// Localization of this resource is can be encouraged or required with an `aapt` flag if this is set
case L10n = 0x01000003
/// for plural support, see `android.content.res.PluralRules#attrForQuantity(int)`
case Other = 0x01000004
case Zero = 0x01000005
case One = 0x01000006
case Two = 0x01000007
case Few = 0x01000008
case Many = 0x01000009
}
/// Bit mask of allowed types, for use with `AttrName.type`.
public enum AttrType: UInt32 {
/// No type has been defined for this attribute, use generic type handling.
/// The low 16 bits are for types that can be handled generically;
/// the upper 16 require additional information in the bag so can not be handled generically for `AttrType.any`.
case any = 0x0000FFFF
/// Attribute holds a references to another resource.
case Reference = 1
/// Attribute holds a generic string.
case String = 2
/// Attribute holds an integer value. `AttrName.Min` and `AttrName.Max`
/// can optionally specify a constrained range of possible integer values.
case Integer = 4
/// Attribute holds a boolean integer.
case Boolean = 8
/// Attribute holds a color value.
case Color = 16
/// Attribute holds a floating point value.
case Float = 32
/// Attribute holds a dimension value, such as "20px".
case Dimension = 64
/// Attribute holds a fraction value, such as "20%".
case Fraction = 128
/// Attribute holds an enumeration.
/// The enumeration values are supplied as additional entries in the map.
case Enum = 0x10000
/// Attribute holds a bitmaks of flags.
/// The flag bit values are supplied as additional entries in the map.
case Flags = 0x20000
}
/// Enum of localization modes, for use with `AttrName.L10n`.
public enum AttrL10n: UInt8 {
case NotRequired = 0
case Suggested = 1
}
}

View File

@@ -0,0 +1,100 @@
/**
* A collection of resource data types within a package.
* Followed by one or more `TblType` and `TblTypeSpec` structures
* containing the entry values for each resource type.
*/
/// Header size: `ChunkHeader` (8B) + `280 Bytes`
public struct TblPackage {
let header: ChunkHeader
/// If this is a base package, its ID.
/// Package IDs start at 1 (corresponding to the value of the package bits in a resource identifier).
/// `0` means this is not a base package.
public let id: UInt32
/// Actual name of this package, `\0`-terminated.
public let name: String // fixed max-length: 128 * UInt16
/// Offset to a `StringPool` defining the resource type symbol table.
/// If zero, this package is inheriting from another base package (overriding specific values in it).
let typeStrings: UInt32
/// Last index into typeStrings that is for public use by others.
let lastPublicType: UInt32
/// Offset to a `StringPool` defining the resource key symbol table.
/// If zero, this package is inheriting from another base package (overriding specific values in it).
let keyStrings: UInt32
/// Last index into keyStrings that is for public use by others.
let lastPublicKey: UInt32
/// Unknown usage. `0` for all tested documents
let typeIdOffset: UInt32
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfHeader)
id = br.read32()
name = br.readStr16(len: 128)
typeStrings = br.read32()
lastPublicType = br.read32()
keyStrings = br.read32()
lastPublicKey = br.read32()
typeIdOffset = br.read32()
}
// MARK: - Public methods
/// Get package-local `StringPool` (via relative offset). You should cache the property somewhere or else it will be regenerated all the time.
public func stringPool(for type: StringPoolType) -> StringPool? {
let offset = type == .Types ? typeStrings : keyStrings
return offset == 0 ? nil : try! StringPool(header.index(.startOfChunk) + Index(keyStrings), header._bytes)
}
public enum StringPoolType {
case Types
case Keys
}
/// Go over all type declarations
///
/// you have to manually init `TblType` in `types`. That is for efficiency.
public func iterTypes(_ block: (_ spec: TblTypeSpec, _ types: [TblType]) throws -> Void) throws {
var spec: TblTypeSpec? = nil
var types: [TblType] = []
// ignore StringPool because they are accessed directly (see above)
try header.iterChildren(filter: { $0.type != .StringPool }) {
if $1.type == .TblTypeSpec {
if spec != nil {
try block(spec!, types)
types.removeAll()
}
spec = TblTypeSpec($1)
} else if $1.type == .TblType {
types.append(TblType($1))
}
}
if spec != nil {
try block(spec!, types)
}
}
/// Lookup specific `TblType` from package. E.g., by using `TblTableRef.type`
public func getType(_ typeId: UInt8) throws -> (TblTypeSpec, [TblType])? {
var spec: TblTypeSpec? = nil
var types: [TblType] = []
try header.iterChildren {
if spec == nil {
if $1.type == .TblTypeSpec {
let tmp = TblTypeSpec($1)
if tmp.id == typeId {
spec = tmp
}
} // else: ignore all other types
} else if $1.type == .TblType {
let tmp = TblType($1)
assert(tmp.id == typeId, "TblType should have same id as parent TblTypeSpec")
types.append(tmp)
} else {
$0 = true // abort further processing
}
}
if spec == nil {
return nil
}
return (spec!, types)
}
}

View File

@@ -0,0 +1,34 @@
/**
* This is a reference to a unique entry (a `TblEntry` structure) in a resource table.
* The value is structured as: `0xpptteeee`, where
* `pp` is the package index,
* `tt` is the type index in that package, and
* `eeee` is the entry index in that type.
* The package and type values start at 1 for the first item, to help catch cases where they have not been supplied.
*/
public struct TblTableRef: CustomStringConvertible {
public let ident: UInt32
public init(_ ident: UInt32) {
self.ident = ident
}
/// `ident` as hex string
public var asHex: String { ident.hex(8) }
/// mask: `0xFF000000`
public var package: UInt8 { UInt8((ident & 0xFF000000) >> 24) }
/// mask: `0x00FF0000`
public var type: UInt8 { UInt8((ident & 0x00FF0000) >> 16) }
/// mask: `0x0000FFFF`
public var entry: UInt16 { UInt16(ident & 0x0000FFFF) }
/// `package != 0 || type != 0 || entry != 0`
public func isVlaid() -> Bool { ident != 0 }
/// `package != 0 || type != 0`
public func check() -> Bool { (ident & 0xFFFF0000) != 0 }
/// `package != 0 && type == 0`
public func isInternal() -> Bool { (ident & 0xFFFF0000) != 0 && (ident & 0x00FF0000) == 0 }
public var description: String { "TblTableRef(" + ident.hex(8) + ")" }
}

View File

@@ -0,0 +1,179 @@
/// Just a name alias to better understand which index is used for what purpose.
public typealias TblDataIndex = Index
/**
* A collection of resource entries for a particular resource data type.
*
* If the flag `Flags.Sparse` is not set in `flags`, then this struct is followed by an array of `UInt32`
* defining the resource values, corresponding to the array of type strings in the `TblPackage::typeStrings` string block.
* Each of these hold an index from `entriesStart`.
* A value of `0xFFFFFFFF` (NO_ENTRY) means that entry is not defined.
*
* If the flag `Flags.Sparse` is set in `flags`, then this struct is followed by an array of `TblType.SparseIndex`
* defining only the entries that have values for this type.
* Each entry is sorted by their entry ID such that a binary search can be performed over the entries.
* The ID and offset are encoded in a `UInt32`.
* See `TblType.SparseIndex`.
*
* There may be multiple of these chunks for a particular resource type,
* supply different configuration variations for the resource values of that type.
*
* It would be nice to have an additional ordered index of entries, so
* we can do a binary search if trying to find a resource by string name.
*/
/// Header size: `ChunkHeader` (8B) + `12 Bytes` + `TblTypeConfig` (64B)
public struct TblType {
let header: ChunkHeader
/// The type identifier this chunk is holding.
/// Type IDs start at 1 (corresponding to the value of the type bits in a resource identifier).
/// `0` is invalid.
public let id: UInt8
/// Flags.
let flags: Flags // UInt8
struct Flags: OptionSet {
let rawValue: UInt8
/// If set, the entry is sparse, and encodes both the entry ID and offset into each entry, and a binary search is used to find the key.
/// Only available on platforms `>= O`.
/// Mark any types that use this with a v26 qualifier to prevent runtime issues on older platforms.
static let Sparse = Flags(rawValue: 0x01)
/// If set, the offsets to the entries are encoded in 16-bit, `real_offset = offset * 4`
/// An 16-bit offset of `0xFFFF` means a NO_ENTRY
static let Offset16 = Flags(rawValue: 0x02)
}
/// Must be `0`.
let reserved: UInt16
/// Number of `UInt32` entry indices that follow.
public let entryCount: UInt32
/// Offset from header where `TblEntry` data starts.
let entriesStart: UInt32
/// Configuration this collection of entries is designed for.
/// This must always be last.
public let config: TblTypeConfig // 64 Bytes
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfHeader)
id = br.read8()
flags = Flags(rawValue: br.read8())
reserved = br.read16()
entryCount = br.read32()
entriesStart = br.read32()
config = TblTypeConfig(&br)
// TODO: check indexing defined by flags
assert(flags.rawValue == 0, "TODO: TblType flags encountered \(flags.rawValue)")
}
// MARK: - Private methods
/**
* An entry in a `TblType` with the flag `Flags.Sparse` set.
*/
private struct SparseIndex {
/// The index of the entry.
let idx: UInt16
/// The offset from `TblType::entriesStart`, divided by 4.
let offset: UInt16
}
/// Iterate over all data indices (respecting flags). If `Flags.Sparse` is not set, `idx` is strictly incrementing `0..<entryCount`
private func entryOffsets() -> [(UInt16, TblDataIndex)] {
let start = header.index(.startOfChunk) + Index(entriesStart)
var br = header.byteReader(at: .startOfData)
let range = (0 ..< UInt16(entryCount)) // downcast should be fine because type indices can only be UInt16
// more efficient to do flags-then-loop instead of loop-then-flags
if flags.contains(.Sparse) {
return range.map { _ in
let sparse = SparseIndex(idx: br.read16(), offset: br.read16())
return (sparse.idx, start + Index(sparse.offset) * 4)
}
} else if flags.contains(.Offset16) {
return range.compactMap {
let val = br.read16()
return val == 0xFFFF ? nil : ($0, start + Index(val) * 4)
}
} else {
return range.compactMap {
let val = br.read32()
return val == 0xFFFFFFFF ? nil : ($0, start + Index(val))
}
}
}
/// Get data entry index (absolute) by calculating offset from index (relative)
private func entryOffset(for idx: Int) -> TblDataIndex? {
precondition(idx < entryCount, "Index out of bounds for TblType entry[\(idx) > \(entryCount)]")
if flags.contains(.Sparse) {
return binarySearchSparse(idx)
}
let offsetIndices = header.index(.startOfData)
if flags.contains(.Offset16) {
let offset = header._bytes.int16(offsetIndices + idx * 2)
guard offset != 0xFFFF else { // NO_ENTRY
return nil
}
return entryIndex(at: UInt32(offset) * 4)
}
let offset = header._bytes.int32(offsetIndices + idx * 4)
guard offset != 0xFFFFFFFF else { // NO_ENTRY
return nil
}
return entryIndex(at: offset)
}
/// Index where data entries start
private func entryIndex(at offset: UInt32) -> TblDataIndex {
let absOffset = header.index(.startOfChunk) + Index(entriesStart) + Index(offset)
assert(absOffset < header.index(.afterChunk), "entryIndex > .afterChunk, \(absOffset) > \(header.index(.afterChunk))")
return absOffset
}
/// Perform binary search on data indices if `Flags.Sparse` is set.
private func binarySearchSparse(_ needleIdx: Int) -> TblDataIndex? {
let start = header.index(.startOfData)
var low = 0, high = Int(entryCount) - 1
while low < high {
let mid = (low + high) / 2
let index = start + mid * 4
// we could use `TblType.SparseIndex` but that would perform unnecessary reads for offset
let currentIdx = Int(header._bytes.int16(index))
if currentIdx < needleIdx {
low = mid + 1
} else if currentIdx > needleIdx {
high = mid - 1
} else {
let offset = UInt32(header._bytes.int16(index + 2)) * 4
return entryIndex(at: offset)
}
}
return nil
}
// MARK: - Public methods
/// Lookup specific `TblEntry` from type. E.g., by using `TblTableRef.entry`
public func getValue(_ idx: UInt16) throws -> TblEntry? {
guard let offset = entryOffset(for: Int(idx)) else {
return nil
}
var br = RawBytes.Reader(data: header._bytes, index: offset)
return try TblEntry(&br)
}
/// Iterate over all type entries.
public func iterValues(_ block: (_ idx: UInt16, _ entry: TblEntry) throws -> Void) throws {
var br = RawBytes.Reader(data: header._bytes, index: 0)
for (idx, offset) in entryOffsets() {
br.index = offset
try block(idx, try TblEntry(&br))
}
}
}

View File

@@ -0,0 +1,401 @@
// MARK: - TblTypeConfig
/**
* Describes a particular resource configuration.
*/
/// Size: `64 Bytes`
public struct TblTypeConfig {
/// Number of bytes in this structure.
let size: UInt32
/// mcc, mnc
public let imsi: TblTypeConfigIMSI // UInt32
/// language, country
public let locale: TblTypeConfigLocale // UInt32
/// orientation, touchscreen, density
public let screenType: TblTypeConfigScreenType // UInt32
/// keyboard, navigation, keysHidden, navHidden, input, grammaticalInflection
public let inputType: TblTypeConfigInput // UInt32
/// screenWidth, screenHeight
public let screenSize: TblTypeConfigScreenSize // UInt32
/// sdkVersion, minorVersion
public let version: TblTypeConfigVersionSDK // UInt32
/// screenLayoutSize, screenLayoutLong, screenLayoutRound, uiModeType, uiModeNight, smallestScreenWidthDp
public let screenConfig: TblTypeConfigScreenConfig // UInt32
/// screenWidthDp, screenHeightDp
public let screenSizeDp: TblTypeConfigScreenSizeDp // UInt32
/// The ISO-15924 short name for the script corresponding to this configuration. (eg. Hant, Latn, etc.).
/// Interpreted in conjunction with the locale field.
public let localeScript: String // fixed len: 4 * UInt8
/// A single BCP-47 variant subtag.
/// Will vary in length between 4 and 8 chars.
/// Interpreted in conjunction with the locale field.
public let localeVariant: String // fixed max-len: 8 * UInt8
/// screenLayoutRound, colorModeWideGamut, colorModeHDR, screenConfigPad2
public let screenConfig2: TblTypeConfigScreenConfig2 // UInt32
/// If `false` and `localeScript` is set, it means that the script of the locale was explicitly provided.
/// If `true`, it means that `localeScript` was automatically computed.
/// `localeScript` may still not be set in this case, which means that we tried but could not compute a script.
public let localeScriptWasComputed: Bool // UInt32??
/// The value of BCP 47 Unicode extension for key 'nu' (numbering system).
/// Varies in length from 3 to 8 chars.
/// Zero-filled value.
public let localeNumberingSystem: String // fixed max-len: 8 * UInt8
init(_ br: inout RawBytes.Reader) {
size = br.read32()
imsi = TblTypeConfigIMSI(
mcc: br.read16(),
mnc: br.read16())
locale = TblTypeConfigLocale(br.read32_be())
screenType = TblTypeConfigScreenType(
_orientation: br.read8(),
_touchscreen: br.read8(),
_density: br.read16())
inputType = TblTypeConfigInput(rawValue: br.read32_be())
screenSize = TblTypeConfigScreenSize(
screenWidth: br.read16(),
screenHeight: br.read16())
version = TblTypeConfigVersionSDK(
sdkVersion: br.read16(),
minorVersion: br.read16())
screenConfig = TblTypeConfigScreenConfig(
screenLayout: br.read8(),
uiMode: br.read8(),
smallestScreenWidthDp: br.read16())
screenSizeDp = TblTypeConfigScreenSizeDp(
screenWidthDp: br.read16(),
screenHeightDp: br.read16())
localeScript = br.readStr8(len: 4)
localeVariant = br.readStr8(len: 8)
screenConfig2 = TblTypeConfigScreenConfig2(
screenLayout2: br.read8(),
colorMode: br.read8(),
screenConfigPad2: br.read16())
localeScriptWasComputed = br.read32() != 0 // ??? really 4 bytes?
localeNumberingSystem = br.readStr8(len: 8)
}
}
// Mark: - TblTypeConfigIMSI
/// mcc (UInt16), mnc (UInt16)
public struct TblTypeConfigIMSI {
/// Mobile country code (from SIM). `0` means "any".
public let mcc: UInt16
/// Mobile network code (from SIM). `0` means "any".
public let mnc: UInt16
}
// MARK: - TblTypeConfigLocale
/// language (UInt16), country (UInt16)
public struct TblTypeConfigLocale {
/// This field can take three different forms:
/// - `\0\0` means "any".
///
/// - Two 7 bit ascii values interpreted as ISO-639-1 language codes ('fr', 'en' etc. etc.).
/// The high bit for both bytes is zero.
///
/// - A single 16 bit little endian packed value representing an ISO-639-2 3 letter language code.
/// This will be of the form:
///
/// `{1, t, t, t, t, t, s, s, s, s, s, f, f, f, f, f}`
///
/// `bit[0, 4]` = first letter of the language code
/// `bit[5, 9]` = second letter of the language code
/// `bit[10, 14]` = third letter of the language code.
/// `bit[15]` = 1 always
///
/// For backwards compatibility, languages that have unambiguous
/// two letter codes are represented in that format.
///
/// The layout is always bigendian irrespective of the runtime architecture.
let _language: UInt16
/// This field can take three different forms:
/// - `\0\0` means "any".
///
/// - Two 7 bit ascii values interpreted as 2 letter region codes ('US', 'GB' etc.).
/// The high bit for both bytes is zero.
///
/// - An UN M.49 3 digit region code.
/// For simplicity, these are packed in the same manner as the language codes,
/// though we should need only 10 bits to represent them, instead of the 15.
///
/// The layout is always bigendian irrespective of the runtime architecture.
let _country: UInt16
/// Converts bit-pattern to String. Cache the result somewhere or else it will be regenerated all the time
public var language: String? { convertBits(_language, 97) }
/// Converts bit-pattern to String. Cache the result somewhere or else it will be regenerated all the time
public var country: String? { convertBits(_country, 48) }
init(_ bidEndian: UInt32) { // big-endian
_language = UInt16((bidEndian & 0xFFFF0000) >> 16)
_country = UInt16(bidEndian & 0xFFFF)
}
private func convertBits(_ bits: UInt16, _ chrStart: UInt8) -> String? {
if bits == 0 {
return nil // any
}
if bits & 0x8000 == 0 {
let f = (bits & 0xFF00) >> 8
let s = (bits & 0x00FF)
return String(decoding: [UInt8(f), UInt8(s)], as: UTF8.self)
}
let f = (bits & 0b0111110000000000) >> 10
let s = (bits & 0b0000001111100000) >> 5
let t = (bits & 0b0000000000011111)
return String(decoding: [chrStart + UInt8(f), chrStart + UInt8(s), chrStart + UInt8(t)], as: UTF8.self)
}
}
// MARK: - TblTypeConfigScreenType
/// orientation (UInt8), touchscreen (UInt8), density (UInt16)
public struct TblTypeConfigScreenType {
let _orientation: UInt8
let _touchscreen: UInt8
let _density: UInt16
public var orientation: Orientation { .init(rawValue: _orientation)! }
public var touchscreen: Touchscreen { .init(rawValue: _touchscreen)! }
public var density: Density { .init(rawValue: _density)! }
public enum Orientation: UInt8 {
case any = 0
case Port = 1
case Land = 2
case Square = 3
}
public enum Touchscreen: UInt8 {
case any = 0
case NoTouch = 1
case Stylus = 2
case Finger = 3
}
public enum Density: UInt16 {
case Default = 0
case Low = 120
case Medium = 160
case Tv = 213
case High = 240
case Xhigh = 320
case XXhigh = 480
case XXXhigh = 640
case any = 0xfffe
case None = 0xffff
}
}
// MARK: - TblTypeConfigInput
// Union of either:
// keyboard (UInt8), navigation (UInt8), inputFlags (UInt8), inputFieldPad0 (UInt8)
// input :24 (UInt32), inputFullPad0 :8 (UInt32)
// grammaticalInflectionPad0[3] (UInt8), grammaticalInflection (UInt8)
public struct TblTypeConfigInput {
let rawValue: UInt32 // big-endian
public var keyboard: Keyboard {
.init(rawValue: UInt8((rawValue & 0xFF000000) >> 24))!
}
public var navigation: Navigation {
.init(rawValue: UInt8((rawValue & 0x00FF0000) >> 16))!
}
public var navHidden: Navhidden {
.init(rawValue: UInt8((rawValue & 0x00000c00) >> 10))!
}
public var keysHidden: Keyshidden {
.init(rawValue: UInt8((rawValue & 0x00000300) >> 8))!
}
public var input: UInt32 {
UInt32((rawValue & 0xFFFFFF00) >> 8)
}
public var grammaticalInflection: GrammaticalGender {
.init(rawValue: UInt8(rawValue & 0x00000003))!
}
public enum Keyboard: UInt8 {
case any = 0
case NoKeys = 1
case Qwerty = 2
case Key12 = 3
}
public enum Navigation: UInt8 {
case any = 0
case NoNav = 1
case Dpad = 2
case Trackball = 3
case Wheel = 4
}
public enum Navhidden: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
public enum Keyshidden: UInt8 {
case any = 0
case No = 1
case Yes = 2
case Soft = 3
}
public enum GrammaticalGender: UInt8 {
case any = 0
case Neuter = 1
case Feminine = 2
case Masculine = 3
}
}
// Mark: - TblTypeConfigScreenSize
/// screenWidth (UInt16), screenHeight (UInt16)
public struct TblTypeConfigScreenSize {
/// `0` means any
public let screenWidth: UInt16
/// `0` means any
public let screenHeight: UInt16
}
// Mark: - TblTypeConfigVersionSDK
/// sdkVersion (UInt16), minorVersion (UInt16)
public struct TblTypeConfigVersionSDK {
/// `0` means any
public let sdkVersion: UInt16
/// `0` means any. For now minorVersion must always be 0!!! Its meaning is currently undefined.
public let minorVersion: UInt16
}
// Mark: - TblTypeConfigScreenConfig
/// screenLayout (UInt8), uiMode (UInt8), smallestScreenWidthDp (UInt16)
public struct TblTypeConfigScreenConfig {
/// direction, long, size
let screenLayout: UInt8
/// night, type
let uiMode: UInt8
/// `0` means any
public let smallestScreenWidthDp: UInt16
/// screenLayout bits for layout direction.
public var screenLayoutDirection: LayoutDir {
.init(rawValue: (screenLayout & 0xC0) >> 6)!
}
/// screenLayout bits for wide/long screen variation.
public var screenLayoutLong: LayoutLong {
.init(rawValue: (screenLayout & 0x30) >> 4)!
}
/// screenLayout bits for screen size class.
public var screenLayoutSize: LayoutSize {
.init(rawValue: (screenLayout & 0x0F))!
}
/// uiMode bits for the night switch.
public var uiModeNight: UIModeNight {
.init(rawValue: (uiMode & 0x30) >> 4)!
}
/// uiMode bits for the mode type.
public var uiModeType: UIModeType {
.init(rawValue: (uiMode & 0x0F))!
}
public enum LayoutDir: UInt8 {
case any = 0
case LTR = 1
case RTL = 2
}
public enum LayoutLong: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
public enum LayoutSize: UInt8 {
case any = 0
case Small = 1
case Normal = 2
case Large = 3
case Xlarge = 4
}
public enum UIModeNight: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
public enum UIModeType: UInt8 {
case any = 0
case Normal = 1
case Desk = 2
case Car = 3
case Television = 4
case Appliance = 5
case Watch = 6
case VrHeadset = 7
}
}
// Mark: - TblTypeConfigScreenSizeDp
public struct TblTypeConfigScreenSizeDp {
/// `0` means any
public let screenWidthDp: UInt16
/// `0` means any
public let screenHeightDp: UInt16
}
// MARK: - TblTypeConfigScreenConfig2
/// screenLayout2 (UInt8), colorMode (UInt8), screenConfigPad2 (UInt16)
public struct TblTypeConfigScreenConfig2 {
/// Contains round/notround qualifier.
let screenLayout2: UInt8
/// Wide-gamut, HDR, etc.
let colorMode: UInt8
/// Reserved padding.
let screenConfigPad2: UInt16
public var screenLayoutRound: LayoutRound { .init(rawValue: (screenLayout2 & 0x03))! }
public var colorModeHDR: ColorModeHDR { .init(rawValue: (colorMode & 0x0c) >> 2)! }
public var colorModeWideGamut: ColorModeWideColorGamut { .init(rawValue: (colorMode & 0x03))! }
/// screenLayout2 bits for round/notround.
public enum LayoutRound: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
/// colorMode bits for HDR/LDR.
public enum ColorModeHDR: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
/// colorMode bits for wide-color gamut/narrow-color gamut.
public enum ColorModeWideColorGamut: UInt8 {
case any = 0
case No = 1
case Yes = 2
}
}

View File

@@ -0,0 +1,91 @@
/**
* A specification of the resources defined by a particular type.
*
* There should be one of these chunks for each resource type.
*
* This structure is followed by an array of integers providing the set of
* configuration change flags (`TblTypeSpecFlags`) that have multiple
* resources for that configuration.
* In addition, the high bit is set if that resource has been made public.
*/
/// Header size: `8 Bytes`
public struct TblTypeSpec {
let header: ChunkHeader
/// The type identifier this chunk is holding.
/// Type IDs start at 1 (corresponding to the value of the type bits in a resource identifier).
/// `0` is invalid.
public let id: UInt8
/// Must be `0`.
let res0: UInt8
/// Used to be reserved, if `>0` specifies the number of `TblType` entries for this spec.
let typesCount: UInt16
/// Number of `UInt32` entry configuration masks that follow.
public let entryCount: UInt32
// then:
// TblTypeSpecFlags entries: (entryCount * UInt32)
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfHeader)
id = br.read8()
res0 = br.read8()
typesCount = br.read16()
entryCount = br.read32()
}
// MARK: - Public methods
/// Return list of configurations masks
public func entries() -> [TblTypeSpecFlags] {
let offset = header.index(.startOfData)
return (0..<Index(entryCount)).map {
TblTypeSpecFlags(rawValue: header._bytes.int32(offset + $0 * 4))
}
}
/// Configurations mask at Index
public func getEntry(_ index: Int) -> TblTypeSpecFlags {
precondition(index < entryCount, "Index out of bounds for TypeSpec entry[\(index) > \(entryCount)]")
let offset = header.index(.startOfData) + index * 4
return TblTypeSpecFlags(rawValue: header._bytes.int32(offset))
}
}
/// Flags indicating a set of config values.
/// These flag constants must match the corresponding ones
/// in `android.content.pm.ActivityInfo` and `attrs_manifest.xml`.
public struct TblTypeSpecFlags: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public static let Mcc = Self(rawValue: 0x0001)
public static let Mnc = Self(rawValue: 0x0002)
public static let Locale = Self(rawValue: 0x0004)
public static let Touchscreen = Self(rawValue: 0x0008)
public static let Keyboard = Self(rawValue: 0x0010)
public static let KeyboardHidden = Self(rawValue: 0x0020)
public static let Navigation = Self(rawValue: 0x0040)
public static let Orientation = Self(rawValue: 0x0080)
public static let Density = Self(rawValue: 0x0100)
public static let ScreenSize = Self(rawValue: 0x0200)
public static let Version = Self(rawValue: 0x0400)
public static let ScreenLayout = Self(rawValue: 0x0800)
public static let UiMode = Self(rawValue: 0x1000)
public static let SmallestScreenSize = Self(rawValue: 0x2000)
public static let Layoutdir = Self(rawValue: 0x4000)
public static let ScreenRound = Self(rawValue: 0x8000)
public static let ColorMode = Self(rawValue: 0x10000)
public static let GrammaticalGender = Self(rawValue: 0x20000)
/// Additional flag indicating an entry is public.
public static let SpecPublic = Self(rawValue: 0x40000000)
/// Additional flag indicating the resource id for this resource may change in a future build.
/// If this flag is set, the `.specPublic` flag is also set since the resource must be public
/// to be exposed as an API to other applications.
public static let SpecStagedApi = Self(rawValue: 0x20000000)
}

View File

@@ -0,0 +1,82 @@
/**
* Header for a resource table.
*
* Its data contains a series of additional chunks:
* * A `StringPool` containing all table values.
* This string pool contains all of the string values in the entire resource table
* (not the names of entries or type identifiers however).
* * One or more `TblPackage` chunks.
*
* Specific entries within a resource table can be uniquely identified
* with a single integer as defined by the `TblTableRef` structure.
*/
/// Header size: `ChunkHeader` (8B) + `4 Bytes`
public struct Tbl_Parser {
let header: ChunkHeader
/// The number of `TblPackage` structures.
public let packageCount: UInt32
/// Global `StringPool` for `StringPoolRef` lookup.
/// Each `TblPackage` may have addicional `StringPool` for lookup.
public let stringPool: StringPool
init(_ bytes: RawBytes) throws {
header = try ChunkHeader(0, bytes, expect: .Table)
packageCount = bytes.int32(header.index(.startOfHeader))
// StringPool must follow first chunk
stringPool = try StringPool(header.index(.startOfData), bytes)
}
// MARK: - Public methods
/// List all packages
public func getPackages() throws -> [TblPackage] {
try header.children(filter: { $0.type == .TblPackage }).map { TblPackage($0) }
}
/// Get a specific package. E.g., by using `TblTableRef.package`
public func getPackage(_ pkgId: UInt8) throws -> TblPackage? {
var rv: TblPackage? = nil
try header.iterChildren { abort, chunk in
guard chunk.type == .TblPackage else { return }
let pkg = TblPackage(chunk)
if pkg.id == pkgId {
abort = true
rv = pkg
}
}
return rv
}
/// Access specific resource using a full `TblTableRef` reference
public func getResource(_ ref: TblTableRef) throws -> Resource? {
guard let pkg = try getPackage(ref.package) else {
return nil
}
guard let (spec, types) = try pkg.getType(ref.type) else {
return nil
}
let entries = try types.compactMap {
if let val = try $0.getValue(ref.entry) {
return Resource.EntrySkin(
config: $0.config, entry: val)
}
return nil
}
let specFlags = spec.getEntry(Int(ref.entry))
return Resource(package: pkg, specFlags: specFlags, entries: entries)
}
/// Structured access to all layers (skins) of a table resource
public struct Resource {
public let package: TblPackage
public let specFlags: TblTypeSpecFlags
public let entries: [EntrySkin]
public struct EntrySkin {
public let config: TblTypeConfig
public let entry: TblEntry
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* Individual attributes of an `XmlStartElement`
*/
/// Size: `12 Bytes` + `ResValue` (8B)
public struct XmlAttribute {
/// Namespace of this attribute.
public let ns: StringPoolRef // UInt32
/// Name of this attribute.
public let name: StringPoolRef // UInt32
/// The original raw string value of this attribute.
/// Should be the same as `typedValue.data` (will be `0xFFFFFFFF` if unset).
public let rawValue: StringPoolRef // UInt32
/// Processed typed value of this attribute.
public let typedValue: ResValue // 8 Bytes
init(_ br: inout RawBytes.Reader) throws {
ns = br.read32()
name = br.read32()
rawValue = br.read32()
typedValue = try ResValue(&br)
}
}

View File

@@ -0,0 +1,20 @@
/**
* Extended XML tree node for CDATA tags -- includes the CDATA string.
* Appears `header.headerSize` bytes after a `XmlNode`.
*/
/// Header size: `ChunkHeader` (8B) + `XmlNode` (8B) + `4 Bytes` + `ResValue` (8B)
public struct XmlCDATA: XmlNode {
let header: ChunkHeader
/// The raw CDATA character data.
public let data: StringPoolRef // UInt32
/// The typed value of the character data if this is a CDATA node.
public let typedData: ResValue // 8 Bytes
init(_ chunk: ChunkHeader) throws {
header = chunk
var br = header.byteReader(at: .startOfData) // skips XmlNode header
data = br.read32()
typedData = try ResValue(&br)
}
}

View File

@@ -0,0 +1,79 @@
// MARK: - Start
/**
* Extended XML tree node for start tags -- includes attribute information.
* Appears `header.headerSize` bytes after a `XmlNode`.
*/
/// Header size: `ChunkHeader` (8B) + `XmlNode` (8B) + `20 Bytes`
public struct XmlStartElement: XmlNode {
let header: ChunkHeader
/// String of the full namespace of this element.
public let ns: StringPoolRef // UInt32
/// String name of this node if it is an `ELEMENT`; the raw character data if this is a `CDATA` node.
public let name: StringPoolRef // UInt32
/// Byte offset from the start of this structure where the attributes start.
let attributeStart: UInt16
/// Size of the `XmlAttribute` structures that follow.
let attributeSize: UInt16
/// Number of attributes associated with an `ELEMENT`.
/// These are available as an array of `XmlAttribute` structures immediately following this node.
public let attributeCount: UInt16
/// Index (1-based) of the "id" attribute. `0` if none.
public let idIndex: UInt16
/// Index (1-based) of the "class" attribute. `0` if none.
public let classIndex: UInt16
/// Index (1-based) of the "style" attribute. `0` if none.
public let styleIndex: UInt16
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfData) // skips XmlNode header
ns = br.read32()
name = br.read32()
attributeStart = br.read16()
attributeSize = br.read16()
attributeCount = br.read16()
idIndex = br.read16()
classIndex = br.read16()
styleIndex = br.read16()
}
// MARK: - Public methods
/// Generate and populate all attributes on the fly
public func attributes() throws -> [XmlAttribute] {
var br = header.byteReader(at: .startOfData)
let offset = br.index + Index(attributeStart)
let size = Index(attributeSize)
return try (0 ..< Index(attributeCount)).map {
br.index = offset + $0 * size
return try XmlAttribute(&br)
}
}
}
// MARK: - End
/**
* Extended XML tree node for element start/end nodes.
* Appears `header.headerSize` bytes after a `XmlNode`.
*/
/// Header size: `ChunkHeader` (8B) + `XmlNode` (8B) + `8 Bytes`
public struct XmlEndElement: XmlNode {
let header: ChunkHeader
/// String of the full namespace of this element.
public let ns: StringPoolRef // UInt32
/// String name of this node if it is an `ELEMENT`; the raw character data if this is a `CDATA` node.
public let name: StringPoolRef // UInt32
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfData) // skips XmlNode header
ns = br.read32()
name = br.read32()
}
}

View File

@@ -0,0 +1,22 @@
/**
* Extended XML tree node for namespace start/end nodes.
* Appears `header.headerSize` bytes after a `XmlNode`.
*/
/// Header size: `ChunkHeader` (8B) + `XmlNode` (8B) + `8 Bytes`
public struct XmlNamespace: XmlNode {
let header: ChunkHeader
/// The prefix of the namespace.
public let prefix: StringPoolRef // UInt32
/// The URI of the namespace.
public let uri: StringPoolRef // UInt32
init(_ chunk: ChunkHeader) {
header = chunk
var br = header.byteReader(at: .startOfData) // skips XmlNode header
prefix = br.read32()
uri = br.read32()
}
}
// NOTE: start and end nodes are identical

View File

@@ -0,0 +1,24 @@
/**
* Basic XML tree node. A single item in the XML document.
* Extended info about the node can be found after `header.headerSize`.
*/
/// Header size: `ChunkHeader` (8B) + `8 Bytes`
protocol XmlNode {
var header: ChunkHeader { get }
}
extension XmlNode {
/// Line number in original source file at which this element appeared.
public var lineNumber: UInt32 {
header._bytes.int32(header.index(.startOfHeader) + 0)
}
/// Optional XML comment that was associated with this element
public var comment: StringPoolRef? {
let val = header._bytes.int32(header.index(.startOfHeader) + 4)
return val == 0xFFFFFFFF ? nil : val // `-1` if none.
}
/// Absolute offset in data
public var offset: Index { header.index(.startOfChunk) }
}

View File

@@ -0,0 +1,20 @@
/**
* Array mapping strings in the string pool back to resource identifiers.
*/
/// Header size: `ChunkHeader` (8B) + `0 Bytes`
public struct XmlResourceMap {
let header: ChunkHeader
init(_ chunk: ChunkHeader) {
header = chunk
}
public var count: Int {
(header.index(.afterChunk) - header.index(.startOfData)) / 4
}
public func entries() -> [TblTableRef] {
var br = header.byteReader(at: .startOfData)
return (0..<count).map { _ in TblTableRef(br.read32()) }
}
}

View File

@@ -0,0 +1,218 @@
/**
* XML tree header. This appears at the front of an XML tree, describing its content.
*
* It is followed by a flat array of `XmlNode` structures; the hierarchy of the XML document is described
* by the occurrance of `XmlStartElement` and corresponding `XmlEndElement` nodes in the array.
*/
/// Header size: `ChunkHeader` (8B)
public struct Xml_Parser {
let header: ChunkHeader
/// Global `StringPool` for `StringPoolRef` lookup.
public let stringPool: StringPool
init(_ bytes: RawBytes) throws {
header = try ChunkHeader(0, bytes, expect: .XmlTree)
// StringPool must follow first chunk
stringPool = try StringPool(header.index(.startOfData), bytes)
}
// MARK: - Public methods
/// Retrieve `.XmlResourceMap` (if there is one)
public func getResourceMap() throws -> XmlResourceMap? {
var rv: XmlResourceMap? = nil
try header.iterChildren { abort, chunk in
if chunk.type == .XmlResourceMap {
abort = true
rv = XmlResourceMap(chunk)
}
}
return rv
}
/// Iterate over whole tree and call `start` and `end` blocks for each element.
/// Each `start` block contains the `tag` name and its attribute `(name, value)` pairs.
public func iterElements(_ start: (_ startTag: String, _ attributes: [(String, ResValue)]) -> Void, _ end: (_ endTag: String) -> Void) throws {
// cache StringPool lookups for faster access
var _lookupTable: [StringPoolRef: String] = [:]
func lookup(_ index: StringPoolRef) -> String {
if let cached = _lookupTable[index] {
return cached
}
let val = stringPool.getString(index)
_lookupTable[index] = val
return val
}
// cache namespace prefix strings
var namespaces: [StringPoolRef: String] = [:]
var nsOnNextTag: StringPoolRef? = nil
try header.iterChildren { _, chunk in
switch chunk.type {
case .StringPool, .XmlResourceMap:
break // ignore
case .XmlCDATA:
// let cdata = try XmlCDATA(chunk)
break // ignore
case .XmlStartNamespace:
let ns = XmlNamespace(chunk)
namespaces[ns.uri] = lookup(ns.prefix)
nsOnNextTag = ns.uri
case .XmlEndNamespace:
namespaces.removeValue(forKey: XmlNamespace(chunk).uri)
nsOnNextTag = nil
case .XmlStartElement:
let elem = XmlStartElement(chunk)
var attrs: [(String, ResValue)] = []
// if tag is preceded by a namespace, apply ns-attribute
if let uri = nsOnNextTag, let prefix = namespaces[uri] {
attrs.append(("xmlns:\(prefix)", ResValue(type: .String, data: uri)))
}
nsOnNextTag = nil
// generate attributes
for attr in try elem.attributes() {
var key = lookup(attr.name)
if let prefix = namespaces[attr.ns] {
key = prefix + ":" + key
}
attrs.append((key, attr.typedValue))
}
start(lookup(elem.name), attrs)
case .XmlEndElement:
end(lookup(XmlEndElement(chunk).name))
default:
throw AXMLError("Dont know how to handle \(chunk.type) (offset: \(chunk.index(.startOfChunk)))")
}
}
}
// MARK: XML string
/// Return data as XML string.
public func xmlString(prettyPrint: Bool = true, collapseEmptyElements: Bool = true, indent: String = " ") throws -> String {
var rv = #"<?xml version="1.0" encoding="utf-8"?>"#
func newLine() -> String {
prettyPrint ? "\n" + String(repeating: indent, count: lvl) : ""
}
var lvl = 0
var lastOpen = false
try iterElements({ startTag, attrs in
if lastOpen {
rv.append(">")
}
rv.append(newLine() + "<" + startTag)
for (key, attr) in attrs {
// yes, strings can contain quotes. Thus we must escape them.
// Hint: no `replace` because that is only available with Foundation
let val = attr.dataType == .String
? stringPool.getString(attr.asStringRef)
.split(separator: #"""#).joined(separator: #"\""#)
: attr.resolve(nil)
rv.append(" \(key)=\"\(val)\"")
}
lvl += 1
if collapseEmptyElements {
lastOpen = true
} else {
rv.append(">")
}
}) { closeTag in
lvl -= 1
if lastOpen {
rv.append("/>")
lastOpen = false
} else {
rv.append(newLine() + "</" + closeTag + ">")
}
}
return rv
}
}
// MARK: SVG converter
extension Xml_Parser {
/// Same as `xmlString()` but converts tag names and attribute names to SVG standard.
@available(*, deprecated, message: "Much more work needed for a proper SVG parser. This is just a very rudimentary converter for a quick preview.")
public func svgString() throws -> String {
var rv = #"<?xml version="1.0" encoding="utf-8"?>"#
func newLine() -> String {
"\n" + String(repeating: " ", count: lvl)
}
func unitless(_ val: ResValue) -> String {
switch val.dataType {
case .Dimension, .Fraction: "\(val.asDimension.value)"
default: val.resolve(stringPool)
}
}
var lvl = 0
var lastOpen = false
try iterElements({ startTag, attrs in
if lastOpen {
rv.append(">")
}
var dict = attrs.reduce(into: [:]) { $0[$1.0] = $1.1 }
if startTag == "vector" {
rv.append(newLine() + #"<svg version="1.1" xmlns="http://www.w3.org/2000/svg""#)
if let val = dict.removeValue(forKey: "android:height") {
rv.append(" height=\"\(unitless(val))\"")
}
if let val = dict.removeValue(forKey: "android:width") {
rv.append(" width=\"\(unitless(val))\"")
}
let vpw = dict.removeValue(forKey: "android:viewportWidth")
let vph = dict.removeValue(forKey: "android:viewportHeight")
if let vpw, let vph {
rv.append(" viewBox=\"0 0 \(unitless(vpw)) \(unitless(vph))\"")
}
} else {
rv.append(newLine() + "<" + startTag)
if let val = dict.removeValue(forKey: "android:fillColor") {
if val.dataType.isColor {
let col = val.asColor
rv.append(" fill=\"\(col.rgb8())\"")
if col.a != 255 {
rv.append(" fill-opacity=\"\(col.fa)\"")
}
} else {
// TODO: other resource lookup & gradient converter
rv.append(" fill=\"\(val.resolve(stringPool))\"")
}
}
if let val = dict.removeValue(forKey: "android:pathData") {
rv.append(" d=\"\(val.resolve(stringPool))\"")
}
}
// append remaining attributes
for (k, v) in dict {
rv.append(" \(k)=\"\(v.resolve(stringPool))\"")
}
lvl += 1
lastOpen = true
}) { closeTag in
lvl -= 1
if lastOpen {
rv.append("/>")
lastOpen = false
} else {
if closeTag == "vector" {
rv.append(newLine() + "</svg>")
} else {
rv.append(newLine() + "</" + closeTag + ">")
}
}
}
return rv
}
}

View File

@@ -0,0 +1,71 @@
import Testing
import Foundation
import AndroidXML
struct Test {
@Test func foo() async throws {
// code for debugging. Run with Cmd+U
// printXml(.t1, "AndroidManifest.xml")
// printRes(.t1, 0x7F100000)
// try allXmlForFolder(.t1)
// try attrResolveEverything(.t1)
print("done.")
}
private enum Pth: String {
case t1 = "/path/to/unpacked/apk/dir/"
}
private func printXml(_ base: Pth, _ path: String) {
print(try! AndroidXML(path: base.rawValue + "/" + path).parseXml().xmlString())
}
private func printRes(_ base: Pth, _ id: UInt32) {
let xml = try! AndroidXML(path: base.rawValue + "/resources.arsc").parseTable()
let res = try! xml.getResource(TblTableRef(id))!
let pkgPool = res.package.stringPool(for: .Keys)!
for x in res.entries {
let val = x.entry.value!
print(pkgPool.getString(x.entry.key), val.resolve(xml.stringPool), x.config.screenType.density)
}
}
private func allXmlForFolder(_ pth: Pth) throws {
let xx = FileManager.default.enumerator(atPath: pth.rawValue + "/res/")!
for x in xx.allObjects as! [String] {
if !x.hasSuffix(".xml") {
continue
}
// only process files which are binary-xml
if let xml = try? AndroidXML(path: pth.rawValue + "/res/" + x) {
print(x)
let _ = try xml.parseXml().xmlString()
}
}
}
private func attrResolveEverything(_ pth: Pth) throws {
let xml = try AndroidXML(path: pth.rawValue + "/resources.arsc").parseTable()
for pkg in try xml.getPackages() {
let pool = pkg.stringPool(for: .Keys)!
try pkg.iterTypes { spec, types in
//print("----- \(spec.id) ------")
for type in types {
try type.iterValues { idx, entry in
if entry.isComplex {
//print("::.::.complex", pool.getString(entry.key), entry.valueMap!.parent.asHex)
for x in entry.valueMap!.entries {
let _ = x.name.asHex
let _ = x.value.resolve(xml.stringPool)
}
} else {
let _ = pool.getString(entry.key)
let _ = entry.value!.resolve(xml.stringPool)
}
}
}
}
}
}
}