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

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
}
}