apktools 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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