Initial
This commit is contained in:
61
Sources/AndroidXML/AndroidXML.swift
Normal file
61
Sources/AndroidXML/AndroidXML.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
48
Sources/AndroidXML/BaseTypes.swift
Normal file
48
Sources/AndroidXML/BaseTypes.swift
Normal 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
|
||||
}
|
||||
81
Sources/AndroidXML/ChunkHeader.swift
Normal file
81
Sources/AndroidXML/ChunkHeader.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
57
Sources/AndroidXML/ColorComponents.swift
Normal file
57
Sources/AndroidXML/ColorComponents.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
135
Sources/AndroidXML/RawBytes.swift
Normal file
135
Sources/AndroidXML/RawBytes.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
198
Sources/AndroidXML/ResValue.swift
Normal file
198
Sources/AndroidXML/ResValue.swift
Normal 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]
|
||||
98
Sources/AndroidXML/StringPool.swift
Normal file
98
Sources/AndroidXML/StringPool.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
22
Sources/AndroidXML/StringPoolSpan.swift
Normal file
22
Sources/AndroidXML/StringPoolSpan.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
91
Sources/AndroidXML/TblEntry.swift
Normal file
91
Sources/AndroidXML/TblEntry.swift
Normal 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
|
||||
//}
|
||||
98
Sources/AndroidXML/TblEntryMap.swift
Normal file
98
Sources/AndroidXML/TblEntryMap.swift
Normal 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
|
||||
}
|
||||
}
|
||||
100
Sources/AndroidXML/TblPkg.swift
Normal file
100
Sources/AndroidXML/TblPkg.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
34
Sources/AndroidXML/TblTableRef.swift
Normal file
34
Sources/AndroidXML/TblTableRef.swift
Normal 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) + ")" }
|
||||
}
|
||||
179
Sources/AndroidXML/TblType.swift
Normal file
179
Sources/AndroidXML/TblType.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
401
Sources/AndroidXML/TblTypeConfig.swift
Normal file
401
Sources/AndroidXML/TblTypeConfig.swift
Normal 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
|
||||
}
|
||||
}
|
||||
91
Sources/AndroidXML/TblTypeSpec.swift
Normal file
91
Sources/AndroidXML/TblTypeSpec.swift
Normal 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)
|
||||
}
|
||||
82
Sources/AndroidXML/Tbl_Parser.swift
Normal file
82
Sources/AndroidXML/Tbl_Parser.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
Sources/AndroidXML/XmlAttribute.swift
Normal file
22
Sources/AndroidXML/XmlAttribute.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
20
Sources/AndroidXML/XmlCDATA.swift
Normal file
20
Sources/AndroidXML/XmlCDATA.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
79
Sources/AndroidXML/XmlElement.swift
Normal file
79
Sources/AndroidXML/XmlElement.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
22
Sources/AndroidXML/XmlNamespace.swift
Normal file
22
Sources/AndroidXML/XmlNamespace.swift
Normal 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
|
||||
24
Sources/AndroidXML/XmlNode.swift
Normal file
24
Sources/AndroidXML/XmlNode.swift
Normal 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) }
|
||||
}
|
||||
20
Sources/AndroidXML/XmlResourceMap.swift
Normal file
20
Sources/AndroidXML/XmlResourceMap.swift
Normal 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()) }
|
||||
}
|
||||
}
|
||||
218
Sources/AndroidXML/Xml_Parser.swift
Normal file
218
Sources/AndroidXML/Xml_Parser.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user