apktools 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. data/lib/apktools/apkresources.rb +683 -623
  2. data/lib/apktools/apkxml.rb +422 -422
  3. metadata +10 -12
@@ -1,14 +1,14 @@
1
1
  # Copyright (C) 2014 Dave Smith
2
- #
2
+ #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
4
  # software and associated documentation files (the "Software"), to deal in the Software
5
5
  # without restriction, including without limitation the rights to use, copy, modify,
6
6
  # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
7
  # persons to whom the Software is furnished to do so, subject to the following conditions:
8
- #
8
+ #
9
9
  # The above copyright notice and this permission notice shall be included in all copies
10
10
  # or substantial portions of the Software.
11
- #
11
+ #
12
12
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
13
  # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
14
  # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
@@ -16,427 +16,427 @@
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17
17
  # DEALINGS IN THE SOFTWARE.
18
18
 
19
- require 'zip/zip'
19
+ require 'zip'
20
20
  require 'apktools/apkresources'
21
21
 
22
22
  ##
23
23
  # Class to parse an APK's binary XML format back into textual XML
24
24
  class ApkXml
25
-
26
- DEBUG = false # :nodoc:
27
-
28
- ##
29
- # Structure defining the type and size of each resource chunk
30
- #
31
- # ChunkHeader = Struct.new(:type, :size, :chunk_size)
32
- ChunkHeader = Struct.new(:type, :size, :chunk_size)
33
-
34
- ##
35
- # Structure that houses a group of strings
36
- #
37
- # StringPool = Struct.new(:header, :string_count, :style_count, :values)
38
- #
39
- # * +header+ = ChunkHeader
40
- # * +string_count+ = Number of normal strings in the pool
41
- # * +style_count+ = Number of styled strings in the pool
42
- # * +values+ = Array of the string values
43
- StringPool = Struct.new(:header, :string_count, :style_count, :values)
44
-
45
- ##
46
- # Structure to house mappings of resource ids to strings
47
- #
48
- # XmlResourceMap = Struct.new(:header, :ids, :strings)
49
- #
50
- # * +header+ = ChunkHeader
51
- # * +ids+ = Array of resource ids
52
- # * +strings+ = Matching Array of resource strings
53
- XmlResourceMap = Struct.new(:header, :ids, :strings)
54
-
55
- ##
56
- # Structure defining header of an XML node
57
- #
58
- # XmlTreeHeader = Struct.new(:header, :line_num, :comment)
59
- #
60
- # * +header+ = ChunkHeader
61
- # * +line_num+ = Line number in original file
62
- # * +comment+ = Optional comment
63
- XmlTreeHeader = Struct.new(:header, :line_num, :comment)
64
-
65
- ##
66
- # Structure defining an XML element
67
- #
68
- # XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
69
- #
70
- # * +header+ = XmlTreeHeader
71
- # * +namespace+ = Namespace prefix of the element
72
- # * +name+ = Name of the element
73
- # * +id_idx+ = Index of the attribute that represents the "id" in this element, if any
74
- # * +class_idx+ = Index of the attribute that represents the "class" in this element, if any
75
- # * +style_idx+ = Index of the attribute that represents the "style" in this element, if any
76
- # * +attributes+ = Array of XmlAttribute elements
77
- # * +is_root+ = Marks if this is the root element
78
- XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
79
-
80
- ##
81
- # Structure defining an XML element's attribute
82
- #
83
- # XmlAttribute = Struct.new(:namespace, :name, :raw, :value)
84
- #
85
- # * +namespace+ = Namespace prefix of the attribute
86
- # * +name+ = Name of the attribute
87
- # * +value+ = Value of the attribute
88
- XmlAttribute = Struct.new(:namespace, :name, :value)
89
-
90
- ##
91
- # Structure that houses the data for a given resource entry
92
- #
93
- # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
94
- #
95
- # * +flags+ = Flags marking if the resource is complex or public
96
- # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
97
- # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
98
- # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
99
- #
100
- # A single resource key can have multiple entries depending on configuration, so these structs
101
- # are often returned in groups, keyed by a ResTypeConfig
102
- ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
103
-
104
- # APK file where parser will search for XML
105
- attr_reader :current_apk
106
- # ApkResources instance used to resolve resources in this APK
107
- attr_reader :apk_resources
108
- # Array of XmlElements from the last parse operation
109
- attr_reader :xml_elements
110
-
111
- ##
112
- # Create a new ApkXml instance from the specified +apk_file+
113
- #
114
- # This opens and parses the contents of the APK's resources.arsc file.
115
- def initialize(apk_file)
116
- @current_apk = apk_file
117
- @apk_resources = ApkResources.new(apk_file)
118
- end #initialize
119
-
120
- ##
121
- # Read the requested XML file from inside the APK and parse out into
122
- # readable textual XML. Returns a string of the parsed XML.
123
- #
124
- # xml_file: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
125
- # pretty: Optionally format the XML output as human readable
126
- # resolve_resources: Optionally, where possible, resolve resource references to their default value
127
- #
128
- # This opens and parses the contents of the APK's resources.arsc file.
129
- def parse_xml(xml_file, pretty = false, resolve_resources = false)
130
- # Reset variables
131
- @xml_elements = Array.new()
132
- xml_output = ""
133
- indent = 0
134
- data = nil
135
-
136
- # Get the XML from the APK file
137
- Zip::ZipFile.foreach(@current_apk) do |f|
138
- if f.name.match(xml_file)
139
- data = f.get_input_stream.read
140
- end
141
- end
142
-
143
- # Parse the Header Chunk
144
- header = ChunkHeader.new( read_short(data, HEADER_START),
145
- read_short(data, HEADER_START+2),
146
- read_word(data, HEADER_START+4) )
147
-
148
- # Parse the StringPool Chunk
149
- startoffset_pool = HEADER_START + header.size
150
- puts "Parse Main StringPool Chunk" if DEBUG
151
- stringpool_main = parse_stringpool(data, startoffset_pool)
152
- puts "#{stringpool_main.values.length} strings found" if DEBUG
153
-
154
- # Parse the remainder of the file chunks based on type
155
- namespaces = Hash.new()
156
- current = startoffset_pool + stringpool_main.header.chunk_size
157
- puts "Parse Remaining Chunks" if DEBUG
158
- while current < data.length
159
- ## Parse Header
160
- header = ChunkHeader.new( read_short(data, current),
161
- read_short(data, current+2),
162
- read_word(data, current+4) )
163
- ## Check Type
164
- if header.type == TYPE_XML_RESOURCEMAP
165
- ## Maps resource ids to strings in the pool
166
- map_ids = Array.new()
167
- map_strings = Array.new()
168
-
169
- index_offset = current + header.size
170
- i = 0
171
- while index_offset < (current + header.chunk_size)
172
- map_ids << read_word(data, index_offset)
173
- map_strings << stringpool_main.values[i]
174
-
175
- i += 1
176
- index_offset = i * 4 + (current + header.size)
177
- end
178
-
179
- current += header.chunk_size
180
- elsif header.type == TYPE_XML_STARTNAMESPACE
181
- tree_header = parse_tree_header(header, data, current)
182
- body_start = current+header.size
183
- prefix = stringpool_main.values[read_word(data, body_start)]
184
- uri = stringpool_main.values[read_word(data, body_start+4)]
185
- namespaces[uri] = prefix
186
- puts "NAMESPACE_START: xmlns:#{prefix} = '#{uri}'" if DEBUG
187
- current += header.chunk_size
188
- elsif header.type == TYPE_XML_ENDNAMESPACE
189
- tree_header = parse_tree_header(header, data, current)
190
- body_start = current+header.size
191
- prefix = stringpool_main.values[read_word(data, body_start)]
192
- uri = stringpool_main.values[read_word(data, body_start+4)]
193
- puts "NAMESPACE_END: xmlns:#{prefix} = '#{uri}'" if DEBUG
194
- current += header.chunk_size
195
- elsif header.type == TYPE_XML_STARTELEMENT
196
- tree_header = parse_tree_header(header, data, current)
197
- body_start = current+header.size
198
- # Parse the element/attribute data
199
- namespace = nil
200
- if read_word(data, body_start) != OFFSET_NO_ENTRY
201
- namespace = stringpool_main.values[read_word(data, body_start)]
202
- end
203
- name = stringpool_main.values[read_word(data, body_start+4)]
204
-
205
- attribute_offset = read_short(data, body_start+8)
206
- attribute_size = read_short(data, body_start+10)
207
- attribute_count = read_short(data, body_start+12)
208
- id_idx = read_short(data, body_start+14)
209
- class_idx = read_short(data, body_start+16)
210
- style_idx = read_short(data, body_start+18)
211
-
212
- attributes = Array.new()
213
- i=0
214
- while i < attribute_count
215
- index_offset = i * attribute_size + (body_start + attribute_offset)
216
- attr_namespace = nil
217
- if read_word(data, index_offset) != OFFSET_NO_ENTRY
218
- attr_uri = stringpool_main.values[read_word(data, index_offset)]
219
- attr_namespace = namespaces[attr_uri]
220
- end
221
- attr_name = stringpool_main.values[read_word(data, index_offset+4)]
222
- attr_raw = nil
223
- if read_word(data, index_offset+8) != OFFSET_NO_ENTRY
224
- # Attribute has a raw value, use it
225
- attr_raw = stringpool_main.values[read_word(data, index_offset+8)]
226
- end
227
- entry = ResTypeEntry.new(0, nil, read_byte(data, index_offset+15), read_word(data, index_offset+16))
228
-
229
- attr_value = nil
230
- if attr_raw != nil # Use raw value
231
- attr_value = attr_raw
232
- elsif entry.data_type == 1 # Value is a references to a resource
233
- # Find the resource
234
- default_res = apk_resources.get_default_resource_value(entry.data)
235
- if resolve_resources && default_res != nil
236
- # Use the default resource value
237
- attr_value = default_res.data
238
- else
239
- key_value = apk_resources.get_resource_key(entry.data, true)
240
- if key_value != nil
241
- # Use the key string
242
- attr_value = key_value
243
- else
244
- #No key found, use raw id marked as a resource
245
- attr_value = "res:0x#{entry.data.to_s(16)}"
246
- end
247
- end
248
- else # Value is a constant
249
- attr_value = "0x#{entry.data.to_s(16)}"
250
- end
251
-
252
-
253
- attributes << XmlAttribute.new(attr_namespace, attr_name, attr_value)
254
- i += 1
255
- end
256
-
257
- element = XmlElement.new(tree_header, namespace, name, id_idx, class_idx, style_idx, attributes, xml_output == "")
258
-
259
- # Print the element/attribute data
260
- puts "ELEMENT_START: #{element.namespace} #{element.name}" if DEBUG
261
- display_name = element.namespace == nil ? element.name : "#{element.namespace}:#{element.name}"
262
-
263
- if pretty
264
- xml_output += "\n" + (" " * indent)
265
- indent += 1
266
- end
267
- xml_output += "<#{display_name} "
268
- # Only print namespaces on the root element
269
- if element.is_root
270
- keys = namespaces.keys
271
- keys.each do |key|
272
- xml_output += "xmlns:#{namespaces[key]}=\"#{key}\" "
273
- if pretty && key != keys.last
274
- xml_output += "\n" + (" " * indent)
275
- end
276
- end
277
- end
278
-
279
- element.attributes.each do |attr|
280
- puts "---ATTRIBUTE: #{attr.namespace} #{attr.name} #{attr.value}" if DEBUG
281
- display_name = attr.namespace == nil ? attr.name : "#{attr.namespace}:#{attr.name}"
282
- if pretty
283
- xml_output += "\n" + (" " * indent)
284
- end
285
- xml_output += "#{display_name}=\"#{attr.value}\" "
286
- end
287
-
288
- xml_output += ">"
289
-
290
- # Push every new element onto the array
291
- @xml_elements << element
292
-
293
- current += header.chunk_size
294
- elsif header.type == TYPE_XML_ENDELEMENT
295
- tree_header = parse_tree_header(header, data, current)
296
- body_start = current+header.size
297
- namespace = nil
298
- if read_word(data, body_start) != OFFSET_NO_ENTRY
299
- namespace = stringpool_main.values[read_word(data, body_start)]
300
- end
301
- name = stringpool_main.values[read_word(data, body_start+4)]
302
-
303
- puts "ELEMENT END: #{namespace} #{name}" if DEBUG
304
- display_name = namespace == nil ? name : "#{namespace}:#{name}"
305
- if pretty
306
- indent -= 1
307
- if indent < 0
308
- indent = 0
309
- end
310
- xml_output += "\n" + (" " * indent)
311
- end
312
- xml_output += "</#{display_name}>"
313
-
314
-
315
- current += header.chunk_size
316
- elsif header.type == TYPE_XML_CDATA
317
- tree_header = parse_tree_header(header, data, current)
318
- body_start = current+header.size
319
-
320
- cdata = stringpool_main.values[read_word(data, body_start)]
321
- cdata_type = read_word(data, body_start+7)
322
- cdata_value = read_word(data, body_start+8)
323
- puts "CDATA: #{cdata} #{cdata_type} #{cdata_value}" if DEBUG
324
-
325
- cdata.split(/\r?\n/).each do |item|
326
- if pretty
327
- xml_output += "\n" + (" " * indent)
328
- end
329
- xml_output += "<![CDATA[#{item.strip}]]>"
330
- end
331
-
332
- current += header.chunk_size
333
- else
334
- puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
335
- ## End Immediately
336
- current = data.length
337
- end
338
- end
339
-
340
- return xml_output
341
- end #parse_xml
342
-
343
- private # Private Helper Methods
344
-
345
- #Flag Constants
346
- FLAG_UTF8 = 0x100 # :nodoc:
347
-
348
- OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
349
- HEADER_START = 0 # :nodoc:
350
-
351
- TYPE_XML_RESOURCEMAP = 0x180 # :nodoc:
352
- TYPE_XML_STARTNAMESPACE = 0x100 # :nodoc:
353
- TYPE_XML_ENDNAMESPACE = 0x101 # :nodoc:
354
- TYPE_XML_STARTELEMENT = 0x102 # :nodoc:
355
- TYPE_XML_ENDELEMENT = 0x103 # :nodoc:
356
- TYPE_XML_CDATA = 0x104 # :nodoc:
357
-
358
- # Read a 32-bit word from a specific location in the data
359
- def read_word(data, offset)
360
- out = data[offset,4].unpack('V').first rescue 0
361
- return out
362
- end
363
-
364
- # Read a 16-bit short from a specific location in the data
365
- def read_short(data, offset)
366
- out = data[offset,2].unpack('v').first rescue 0
367
- return out
368
- end
369
-
370
- # Read a 8-bit byte from a specific location in the data
371
- def read_byte(data, offset)
372
- out = data[offset,1].unpack('C').first rescue 0
373
- return out
374
- end
375
-
376
- # Read in length bytes in as a String
377
- def read_string(data, offset, length, encoding)
378
- if "UTF-16".casecmp(encoding) == 0
379
- out = data[offset, length].unpack('v*').pack('U*')
380
- else
381
- out = data[offset, length].unpack('C*').pack('U*')
382
- end
383
- return out
384
- end
385
-
386
- # Parse out an XmlTreeHeader
387
- def parse_tree_header(chunk_header, data, offset)
388
- line_num = read_word(data, offset+8)
389
- comment = nil
390
- if read_word(data, offset+12) != OFFSET_NO_ENTRY
391
- comment = stringpool_main.values[read_word(data, offset+12)]
392
- end
393
- return XmlTreeHeader.new(chunk_header, line_num, comment)
394
- end
395
-
396
- # Parse out a StringPool chunk
397
- def parse_stringpool(data, offset)
398
- pool_header = ChunkHeader.new( read_short(data, offset),
399
- read_short(data, offset+2),
400
- read_word(data, offset+4) )
401
-
402
- pool_string_count = read_word(data, offset+8)
403
- pool_style_count = read_word(data, offset+12)
404
- pool_flags = read_word(data, offset+16)
405
- format_utf8 = (pool_flags & FLAG_UTF8) != 0
406
- puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
407
-
408
- pool_string_offset = read_word(data, offset+20)
409
- pool_style_offset = read_word(data, offset+24)
410
-
411
- values = Array.new()
412
- i = 0
413
- while i < pool_string_count
414
- # Read the string value
415
- index = i * 4 + (offset+28)
416
- offset_addr = pool_string_offset + offset + read_word(data, index)
417
- if format_utf8
418
- length = read_byte(data, offset_addr)
419
- if (length & 0x80) != 0
420
- length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
421
- end
422
-
423
- values << read_string(data, offset_addr + 2, length, "UTF-8")
424
- else
425
- length = read_short(data, offset_addr)
426
- if (length & 0x8000) != 0
427
- #There is one more length value before the data
428
- length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
429
- values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
430
- else
431
- # Read the data
432
- values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
433
- end
434
- end
435
-
436
- i += 1
437
- end
438
-
439
- return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
440
- end
441
-
442
- end
25
+
26
+ DEBUG = false # :nodoc:
27
+
28
+ ##
29
+ # Structure defining the type and size of each resource chunk
30
+ #
31
+ # ChunkHeader = Struct.new(:type, :size, :chunk_size)
32
+ ChunkHeader = Struct.new(:type, :size, :chunk_size)
33
+
34
+ ##
35
+ # Structure that houses a group of strings
36
+ #
37
+ # StringPool = Struct.new(:header, :string_count, :style_count, :values)
38
+ #
39
+ # * +header+ = ChunkHeader
40
+ # * +string_count+ = Number of normal strings in the pool
41
+ # * +style_count+ = Number of styled strings in the pool
42
+ # * +values+ = Array of the string values
43
+ StringPool = Struct.new(:header, :string_count, :style_count, :values)
44
+
45
+ ##
46
+ # Structure to house mappings of resource ids to strings
47
+ #
48
+ # XmlResourceMap = Struct.new(:header, :ids, :strings)
49
+ #
50
+ # * +header+ = ChunkHeader
51
+ # * +ids+ = Array of resource ids
52
+ # * +strings+ = Matching Array of resource strings
53
+ XmlResourceMap = Struct.new(:header, :ids, :strings)
54
+
55
+ ##
56
+ # Structure defining header of an XML node
57
+ #
58
+ # XmlTreeHeader = Struct.new(:header, :line_num, :comment)
59
+ #
60
+ # * +header+ = ChunkHeader
61
+ # * +line_num+ = Line number in original file
62
+ # * +comment+ = Optional comment
63
+ XmlTreeHeader = Struct.new(:header, :line_num, :comment)
64
+
65
+ ##
66
+ # Structure defining an XML element
67
+ #
68
+ # XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
69
+ #
70
+ # * +header+ = XmlTreeHeader
71
+ # * +namespace+ = Namespace prefix of the element
72
+ # * +name+ = Name of the element
73
+ # * +id_idx+ = Index of the attribute that represents the "id" in this element, if any
74
+ # * +class_idx+ = Index of the attribute that represents the "class" in this element, if any
75
+ # * +style_idx+ = Index of the attribute that represents the "style" in this element, if any
76
+ # * +attributes+ = Array of XmlAttribute elements
77
+ # * +is_root+ = Marks if this is the root element
78
+ XmlElement = Struct.new(:header, :namespace, :name, :id_idx, :class_idx, :style_idx, :attributes, :is_root)
79
+
80
+ ##
81
+ # Structure defining an XML element's attribute
82
+ #
83
+ # XmlAttribute = Struct.new(:namespace, :name, :raw, :value)
84
+ #
85
+ # * +namespace+ = Namespace prefix of the attribute
86
+ # * +name+ = Name of the attribute
87
+ # * +value+ = Value of the attribute
88
+ XmlAttribute = Struct.new(:namespace, :name, :value)
89
+
90
+ ##
91
+ # Structure that houses the data for a given resource entry
92
+ #
93
+ # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
94
+ #
95
+ # * +flags+ = Flags marking if the resource is complex or public
96
+ # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
97
+ # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
98
+ # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
99
+ #
100
+ # A single resource key can have multiple entries depending on configuration, so these structs
101
+ # are often returned in groups, keyed by a ResTypeConfig
102
+ ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
103
+
104
+ # APK file where parser will search for XML
105
+ attr_reader :current_apk
106
+ # ApkResources instance used to resolve resources in this APK
107
+ attr_reader :apk_resources
108
+ # Array of XmlElements from the last parse operation
109
+ attr_reader :xml_elements
110
+
111
+ ##
112
+ # Create a new ApkXml instance from the specified +apk_file+
113
+ #
114
+ # This opens and parses the contents of the APK's resources.arsc file.
115
+ def initialize(apk_file)
116
+ @current_apk = apk_file
117
+ @apk_resources = ApkResources.new(apk_file)
118
+ end #initialize
119
+
120
+ ##
121
+ # Read the requested XML file from inside the APK and parse out into
122
+ # readable textual XML. Returns a string of the parsed XML.
123
+ #
124
+ # xml_file: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
125
+ # pretty: Optionally format the XML output as human readable
126
+ # resolve_resources: Optionally, where possible, resolve resource references to their default value
127
+ #
128
+ # This opens and parses the contents of the APK's resources.arsc file.
129
+ def parse_xml(xml_file, pretty = false, resolve_resources = false)
130
+ # Reset variables
131
+ @xml_elements = Array.new()
132
+ xml_output = ""
133
+ indent = 0
134
+ data = nil
135
+
136
+ # Get the XML from the APK file
137
+ Zip::File.foreach(@current_apk) do |f|
138
+ if f.name.match(xml_file)
139
+ data = f.get_input_stream.read
140
+ end
141
+ end
142
+
143
+ # Parse the Header Chunk
144
+ header = ChunkHeader.new( read_short(data, HEADER_START),
145
+ read_short(data, HEADER_START+2),
146
+ read_word(data, HEADER_START+4) )
147
+
148
+ # Parse the StringPool Chunk
149
+ startoffset_pool = HEADER_START + header.size
150
+ puts "Parse Main StringPool Chunk" if DEBUG
151
+ stringpool_main = parse_stringpool(data, startoffset_pool)
152
+ puts "#{stringpool_main.values.length} strings found" if DEBUG
153
+
154
+ # Parse the remainder of the file chunks based on type
155
+ namespaces = Hash.new()
156
+ current = startoffset_pool + stringpool_main.header.chunk_size
157
+ puts "Parse Remaining Chunks" if DEBUG
158
+ while current < data.length
159
+ ## Parse Header
160
+ header = ChunkHeader.new( read_short(data, current),
161
+ read_short(data, current+2),
162
+ read_word(data, current+4) )
163
+ ## Check Type
164
+ if header.type == TYPE_XML_RESOURCEMAP
165
+ ## Maps resource ids to strings in the pool
166
+ map_ids = Array.new()
167
+ map_strings = Array.new()
168
+
169
+ index_offset = current + header.size
170
+ i = 0
171
+ while index_offset < (current + header.chunk_size)
172
+ map_ids << read_word(data, index_offset)
173
+ map_strings << stringpool_main.values[i]
174
+
175
+ i += 1
176
+ index_offset = i * 4 + (current + header.size)
177
+ end
178
+
179
+ current += header.chunk_size
180
+ elsif header.type == TYPE_XML_STARTNAMESPACE
181
+ tree_header = parse_tree_header(header, data, current)
182
+ body_start = current+header.size
183
+ prefix = stringpool_main.values[read_word(data, body_start)]
184
+ uri = stringpool_main.values[read_word(data, body_start+4)]
185
+ namespaces[uri] = prefix
186
+ puts "NAMESPACE_START: xmlns:#{prefix} = '#{uri}'" if DEBUG
187
+ current += header.chunk_size
188
+ elsif header.type == TYPE_XML_ENDNAMESPACE
189
+ tree_header = parse_tree_header(header, data, current)
190
+ body_start = current+header.size
191
+ prefix = stringpool_main.values[read_word(data, body_start)]
192
+ uri = stringpool_main.values[read_word(data, body_start+4)]
193
+ puts "NAMESPACE_END: xmlns:#{prefix} = '#{uri}'" if DEBUG
194
+ current += header.chunk_size
195
+ elsif header.type == TYPE_XML_STARTELEMENT
196
+ tree_header = parse_tree_header(header, data, current)
197
+ body_start = current+header.size
198
+ # Parse the element/attribute data
199
+ namespace = nil
200
+ if read_word(data, body_start) != OFFSET_NO_ENTRY
201
+ namespace = stringpool_main.values[read_word(data, body_start)]
202
+ end
203
+ name = stringpool_main.values[read_word(data, body_start+4)]
204
+
205
+ attribute_offset = read_short(data, body_start+8)
206
+ attribute_size = read_short(data, body_start+10)
207
+ attribute_count = read_short(data, body_start+12)
208
+ id_idx = read_short(data, body_start+14)
209
+ class_idx = read_short(data, body_start+16)
210
+ style_idx = read_short(data, body_start+18)
211
+
212
+ attributes = Array.new()
213
+ i=0
214
+ while i < attribute_count
215
+ index_offset = i * attribute_size + (body_start + attribute_offset)
216
+ attr_namespace = nil
217
+ if read_word(data, index_offset) != OFFSET_NO_ENTRY
218
+ attr_uri = stringpool_main.values[read_word(data, index_offset)]
219
+ attr_namespace = namespaces[attr_uri]
220
+ end
221
+ attr_name = stringpool_main.values[read_word(data, index_offset+4)]
222
+ attr_raw = nil
223
+ if read_word(data, index_offset+8) != OFFSET_NO_ENTRY
224
+ # Attribute has a raw value, use it
225
+ attr_raw = stringpool_main.values[read_word(data, index_offset+8)]
226
+ end
227
+ entry = ResTypeEntry.new(0, nil, read_byte(data, index_offset+15), read_word(data, index_offset+16))
228
+
229
+ attr_value = nil
230
+ if attr_raw != nil # Use raw value
231
+ attr_value = attr_raw
232
+ elsif entry.data_type == 1 # Value is a references to a resource
233
+ # Find the resource
234
+ default_res = apk_resources.get_default_resource_value(entry.data)
235
+ if resolve_resources && default_res != nil
236
+ # Use the default resource value
237
+ attr_value = default_res.data
238
+ else
239
+ key_value = apk_resources.get_resource_key(entry.data, true)
240
+ if key_value != nil
241
+ # Use the key string
242
+ attr_value = key_value
243
+ else
244
+ #No key found, use raw id marked as a resource
245
+ attr_value = "res:0x#{entry.data.to_s(16)}"
246
+ end
247
+ end
248
+ else # Value is a constant
249
+ attr_value = "0x#{entry.data.to_s(16)}"
250
+ end
251
+
252
+
253
+ attributes << XmlAttribute.new(attr_namespace, attr_name, attr_value)
254
+ i += 1
255
+ end
256
+
257
+ element = XmlElement.new(tree_header, namespace, name, id_idx, class_idx, style_idx, attributes, xml_output == "")
258
+
259
+ # Print the element/attribute data
260
+ puts "ELEMENT_START: #{element.namespace} #{element.name}" if DEBUG
261
+ display_name = element.namespace == nil ? element.name : "#{element.namespace}:#{element.name}"
262
+
263
+ if pretty
264
+ xml_output += "\n" + (" " * indent)
265
+ indent += 1
266
+ end
267
+ xml_output += "<#{display_name} "
268
+ # Only print namespaces on the root element
269
+ if element.is_root
270
+ keys = namespaces.keys
271
+ keys.each do |key|
272
+ xml_output += "xmlns:#{namespaces[key]}=\"#{key}\" "
273
+ if pretty && key != keys.last
274
+ xml_output += "\n" + (" " * indent)
275
+ end
276
+ end
277
+ end
278
+
279
+ element.attributes.each do |attr|
280
+ puts "---ATTRIBUTE: #{attr.namespace} #{attr.name} #{attr.value}" if DEBUG
281
+ display_name = attr.namespace == nil ? attr.name : "#{attr.namespace}:#{attr.name}"
282
+ if pretty
283
+ xml_output += "\n" + (" " * indent)
284
+ end
285
+ xml_output += "#{display_name}=\"#{attr.value}\" "
286
+ end
287
+
288
+ xml_output += ">"
289
+
290
+ # Push every new element onto the array
291
+ @xml_elements << element
292
+
293
+ current += header.chunk_size
294
+ elsif header.type == TYPE_XML_ENDELEMENT
295
+ tree_header = parse_tree_header(header, data, current)
296
+ body_start = current+header.size
297
+ namespace = nil
298
+ if read_word(data, body_start) != OFFSET_NO_ENTRY
299
+ namespace = stringpool_main.values[read_word(data, body_start)]
300
+ end
301
+ name = stringpool_main.values[read_word(data, body_start+4)]
302
+
303
+ puts "ELEMENT END: #{namespace} #{name}" if DEBUG
304
+ display_name = namespace == nil ? name : "#{namespace}:#{name}"
305
+ if pretty
306
+ indent -= 1
307
+ if indent < 0
308
+ indent = 0
309
+ end
310
+ xml_output += "\n" + (" " * indent)
311
+ end
312
+ xml_output += "</#{display_name}>"
313
+
314
+
315
+ current += header.chunk_size
316
+ elsif header.type == TYPE_XML_CDATA
317
+ tree_header = parse_tree_header(header, data, current)
318
+ body_start = current+header.size
319
+
320
+ cdata = stringpool_main.values[read_word(data, body_start)]
321
+ cdata_type = read_word(data, body_start+7)
322
+ cdata_value = read_word(data, body_start+8)
323
+ puts "CDATA: #{cdata} #{cdata_type} #{cdata_value}" if DEBUG
324
+
325
+ cdata.split(/\r?\n/).each do |item|
326
+ if pretty
327
+ xml_output += "\n" + (" " * indent)
328
+ end
329
+ xml_output += "<![CDATA[#{item.strip}]]>"
330
+ end
331
+
332
+ current += header.chunk_size
333
+ else
334
+ puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
335
+ ## End Immediately
336
+ current = data.length
337
+ end
338
+ end
339
+
340
+ return xml_output
341
+ end #parse_xml
342
+
343
+ private # Private Helper Methods
344
+
345
+ #Flag Constants
346
+ FLAG_UTF8 = 0x100 # :nodoc:
347
+
348
+ OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
349
+ HEADER_START = 0 # :nodoc:
350
+
351
+ TYPE_XML_RESOURCEMAP = 0x180 # :nodoc:
352
+ TYPE_XML_STARTNAMESPACE = 0x100 # :nodoc:
353
+ TYPE_XML_ENDNAMESPACE = 0x101 # :nodoc:
354
+ TYPE_XML_STARTELEMENT = 0x102 # :nodoc:
355
+ TYPE_XML_ENDELEMENT = 0x103 # :nodoc:
356
+ TYPE_XML_CDATA = 0x104 # :nodoc:
357
+
358
+ # Read a 32-bit word from a specific location in the data
359
+ def read_word(data, offset)
360
+ out = data[offset,4].unpack('V').first rescue 0
361
+ return out
362
+ end
363
+
364
+ # Read a 16-bit short from a specific location in the data
365
+ def read_short(data, offset)
366
+ out = data[offset,2].unpack('v').first rescue 0
367
+ return out
368
+ end
369
+
370
+ # Read a 8-bit byte from a specific location in the data
371
+ def read_byte(data, offset)
372
+ out = data[offset,1].unpack('C').first rescue 0
373
+ return out
374
+ end
375
+
376
+ # Read in length bytes in as a String
377
+ def read_string(data, offset, length, encoding)
378
+ if "UTF-16".casecmp(encoding) == 0
379
+ out = data[offset, length].unpack('v*').pack('U*')
380
+ else
381
+ out = data[offset, length].unpack('C*').pack('U*')
382
+ end
383
+ return out
384
+ end
385
+
386
+ # Parse out an XmlTreeHeader
387
+ def parse_tree_header(chunk_header, data, offset)
388
+ line_num = read_word(data, offset+8)
389
+ comment = nil
390
+ if read_word(data, offset+12) != OFFSET_NO_ENTRY
391
+ comment = stringpool_main.values[read_word(data, offset+12)]
392
+ end
393
+ return XmlTreeHeader.new(chunk_header, line_num, comment)
394
+ end
395
+
396
+ # Parse out a StringPool chunk
397
+ def parse_stringpool(data, offset)
398
+ pool_header = ChunkHeader.new( read_short(data, offset),
399
+ read_short(data, offset+2),
400
+ read_word(data, offset+4) )
401
+
402
+ pool_string_count = read_word(data, offset+8)
403
+ pool_style_count = read_word(data, offset+12)
404
+ pool_flags = read_word(data, offset+16)
405
+ format_utf8 = (pool_flags & FLAG_UTF8) != 0
406
+ puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
407
+
408
+ pool_string_offset = read_word(data, offset+20)
409
+ pool_style_offset = read_word(data, offset+24)
410
+
411
+ values = Array.new()
412
+ i = 0
413
+ while i < pool_string_count
414
+ # Read the string value
415
+ index = i * 4 + (offset+28)
416
+ offset_addr = pool_string_offset + offset + read_word(data, index)
417
+ if format_utf8
418
+ length = read_byte(data, offset_addr)
419
+ if (length & 0x80) != 0
420
+ length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
421
+ end
422
+
423
+ values << read_string(data, offset_addr + 2, length, "UTF-8")
424
+ else
425
+ length = read_short(data, offset_addr)
426
+ if (length & 0x8000) != 0
427
+ #There is one more length value before the data
428
+ length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
429
+ values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
430
+ else
431
+ # Read the data
432
+ values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
433
+ end
434
+ end
435
+
436
+ i += 1
437
+ end
438
+
439
+ return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
440
+ end
441
+
442
+ end