apktools 0.1.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.
- data/lib/apktools/apkresources.rb +586 -0
- data/lib/apktools/apkxml.rb +424 -0
- data/lib/apktools/resconfiguration.rb +102 -0
- metadata +63 -0
@@ -0,0 +1,586 @@
|
|
1
|
+
# Copyright (C) 2012 Dave Smith
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
# software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
# without restriction, including without limitation the rights to use, copy, modify,
|
6
|
+
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies
|
10
|
+
# or substantial portions of the Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
13
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
14
|
+
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
15
|
+
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
17
|
+
# DEALINGS IN THE SOFTWARE.
|
18
|
+
|
19
|
+
require 'zip/zip'
|
20
|
+
|
21
|
+
##
|
22
|
+
# Class to parse an APK's resources.arsc data and retrieve resource
|
23
|
+
# data associated with a given R.id value
|
24
|
+
class ApkResources
|
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 defining the data inside of the package chunk
|
47
|
+
#
|
48
|
+
# PackageHeader = Struct.new(:header, :id, :name, :type_strings, :key_strings)
|
49
|
+
#
|
50
|
+
# * +header+ = ChunkHeader
|
51
|
+
# * +id+ = Package id; usually 0x7F for application resources
|
52
|
+
# * +name+ = Package name (e.g. "com.example.application")
|
53
|
+
# * +type_strings+ = Array of the type string values present (e.g. "drawable")
|
54
|
+
# * +key_strings+ = Array of the key string values present (e.g. "ic_launcher")
|
55
|
+
PackageHeader = Struct.new(:header, :id, :name, :type_strings, :key_strings)
|
56
|
+
|
57
|
+
##
|
58
|
+
# Structure defining the flags for a block of common resources
|
59
|
+
#
|
60
|
+
# ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
|
61
|
+
#
|
62
|
+
# * +header+ = ChunkHeader
|
63
|
+
# * +id+ = String value of the referenced type (e.g. "drawable")
|
64
|
+
# * +entry_count+ = Number of type entries in this chunk
|
65
|
+
# * +entries+ = Array of config flags for each type entry
|
66
|
+
# * +types+ = The ResType associated with this spec
|
67
|
+
ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
|
68
|
+
|
69
|
+
##
|
70
|
+
# Structure that houses all the resources for a given type
|
71
|
+
#
|
72
|
+
# ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
|
73
|
+
#
|
74
|
+
# * +header+ = ChunkHeader
|
75
|
+
# * +id+ = String value of the referenced type (e.g. "drawable")
|
76
|
+
# * +config+ = ResTypeConfig defining the configuration for this type
|
77
|
+
# * +entry_count+ = Number of entries in this chunk
|
78
|
+
# * +entries+ = Array of Hashes of [ResTypeConfig, ResTypeEntry] in this chunk
|
79
|
+
ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
|
80
|
+
|
81
|
+
##
|
82
|
+
# Structure that houses the configuration flags for a given resource.
|
83
|
+
#
|
84
|
+
# ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
|
85
|
+
#
|
86
|
+
# * +imsi+ = Flags marking country code and network code
|
87
|
+
# * +locale+ = Flags marking locale requirements (language)
|
88
|
+
# * +screen_type+ = Flags/values for screen density
|
89
|
+
# * +input+ = Flags marking input types and visibility status
|
90
|
+
# * +screen_size+ = Flags marking screen size and length
|
91
|
+
# * +version+ = Minimum API version
|
92
|
+
# * +screen_config+ = Flags marking screen configuration (like orientation)
|
93
|
+
# * +screen_size_dp+ = Flags marking smallest width constraints
|
94
|
+
#
|
95
|
+
# A default configuration is defined as ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
|
96
|
+
ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
|
97
|
+
|
98
|
+
##
|
99
|
+
# Structure that houses the data for a given resource entry
|
100
|
+
#
|
101
|
+
# ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
|
102
|
+
#
|
103
|
+
# * +flags+ = Flags marking if the resource is complex or public
|
104
|
+
# * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
|
105
|
+
# * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
|
106
|
+
# * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
|
107
|
+
#
|
108
|
+
# A single resource key can have multiple entries depending on configuration, so these structs
|
109
|
+
# are often returned in groups, keyed by a ResTypeConfig
|
110
|
+
ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
|
111
|
+
|
112
|
+
# PackageHeader containing information about all the type and key strings in the package
|
113
|
+
attr_reader :package_header
|
114
|
+
# StringPool containing all value strings in the package
|
115
|
+
attr_reader :stringpool_main
|
116
|
+
# StringPool containing all type strings in the package
|
117
|
+
attr_reader :stringpool_typestrings
|
118
|
+
# StringPool containing all key strings in the package
|
119
|
+
attr_reader :stringpool_keystrings
|
120
|
+
# Array of the ResTypeSpec chunks in the package
|
121
|
+
attr_reader :type_data
|
122
|
+
|
123
|
+
##
|
124
|
+
# Create a new ApkResources instance from the specified +apk_file+
|
125
|
+
#
|
126
|
+
# This opens and parses the contents of the APK's resources.arsc file.
|
127
|
+
|
128
|
+
def initialize(apk_file)
|
129
|
+
data = nil
|
130
|
+
# Get resources.arsc from the APK file
|
131
|
+
Zip::ZipFile.foreach(apk_file) do |f|
|
132
|
+
if f.name.match(/resources.arsc/)
|
133
|
+
data = f.get_input_stream.read
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Parse the Table Chunk
|
138
|
+
## Header
|
139
|
+
header_type = read_short(data, HEADER_START)
|
140
|
+
header_size = read_short(data, HEADER_START+2)
|
141
|
+
header_chunk_size = read_word(data, HEADER_START+4)
|
142
|
+
header_package_count = read_word(data, HEADER_START+8)
|
143
|
+
puts "Resource Package Count = #{header_package_count}" if DEBUG
|
144
|
+
if header_package_count > 1
|
145
|
+
puts "ApkResources only supports single package resources."
|
146
|
+
exit(1)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Parse the StringPool Chunk
|
150
|
+
## Header
|
151
|
+
startoffset_pool = HEADER_START + header_size
|
152
|
+
puts "Parse Main StringPool Chunk" if DEBUG
|
153
|
+
@stringpool_main = parse_stringpool(data, startoffset_pool)
|
154
|
+
puts "#{@stringpool_main.values.length} strings found" if DEBUG
|
155
|
+
|
156
|
+
# Parse the Package Chunk
|
157
|
+
## Header
|
158
|
+
startoffset_package = startoffset_pool + @stringpool_main.header.chunk_size
|
159
|
+
header = ChunkHeader.new( read_short(data, startoffset_package),
|
160
|
+
read_short(data, startoffset_package+2),
|
161
|
+
read_word(data, startoffset_package+4) )
|
162
|
+
|
163
|
+
package_id = read_word(data, startoffset_package+8)
|
164
|
+
package_name = read_string(data, startoffset_package+12, 256, "UTF-8")
|
165
|
+
package_type_strings = read_word(data, startoffset_package+268)
|
166
|
+
package_last_type = read_word(data, startoffset_package+272)
|
167
|
+
package_key_strings = read_word(data, startoffset_package+276)
|
168
|
+
package_last_key = read_word(data, startoffset_package+280)
|
169
|
+
|
170
|
+
@package_header = PackageHeader.new(header, package_id, package_name, package_type_strings, package_key_strings)
|
171
|
+
|
172
|
+
## typeStrings StringPool
|
173
|
+
startoffset_typestrings = startoffset_package + package_type_strings
|
174
|
+
puts "Parse typeStrings StringPool Chunk" if DEBUG
|
175
|
+
@stringpool_typestrings = parse_stringpool(data, startoffset_typestrings)
|
176
|
+
|
177
|
+
## keyStrings StringPool
|
178
|
+
startoffset_keystrings = startoffset_package + package_key_strings
|
179
|
+
puts "Parse keyStrings StringPool Chunk" if DEBUG
|
180
|
+
@stringpool_keystrings = parse_stringpool(data, startoffset_keystrings)
|
181
|
+
|
182
|
+
## typeSpec/type Chunks
|
183
|
+
@type_data = Array.new()
|
184
|
+
current_spec = nil
|
185
|
+
|
186
|
+
current = startoffset_keystrings + @stringpool_keystrings.header.chunk_size
|
187
|
+
puts "Parse Type/TypeSpec Chunks" if DEBUG
|
188
|
+
while current < data.length
|
189
|
+
## Parse Header
|
190
|
+
header = ChunkHeader.new( read_short(data, current),
|
191
|
+
read_short(data, current+2),
|
192
|
+
read_word(data, current+4) )
|
193
|
+
## Check Type
|
194
|
+
if header.type == CHUNKTYPE_TYPESPEC
|
195
|
+
typespec_id = read_byte(data, current+8)
|
196
|
+
typespec_entrycount = read_word(data, current+12)
|
197
|
+
|
198
|
+
## Parse the config flags for each entry
|
199
|
+
typespec_entries = Array.new()
|
200
|
+
i=0
|
201
|
+
while i < typespec_entrycount
|
202
|
+
offset = i * 4 + (current+16)
|
203
|
+
typespec_entries << read_word(data, offset)
|
204
|
+
|
205
|
+
i += 1
|
206
|
+
end
|
207
|
+
|
208
|
+
typespec_name = @stringpool_typestrings.values[typespec_id - 1]
|
209
|
+
current_spec = ResTypeSpec.new(header, typespec_name, typespec_entrycount, typespec_entries, nil)
|
210
|
+
|
211
|
+
@type_data << current_spec
|
212
|
+
current += header.chunk_size
|
213
|
+
elsif header.type == CHUNKTYPE_TYPE
|
214
|
+
type_id = read_byte(data, current+8)
|
215
|
+
type_entrycount = read_word(data, current+12)
|
216
|
+
type_entryoffset = read_word(data, current+16)
|
217
|
+
|
218
|
+
## The config flags set for this type chunk
|
219
|
+
## TODO: Vary the size of the config structure based on size to accomodate for new flags
|
220
|
+
config_size = read_word(data, current+20) # Number of bytes in structure
|
221
|
+
type_config = ResTypeConfig.new( read_word(data, current+24),
|
222
|
+
read_word(data, current+28),
|
223
|
+
read_word(data, current+32),
|
224
|
+
read_word(data, current+36 ),
|
225
|
+
read_word(data, current+40),
|
226
|
+
read_word(data, current+44),
|
227
|
+
read_word(data, current+48),
|
228
|
+
read_word(data, current+52) )
|
229
|
+
|
230
|
+
## The remainder of the chunk is a list of the entry values for that type/configuration
|
231
|
+
type_name = @stringpool_typestrings.values[type_id - 1]
|
232
|
+
if current_spec.types == nil
|
233
|
+
current_spec.types = ResType.new(header, type_name, type_config, type_entrycount, Array.new())
|
234
|
+
end
|
235
|
+
|
236
|
+
i=0
|
237
|
+
while i < type_entrycount
|
238
|
+
## Ensure a hash exists for each type
|
239
|
+
if current_spec.types.entries[i] == nil
|
240
|
+
current_spec.types.entries[i] = Hash.new()
|
241
|
+
end
|
242
|
+
current_entry = current_spec.types.entries[i]
|
243
|
+
|
244
|
+
## Get the start of the type from the offsets table
|
245
|
+
index_offset = i * 4 + (current+56)
|
246
|
+
start_offset = read_word(data, index_offset)
|
247
|
+
if start_offset != OFFSET_NO_ENTRY
|
248
|
+
## Set the index_offset to the start of the current entry
|
249
|
+
index_offset = current + type_entryoffset + start_offset
|
250
|
+
|
251
|
+
entry_flags = read_short(data, index_offset+2)
|
252
|
+
entry_key = read_word(data, index_offset+4)
|
253
|
+
entry_data_type = read_byte(data, index_offset+11)
|
254
|
+
entry_data = read_word(data, index_offset+12)
|
255
|
+
|
256
|
+
# Find the key in our strings index
|
257
|
+
key_name = @stringpool_keystrings.values[entry_key]
|
258
|
+
# Parse the value into a string
|
259
|
+
data_value = nil
|
260
|
+
case type_name
|
261
|
+
when TYPE_STRING, TYPE_DRAWABLE
|
262
|
+
data_value = get_resource_string(entry_data_type, entry_data)
|
263
|
+
when TYPE_COLOR
|
264
|
+
data_value = get_resource_color(entry_data_type, entry_data)
|
265
|
+
when TYPE_DIMENSION
|
266
|
+
data_value = get_resource_dimension(entry_data_type, entry_data)
|
267
|
+
when TYPE_INTEGER
|
268
|
+
data_value = get_resource_integer(entry_data_type, entry_data)
|
269
|
+
when TYPE_BOOLEAN
|
270
|
+
data_value = get_resource_bool(entry_data_type, entry_data)
|
271
|
+
else
|
272
|
+
puts "Complex Resources not yet supported." if DEBUG
|
273
|
+
data_value = entry_data.to_s
|
274
|
+
end
|
275
|
+
current_entry[type_config] = ResTypeEntry.new(entry_flags, key_name, entry_data_type, data_value)
|
276
|
+
end
|
277
|
+
i += 1
|
278
|
+
end
|
279
|
+
|
280
|
+
current += header.chunk_size
|
281
|
+
else
|
282
|
+
puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
|
283
|
+
## End Immediately
|
284
|
+
current = data.length
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
end #initalize
|
289
|
+
|
290
|
+
##
|
291
|
+
# Return array of all string values in the file
|
292
|
+
|
293
|
+
def get_all_strings
|
294
|
+
return @stringpool_main.values
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Return array of all the type values in the file
|
299
|
+
|
300
|
+
def get_all_types
|
301
|
+
return @stringpool_typestrings.values
|
302
|
+
end
|
303
|
+
|
304
|
+
##
|
305
|
+
# Return array of all the key values in the file
|
306
|
+
|
307
|
+
def get_all_keys
|
308
|
+
return @stringpool_keystrings.values
|
309
|
+
end
|
310
|
+
|
311
|
+
##
|
312
|
+
# Obtain the key value for a given resource id
|
313
|
+
#
|
314
|
+
# res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
|
315
|
+
# xml_format: Optionally format return string for XML files.
|
316
|
+
#
|
317
|
+
# If xml_format is true, return value will be @<type>/<key>
|
318
|
+
# If xml_format is false or missing, return value will be R.<type>.<key>
|
319
|
+
|
320
|
+
def get_resource_key(res_id, xml_format=false)
|
321
|
+
if res_id.is_a? String
|
322
|
+
res_id = res_id.hex
|
323
|
+
end
|
324
|
+
|
325
|
+
# R.id integers are a concatenation of package_id, type_id, and entry index
|
326
|
+
res_package = (res_id >> 24) & 0xFF
|
327
|
+
res_type = (res_id >> 16) & 0xFF
|
328
|
+
res_index = res_id & 0xFFFF
|
329
|
+
|
330
|
+
if res_package != @package_header.id
|
331
|
+
# This is not a resource we can parse
|
332
|
+
return res_id_to_s(res_id)
|
333
|
+
end
|
334
|
+
|
335
|
+
res_spec = @type_data[res_type-1]
|
336
|
+
entry = res_spec.types.entries[res_index]
|
337
|
+
|
338
|
+
if entry == nil
|
339
|
+
# There is no entry in our table for this resource
|
340
|
+
return res_id_to_s(res_id)
|
341
|
+
end
|
342
|
+
|
343
|
+
if xml_format
|
344
|
+
return "@#{res_spec.id}/#{entry.values[0].key}"
|
345
|
+
else
|
346
|
+
return "R.#{res_spec.id}.#{entry.values[0].key}"
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
##
|
351
|
+
# Obtain the default value for a given resource id
|
352
|
+
#
|
353
|
+
# res_id: ID values of a resources as a FixNum or String representation (i.e. 0x7F060001)
|
354
|
+
#
|
355
|
+
# Returns: The default ResTypeEntry to the given id, or nil if no default exists
|
356
|
+
|
357
|
+
def get_default_resource_value(res_id)
|
358
|
+
entries = get_resource_value(res_id)
|
359
|
+
default = ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
|
360
|
+
|
361
|
+
return entries[default]
|
362
|
+
end
|
363
|
+
|
364
|
+
##
|
365
|
+
# Obtain the value(s) for a given resource id.
|
366
|
+
# A default resource is one defined in an unqualified directory.
|
367
|
+
#
|
368
|
+
# res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
|
369
|
+
#
|
370
|
+
# Returns: Hash of all entries matching this id, keyed by their matching ResTypeConfig
|
371
|
+
|
372
|
+
def get_resource_value(res_id)
|
373
|
+
if res_id.is_a? String
|
374
|
+
res_id = res_id.hex
|
375
|
+
end
|
376
|
+
|
377
|
+
# R.id integers are a concatenation of package_id, type_id, and entry index
|
378
|
+
res_package = (res_id >> 24) & 0xFF
|
379
|
+
res_type = (res_id >> 16) & 0xFF
|
380
|
+
res_index = res_id & 0xFFFF
|
381
|
+
|
382
|
+
if res_package != @package_header.id
|
383
|
+
# This is not a resource we can parse
|
384
|
+
return res_id_to_s(res_id)
|
385
|
+
end
|
386
|
+
|
387
|
+
res_spec = @type_data[res_type-1]
|
388
|
+
|
389
|
+
entries = res_spec.types.entries[res_index]
|
390
|
+
if entries == nil
|
391
|
+
puts "Could not find #{type_name} ResType chunk" if DEBUG
|
392
|
+
return res_id_to_s(res_id)
|
393
|
+
end
|
394
|
+
|
395
|
+
return entries
|
396
|
+
end
|
397
|
+
|
398
|
+
private # Private Helper Methods
|
399
|
+
|
400
|
+
# Type Constants
|
401
|
+
TYPE_ARRAY = "array" # :nodoc:
|
402
|
+
TYPE_ATTRIBUTE = "attr" # :nodoc:
|
403
|
+
TYPE_BOOLEAN = "bool" # :nodoc:
|
404
|
+
TYPE_COLOR = "color" # :nodoc:
|
405
|
+
TYPE_DIMENSION = "dimen" # :nodoc:
|
406
|
+
TYPE_DRAWABLE = "drawable" # :nodoc:
|
407
|
+
TYPE_FRACTION = "fraction" # :nodoc:
|
408
|
+
TYPE_INTEGER = "integer" # :nodoc:
|
409
|
+
TYPE_LAYOUT = "layout" # :nodoc:
|
410
|
+
TYPE_PLURALS = "plurals" # :nodoc:
|
411
|
+
TYPE_STRING = "string" # :nodoc:
|
412
|
+
TYPE_STYLE = "style" # :nodoc:
|
413
|
+
|
414
|
+
# Data Type Constants
|
415
|
+
TYPE_INT_DEC = 0x10 # :nodoc:
|
416
|
+
TYPE_INT_HEX = 0x11 # :nodoc:
|
417
|
+
TYPE_BOOL = 0x12 # :nodoc:
|
418
|
+
TYPE_INT_COLOR_RGB4 = 0x1F # :nodoc:
|
419
|
+
TYPE_INT_COLOR_ARGB4 = 0x1E # :nodoc:
|
420
|
+
TYPE_INT_COLOR_RGB8 = 0x1D # :nodoc:
|
421
|
+
TYPE_INT_COLOR_ARGB8 = 0x1C # :nodoc:
|
422
|
+
COMPLEX_UNIT_PX = 0x0 # :nodoc:
|
423
|
+
COMPLEX_UNIT_DIP = 0x1 # :nodoc:
|
424
|
+
COMPLEX_UNIT_SP = 0x2 # :nodoc:
|
425
|
+
COMPLEX_UNIT_PT = 0x3 # :nodoc:
|
426
|
+
COMPLEX_UNIT_IN = 0x4 # :nodoc:
|
427
|
+
COMPLEX_UNIT_MM = 0x5 # :nodoc:
|
428
|
+
|
429
|
+
# Data Constants
|
430
|
+
TYPE_BOOL_TRUE = 0xFFFFFFFF # :nodoc:
|
431
|
+
TYPE_BOOL_FALSE = 0x00000000 # :nodoc:
|
432
|
+
|
433
|
+
# Header Constants
|
434
|
+
CHUNKTYPE_TYPESPEC = 0x202 # :nodoc:
|
435
|
+
CHUNKTYPE_TYPE = 0x201 # :nodoc:
|
436
|
+
|
437
|
+
#Flag Constants
|
438
|
+
FLAG_UTF8 = 0x100 # :nodoc:
|
439
|
+
|
440
|
+
OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
|
441
|
+
HEADER_START = 0 # :nodoc:
|
442
|
+
|
443
|
+
# Read a 32-bit word from a specific location in the data
|
444
|
+
def read_word(data, offset)
|
445
|
+
out = data[offset,4].unpack('V').first rescue 0
|
446
|
+
return out
|
447
|
+
end
|
448
|
+
|
449
|
+
# Read a 16-bit short from a specific location in the data
|
450
|
+
def read_short(data, offset)
|
451
|
+
out = data[offset,2].unpack('v').first rescue 0
|
452
|
+
return out
|
453
|
+
end
|
454
|
+
|
455
|
+
# Read a 8-bit byte from a specific location in the data
|
456
|
+
def read_byte(data, offset)
|
457
|
+
out = data[offset,1].unpack('C').first rescue 0
|
458
|
+
return out
|
459
|
+
end
|
460
|
+
|
461
|
+
# Read in length bytes in as a String
|
462
|
+
def read_string(data, offset, length, encoding)
|
463
|
+
if "UTF-16".casecmp(encoding) == 0
|
464
|
+
out = data[offset, length].unpack('v*').pack('U*')
|
465
|
+
else
|
466
|
+
out = data[offset, length].unpack('C*').pack('U*')
|
467
|
+
end
|
468
|
+
return out
|
469
|
+
end
|
470
|
+
|
471
|
+
# Return id as a hex string
|
472
|
+
def res_id_to_s(res_id)
|
473
|
+
return "0x#{res_id.to_s(16)}"
|
474
|
+
end
|
475
|
+
|
476
|
+
# Parse out a StringPool chunk
|
477
|
+
def parse_stringpool(data, offset)
|
478
|
+
pool_header = ChunkHeader.new( read_short(data, offset),
|
479
|
+
read_short(data, offset+2),
|
480
|
+
read_word(data, offset+4) )
|
481
|
+
|
482
|
+
pool_string_count = read_word(data, offset+8)
|
483
|
+
pool_style_count = read_word(data, offset+12)
|
484
|
+
pool_flags = read_word(data, offset+16)
|
485
|
+
format_utf8 = (pool_flags & FLAG_UTF8) != 0
|
486
|
+
puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
|
487
|
+
|
488
|
+
pool_string_offset = read_word(data, offset+20)
|
489
|
+
pool_style_offset = read_word(data, offset+24)
|
490
|
+
|
491
|
+
values = Array.new()
|
492
|
+
i = 0
|
493
|
+
while i < pool_string_count
|
494
|
+
# Read the string value
|
495
|
+
index = i * 4 + (offset+28)
|
496
|
+
offset_addr = pool_string_offset + offset + read_word(data, index)
|
497
|
+
if format_utf8
|
498
|
+
length = read_byte(data, offset_addr)
|
499
|
+
if (length & 0x80) != 0
|
500
|
+
length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
|
501
|
+
end
|
502
|
+
|
503
|
+
values << read_string(data, offset_addr + 2, length, "UTF-8")
|
504
|
+
else
|
505
|
+
length = read_short(data, offset_addr)
|
506
|
+
if (length & 0x8000) != 0
|
507
|
+
#There is one more length value before the data
|
508
|
+
length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
|
509
|
+
values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
|
510
|
+
else
|
511
|
+
# Read the data
|
512
|
+
values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
i += 1
|
517
|
+
end
|
518
|
+
|
519
|
+
return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
|
520
|
+
end
|
521
|
+
|
522
|
+
# Obtain string value for resource id
|
523
|
+
def get_resource_string(entry_datatype, entry_data)
|
524
|
+
result = @stringpool_main.values[entry_data]
|
525
|
+
return result
|
526
|
+
end
|
527
|
+
|
528
|
+
# Obtain boolean value for resource id
|
529
|
+
def get_resource_bool(entry_datatype, entry_data)
|
530
|
+
if entry_data == TYPE_BOOL_TRUE
|
531
|
+
return "true"
|
532
|
+
elsif entry_data == TYPE_BOOL_FALSE
|
533
|
+
return "false"
|
534
|
+
else
|
535
|
+
return "undefined"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Obtain integer value for resource id
|
540
|
+
def get_resource_integer(entry_datatype, entry_data)
|
541
|
+
if entry_datatype == TYPE_INT_HEX
|
542
|
+
return "0x#{entry_data.to_s(16)}"
|
543
|
+
else
|
544
|
+
return entry_data.to_s
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
# Obtain color value for resource id
|
549
|
+
def get_resource_color(entry_datatype, entry_data)
|
550
|
+
case entry_datatype
|
551
|
+
when TYPE_INT_COLOR_RGB4
|
552
|
+
return "#" + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
|
553
|
+
when TYPE_INT_COLOR_ARGB4
|
554
|
+
return "#" + ((entry_data >> 24) & 0xF).to_s(16) + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
|
555
|
+
when TYPE_INT_COLOR_RGB8
|
556
|
+
return "#" + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
|
557
|
+
when TYPE_INT_COLOR_ARGB8
|
558
|
+
return "#" + ((entry_data >> 24) & 0xFF).to_s(16) + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
|
559
|
+
else
|
560
|
+
return "0x#{entry_data.to_s(16)}"
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
# Obtain dimension value for resource id
|
565
|
+
def get_resource_dimension(entry_datatype, entry_data)
|
566
|
+
unit_type = (entry_data & 0xFF)
|
567
|
+
case unit_type
|
568
|
+
when COMPLEX_UNIT_PX
|
569
|
+
unit_name = "px"
|
570
|
+
when COMPLEX_UNIT_DIP
|
571
|
+
unit_name = "dp"
|
572
|
+
when COMPLEX_UNIT_SP
|
573
|
+
unit_name = "sp"
|
574
|
+
when COMPLEX_UNIT_PT
|
575
|
+
unit_name = "pt"
|
576
|
+
when COMPLEX_UNIT_IN
|
577
|
+
unit_name = "in"
|
578
|
+
when COMPLEX_UNIT_MM
|
579
|
+
unit_name = "mm"
|
580
|
+
else
|
581
|
+
unit_name = ""
|
582
|
+
end
|
583
|
+
|
584
|
+
return ((entry_data >> 8) & 0xFFFFFF).to_s + unit_name
|
585
|
+
end
|
586
|
+
end
|
@@ -0,0 +1,424 @@
|
|
1
|
+
# Copyright (C) 2012 Dave Smith
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
# software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
# without restriction, including without limitation the rights to use, copy, modify,
|
6
|
+
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies
|
10
|
+
# or substantial portions of the Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
13
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
14
|
+
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
15
|
+
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
17
|
+
# DEALINGS IN THE SOFTWARE.
|
18
|
+
|
19
|
+
require 'zip/zip'
|
20
|
+
require 'apktools/apkresources'
|
21
|
+
|
22
|
+
##
|
23
|
+
# Class to parse an APK's binary XML format back into textual XML
|
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
|
+
# * +raw+ = Original raw string value of the attribute, if one exists
|
88
|
+
# * +value+ = ResTypeEntry for the typed value of the attribute, if one exists
|
89
|
+
XmlAttribute = Struct.new(:namespace, :name, :raw, :value)
|
90
|
+
|
91
|
+
##
|
92
|
+
# Structure that houses the data for a given resource entry
|
93
|
+
#
|
94
|
+
# ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
|
95
|
+
#
|
96
|
+
# * +flags+ = Flags marking if the resource is complex or public
|
97
|
+
# * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
|
98
|
+
# * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
|
99
|
+
# * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
|
100
|
+
#
|
101
|
+
# A single resource key can have multiple entries depending on configuration, so these structs
|
102
|
+
# are often returned in groups, keyed by a ResTypeConfig
|
103
|
+
ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
|
104
|
+
|
105
|
+
# APK file where parser will search for XML
|
106
|
+
attr_reader :current_apk
|
107
|
+
# ApkResources instance used to resolve resources in this APK
|
108
|
+
attr_reader :apk_resources
|
109
|
+
|
110
|
+
##
|
111
|
+
# Create a new ApkXml instance from the specified +apk_file+
|
112
|
+
#
|
113
|
+
# This opens and parses the contents of the APK's resources.arsc file.
|
114
|
+
def initialize(apk_file)
|
115
|
+
@current_apk = apk_file
|
116
|
+
@apk_resources = ApkResources.new(apk_file)
|
117
|
+
end #initialize
|
118
|
+
|
119
|
+
##
|
120
|
+
# Read the requested XML file from inside the APK and parse out into
|
121
|
+
# readable textual XML. Returns a string of the parsed XML.
|
122
|
+
#
|
123
|
+
# xml_file: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
|
124
|
+
# pretty: Optionally format the XML output as human readable
|
125
|
+
# resolve_resources: Optionally, where possible, resolve resource references to their default value
|
126
|
+
#
|
127
|
+
# This opens and parses the contents of the APK's resources.arsc file.
|
128
|
+
def parse_xml(xml_file, pretty = false, resolve_resources = false)
|
129
|
+
xml_output = ""
|
130
|
+
indent = 0
|
131
|
+
data = nil
|
132
|
+
|
133
|
+
# Get the XML from the APK file
|
134
|
+
Zip::ZipFile.foreach(@current_apk) do |f|
|
135
|
+
if f.name.match(xml_file)
|
136
|
+
data = f.get_input_stream.read
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Parse the Header Chunk
|
141
|
+
header = ChunkHeader.new( read_short(data, HEADER_START),
|
142
|
+
read_short(data, HEADER_START+2),
|
143
|
+
read_word(data, HEADER_START+4) )
|
144
|
+
|
145
|
+
# Parse the StringPool Chunk
|
146
|
+
startoffset_pool = HEADER_START + header.size
|
147
|
+
puts "Parse Main StringPool Chunk" if DEBUG
|
148
|
+
stringpool_main = parse_stringpool(data, startoffset_pool)
|
149
|
+
puts "#{stringpool_main.values.length} strings found" if DEBUG
|
150
|
+
|
151
|
+
# Parse the remainder of the file chunks based on type
|
152
|
+
namespaces = Hash.new()
|
153
|
+
current = startoffset_pool + stringpool_main.header.chunk_size
|
154
|
+
puts "Parse Remaining Chunks" if DEBUG
|
155
|
+
while current < data.length
|
156
|
+
## Parse Header
|
157
|
+
header = ChunkHeader.new( read_short(data, current),
|
158
|
+
read_short(data, current+2),
|
159
|
+
read_word(data, current+4) )
|
160
|
+
## Check Type
|
161
|
+
if header.type == TYPE_XML_RESOURCEMAP
|
162
|
+
## Maps resource ids to strings in the pool
|
163
|
+
map_ids = Array.new()
|
164
|
+
map_strings = Array.new()
|
165
|
+
|
166
|
+
index_offset = current + header.size
|
167
|
+
i = 0
|
168
|
+
while index_offset < (current + header.chunk_size)
|
169
|
+
map_ids << read_word(data, index_offset)
|
170
|
+
map_strings << stringpool_main.values[i]
|
171
|
+
|
172
|
+
i += 1
|
173
|
+
index_offset = i * 4 + (current + header.size)
|
174
|
+
end
|
175
|
+
|
176
|
+
current += header.chunk_size
|
177
|
+
elsif header.type == TYPE_XML_STARTNAMESPACE
|
178
|
+
tree_header = parse_tree_header(header, data, current)
|
179
|
+
body_start = current+header.size
|
180
|
+
prefix = stringpool_main.values[read_word(data, body_start)]
|
181
|
+
uri = stringpool_main.values[read_word(data, body_start+4)]
|
182
|
+
namespaces[uri] = prefix
|
183
|
+
puts "NAMESPACE_START: xmlns:#{prefix} = '#{uri}'" if DEBUG
|
184
|
+
current += header.chunk_size
|
185
|
+
elsif header.type == TYPE_XML_ENDNAMESPACE
|
186
|
+
tree_header = parse_tree_header(header, data, current)
|
187
|
+
body_start = current+header.size
|
188
|
+
prefix = stringpool_main.values[read_word(data, body_start)]
|
189
|
+
uri = stringpool_main.values[read_word(data, body_start+4)]
|
190
|
+
puts "NAMESPACE_END: xmlns:#{prefix} = '#{uri}'" if DEBUG
|
191
|
+
current += header.chunk_size
|
192
|
+
elsif header.type == TYPE_XML_STARTELEMENT
|
193
|
+
tree_header = parse_tree_header(header, data, current)
|
194
|
+
body_start = current+header.size
|
195
|
+
namespace = nil
|
196
|
+
if read_word(data, body_start) != OFFSET_NO_ENTRY
|
197
|
+
namespace = stringpool_main.values[read_word(data, body_start)]
|
198
|
+
end
|
199
|
+
name = stringpool_main.values[read_word(data, body_start+4)]
|
200
|
+
|
201
|
+
attribute_offset = read_short(data, body_start+8)
|
202
|
+
attribute_size = read_short(data, body_start+10)
|
203
|
+
attribute_count = read_short(data, body_start+12)
|
204
|
+
id_idx = read_short(data, body_start+14)
|
205
|
+
class_idx = read_short(data, body_start+16)
|
206
|
+
style_idx = read_short(data, body_start+18)
|
207
|
+
|
208
|
+
attributes = Array.new()
|
209
|
+
i=0
|
210
|
+
while i < attribute_count
|
211
|
+
index_offset = i * attribute_size + (body_start + attribute_offset)
|
212
|
+
attr_namespace = nil
|
213
|
+
if read_word(data, index_offset) != OFFSET_NO_ENTRY
|
214
|
+
attr_uri = stringpool_main.values[read_word(data, index_offset)]
|
215
|
+
attr_namespace = namespaces[attr_uri]
|
216
|
+
end
|
217
|
+
attr_name = stringpool_main.values[read_word(data, index_offset+4)]
|
218
|
+
attr_raw = nil
|
219
|
+
if read_word(data, index_offset+8) != OFFSET_NO_ENTRY
|
220
|
+
attr_raw = stringpool_main.values[read_word(data, index_offset+8)]
|
221
|
+
end
|
222
|
+
entry = ResTypeEntry.new(0, nil, read_byte(data, index_offset+15), read_word(data, index_offset+16))
|
223
|
+
|
224
|
+
attributes << XmlAttribute.new(attr_namespace, attr_name, attr_raw, entry)
|
225
|
+
i += 1
|
226
|
+
end
|
227
|
+
|
228
|
+
element = XmlElement.new(tree_header, namespace, name, id_idx, class_idx, style_idx, attributes, xml_output == "")
|
229
|
+
|
230
|
+
puts "ELEMENT_START: #{element.namespace} #{element.name}" if DEBUG
|
231
|
+
display_name = element.namespace == nil ? element.name : "#{element.namespace}:#{element.name}"
|
232
|
+
|
233
|
+
if pretty
|
234
|
+
xml_output += "\n" + (" " * indent)
|
235
|
+
indent += 1
|
236
|
+
end
|
237
|
+
xml_output += "<#{display_name} "
|
238
|
+
# Only print namespaces on the root element
|
239
|
+
if element.is_root
|
240
|
+
keys = namespaces.keys
|
241
|
+
keys.each do |key|
|
242
|
+
xml_output += "xmlns:#{namespaces[key]}=\"#{key}\" "
|
243
|
+
if pretty && key != keys.last
|
244
|
+
xml_output += "\n" + (" " * indent)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
element.attributes.each do |attr|
|
250
|
+
puts "---ATTRIBUTE: #{attr.namespace} #{attr.name} #{attr.raw} => #{attr.value.data_type} #{attr.value.data.to_s(16)}" if DEBUG
|
251
|
+
display_name = attr.namespace == nil ? attr.name : "#{attr.namespace}:#{attr.name}"
|
252
|
+
display_value = nil
|
253
|
+
if attr.raw != nil # Use raw value
|
254
|
+
display_value = attr.raw
|
255
|
+
elsif attr.value.data_type == 1
|
256
|
+
# Find the resource
|
257
|
+
default_res = apk_resources.get_default_resource_value(attr.value.data)
|
258
|
+
if resolve_resources && default_res != nil
|
259
|
+
display_value = default_res.data
|
260
|
+
else
|
261
|
+
display_value = apk_resources.get_resource_key(attr.value.data, true)
|
262
|
+
end
|
263
|
+
else # Value is a constant
|
264
|
+
display_value = "0x#{attr.value.data.to_s(16)}"
|
265
|
+
end
|
266
|
+
|
267
|
+
if pretty
|
268
|
+
xml_output += "\n" + (" " * indent)
|
269
|
+
end
|
270
|
+
xml_output += "#{display_name}=\"#{display_value}\" "
|
271
|
+
end
|
272
|
+
|
273
|
+
xml_output += ">"
|
274
|
+
|
275
|
+
current += header.chunk_size
|
276
|
+
elsif header.type == TYPE_XML_ENDELEMENT
|
277
|
+
tree_header = parse_tree_header(header, data, current)
|
278
|
+
body_start = current+header.size
|
279
|
+
namespace = nil
|
280
|
+
if read_word(data, body_start) != OFFSET_NO_ENTRY
|
281
|
+
namespace = stringpool_main.values[read_word(data, body_start)]
|
282
|
+
end
|
283
|
+
name = stringpool_main.values[read_word(data, body_start+4)]
|
284
|
+
|
285
|
+
puts "ELEMENT END: #{namespace} #{name}" if DEBUG
|
286
|
+
display_name = namespace == nil ? name : "#{namespace}:#{name}"
|
287
|
+
if pretty
|
288
|
+
indent -= 1
|
289
|
+
if indent < 0
|
290
|
+
indent = 0
|
291
|
+
end
|
292
|
+
xml_output += "\n" + (" " * indent)
|
293
|
+
end
|
294
|
+
xml_output += "</#{display_name}>"
|
295
|
+
|
296
|
+
|
297
|
+
current += header.chunk_size
|
298
|
+
elsif header.type == TYPE_XML_CDATA
|
299
|
+
tree_header = parse_tree_header(header, data, current)
|
300
|
+
body_start = current+header.size
|
301
|
+
|
302
|
+
cdata = stringpool_main.values[read_word(data, body_start)]
|
303
|
+
cdata_type = read_word(data, body_start+7)
|
304
|
+
cdata_value = read_word(data, body_start+8)
|
305
|
+
puts "CDATA: #{cdata} #{cdata_type} #{cdata_value}" if DEBUG
|
306
|
+
|
307
|
+
cdata.split(/\r?\n/).each do |item|
|
308
|
+
if pretty
|
309
|
+
xml_output += "\n" + (" " * indent)
|
310
|
+
end
|
311
|
+
xml_output += "<![CDATA[#{item.strip}]]>"
|
312
|
+
end
|
313
|
+
|
314
|
+
current += header.chunk_size
|
315
|
+
else
|
316
|
+
puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
|
317
|
+
## End Immediately
|
318
|
+
current = data.length
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
return xml_output
|
323
|
+
end #parse_xml
|
324
|
+
|
325
|
+
private # Private Helper Methods
|
326
|
+
|
327
|
+
#Flag Constants
|
328
|
+
FLAG_UTF8 = 0x100 # :nodoc:
|
329
|
+
|
330
|
+
OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
|
331
|
+
HEADER_START = 0 # :nodoc:
|
332
|
+
|
333
|
+
TYPE_XML_RESOURCEMAP = 0x180 # :nodoc:
|
334
|
+
TYPE_XML_STARTNAMESPACE = 0x100 # :nodoc:
|
335
|
+
TYPE_XML_ENDNAMESPACE = 0x101 # :nodoc:
|
336
|
+
TYPE_XML_STARTELEMENT = 0x102 # :nodoc:
|
337
|
+
TYPE_XML_ENDELEMENT = 0x103 # :nodoc:
|
338
|
+
TYPE_XML_CDATA = 0x104 # :nodoc:
|
339
|
+
|
340
|
+
# Read a 32-bit word from a specific location in the data
|
341
|
+
def read_word(data, offset)
|
342
|
+
out = data[offset,4].unpack('V').first rescue 0
|
343
|
+
return out
|
344
|
+
end
|
345
|
+
|
346
|
+
# Read a 16-bit short from a specific location in the data
|
347
|
+
def read_short(data, offset)
|
348
|
+
out = data[offset,2].unpack('v').first rescue 0
|
349
|
+
return out
|
350
|
+
end
|
351
|
+
|
352
|
+
# Read a 8-bit byte from a specific location in the data
|
353
|
+
def read_byte(data, offset)
|
354
|
+
out = data[offset,1].unpack('C').first rescue 0
|
355
|
+
return out
|
356
|
+
end
|
357
|
+
|
358
|
+
# Read in length bytes in as a String
|
359
|
+
def read_string(data, offset, length, encoding)
|
360
|
+
if "UTF-16".casecmp(encoding) == 0
|
361
|
+
out = data[offset, length].unpack('v*').pack('U*')
|
362
|
+
else
|
363
|
+
out = data[offset, length].unpack('C*').pack('U*')
|
364
|
+
end
|
365
|
+
return out
|
366
|
+
end
|
367
|
+
|
368
|
+
# Parse out an XmlTreeHeader
|
369
|
+
def parse_tree_header(chunk_header, data, offset)
|
370
|
+
line_num = read_word(data, offset+8)
|
371
|
+
comment = nil
|
372
|
+
if read_word(data, offset+12) != OFFSET_NO_ENTRY
|
373
|
+
comment = stringpool_main.values[read_word(data, offset+12)]
|
374
|
+
end
|
375
|
+
return XmlTreeHeader.new(chunk_header, line_num, comment)
|
376
|
+
end
|
377
|
+
|
378
|
+
# Parse out a StringPool chunk
|
379
|
+
def parse_stringpool(data, offset)
|
380
|
+
pool_header = ChunkHeader.new( read_short(data, offset),
|
381
|
+
read_short(data, offset+2),
|
382
|
+
read_word(data, offset+4) )
|
383
|
+
|
384
|
+
pool_string_count = read_word(data, offset+8)
|
385
|
+
pool_style_count = read_word(data, offset+12)
|
386
|
+
pool_flags = read_word(data, offset+16)
|
387
|
+
format_utf8 = (pool_flags & FLAG_UTF8) != 0
|
388
|
+
puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
|
389
|
+
|
390
|
+
pool_string_offset = read_word(data, offset+20)
|
391
|
+
pool_style_offset = read_word(data, offset+24)
|
392
|
+
|
393
|
+
values = Array.new()
|
394
|
+
i = 0
|
395
|
+
while i < pool_string_count
|
396
|
+
# Read the string value
|
397
|
+
index = i * 4 + (offset+28)
|
398
|
+
offset_addr = pool_string_offset + offset + read_word(data, index)
|
399
|
+
if format_utf8
|
400
|
+
length = read_byte(data, offset_addr)
|
401
|
+
if (length & 0x80) != 0
|
402
|
+
length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
|
403
|
+
end
|
404
|
+
|
405
|
+
values << read_string(data, offset_addr + 2, length, "UTF-8")
|
406
|
+
else
|
407
|
+
length = read_short(data, offset_addr)
|
408
|
+
if (length & 0x8000) != 0
|
409
|
+
#There is one more length value before the data
|
410
|
+
length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
|
411
|
+
values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
|
412
|
+
else
|
413
|
+
# Read the data
|
414
|
+
values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
i += 1
|
419
|
+
end
|
420
|
+
|
421
|
+
return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
|
422
|
+
end
|
423
|
+
|
424
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# Copyright (C) 2012 Dave Smith
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
4
|
+
# software and associated documentation files (the "Software"), to deal in the Software
|
5
|
+
# without restriction, including without limitation the rights to use, copy, modify,
|
6
|
+
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
7
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
#
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies
|
10
|
+
# or substantial portions of the Software.
|
11
|
+
#
|
12
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
13
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
14
|
+
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
15
|
+
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
17
|
+
# DEALINGS IN THE SOFTWARE.
|
18
|
+
|
19
|
+
##
|
20
|
+
# Constants for configuration flags
|
21
|
+
module ResConfiguration
|
22
|
+
|
23
|
+
ACONFIGURATION_ORIENTATION_ANY = 0x0000
|
24
|
+
ACONFIGURATION_ORIENTATION_PORT = 0x0001
|
25
|
+
ACONFIGURATION_ORIENTATION_LAND = 0x0002
|
26
|
+
ACONFIGURATION_ORIENTATION_SQUARE = 0x0003
|
27
|
+
|
28
|
+
ACONFIGURATION_TOUCHSCREEN_ANY = 0x0000
|
29
|
+
ACONFIGURATION_TOUCHSCREEN_NOTOUCH = 0x0001
|
30
|
+
ACONFIGURATION_TOUCHSCREEN_STYLUS = 0x0002
|
31
|
+
ACONFIGURATION_TOUCHSCREEN_FINGER = 0x0003
|
32
|
+
|
33
|
+
ACONFIGURATION_DENSITY_DEFAULT = 0
|
34
|
+
ACONFIGURATION_DENSITY_LOW = 120
|
35
|
+
ACONFIGURATION_DENSITY_MEDIUM = 160
|
36
|
+
ACONFIGURATION_DENSITY_TV = 213
|
37
|
+
ACONFIGURATION_DENSITY_HIGH = 240
|
38
|
+
ACONFIGURATION_DENSITY_XHIGH = 320
|
39
|
+
ACONFIGURATION_DENSITY_XXHIGH = 480
|
40
|
+
ACONFIGURATION_DENSITY_NONE = 0xffff
|
41
|
+
|
42
|
+
ACONFIGURATION_KEYBOARD_ANY = 0x0000
|
43
|
+
ACONFIGURATION_KEYBOARD_NOKEYS = 0x0001
|
44
|
+
ACONFIGURATION_KEYBOARD_QWERTY = 0x0002
|
45
|
+
ACONFIGURATION_KEYBOARD_12KEY = 0x0003
|
46
|
+
|
47
|
+
ACONFIGURATION_NAVIGATION_ANY = 0x0000
|
48
|
+
ACONFIGURATION_NAVIGATION_NONAV = 0x0001
|
49
|
+
ACONFIGURATION_NAVIGATION_DPAD = 0x0002
|
50
|
+
ACONFIGURATION_NAVIGATION_TRACKBALL = 0x0003
|
51
|
+
ACONFIGURATION_NAVIGATION_WHEEL = 0x0004
|
52
|
+
|
53
|
+
ACONFIGURATION_KEYSHIDDEN_ANY = 0x0000
|
54
|
+
ACONFIGURATION_KEYSHIDDEN_NO = 0x0001
|
55
|
+
ACONFIGURATION_KEYSHIDDEN_YES = 0x0002
|
56
|
+
ACONFIGURATION_KEYSHIDDEN_SOFT = 0x0003
|
57
|
+
|
58
|
+
ACONFIGURATION_NAVHIDDEN_ANY = 0x0000
|
59
|
+
ACONFIGURATION_NAVHIDDEN_NO = 0x0001
|
60
|
+
ACONFIGURATION_NAVHIDDEN_YES = 0x0002
|
61
|
+
|
62
|
+
ACONFIGURATION_SCREENSIZE_ANY = 0x00
|
63
|
+
ACONFIGURATION_SCREENSIZE_SMALL = 0x01
|
64
|
+
ACONFIGURATION_SCREENSIZE_NORMAL = 0x02
|
65
|
+
ACONFIGURATION_SCREENSIZE_LARGE = 0x03
|
66
|
+
ACONFIGURATION_SCREENSIZE_XLARGE = 0x04
|
67
|
+
|
68
|
+
ACONFIGURATION_SCREENLONG_ANY = 0x00
|
69
|
+
ACONFIGURATION_SCREENLONG_NO = 0x1
|
70
|
+
ACONFIGURATION_SCREENLONG_YES = 0x2
|
71
|
+
|
72
|
+
ACONFIGURATION_UI_MODE_TYPE_ANY = 0x00
|
73
|
+
ACONFIGURATION_UI_MODE_TYPE_NORMAL = 0x01
|
74
|
+
ACONFIGURATION_UI_MODE_TYPE_DESK = 0x02
|
75
|
+
ACONFIGURATION_UI_MODE_TYPE_CAR = 0x03
|
76
|
+
ACONFIGURATION_UI_MODE_TYPE_TELEVISION = 0x04
|
77
|
+
ACONFIGURATION_UI_MODE_TYPE_APPLIANCE = 0x05
|
78
|
+
|
79
|
+
ACONFIGURATION_UI_MODE_NIGHT_ANY = 0x00
|
80
|
+
ACONFIGURATION_UI_MODE_NIGHT_NO = 0x1
|
81
|
+
ACONFIGURATION_UI_MODE_NIGHT_YES = 0x2
|
82
|
+
|
83
|
+
ACONFIGURATION_SCREEN_WIDTH_DP_ANY = 0x0000
|
84
|
+
ACONFIGURATION_SCREEN_HEIGHT_DP_ANY = 0x0000
|
85
|
+
ACONFIGURATION_SMALLEST_SCREEN_WIDTH_DP_ANY = 0x0000
|
86
|
+
|
87
|
+
ACONFIGURATION_MCC = 0x0001
|
88
|
+
ACONFIGURATION_MNC = 0x0002
|
89
|
+
ACONFIGURATION_LOCALE = 0x0004
|
90
|
+
ACONFIGURATION_TOUCHSCREEN = 0x0008
|
91
|
+
ACONFIGURATION_KEYBOARD = 0x0010
|
92
|
+
ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020
|
93
|
+
ACONFIGURATION_NAVIGATION = 0x0040
|
94
|
+
ACONFIGURATION_ORIENTATION = 0x0080
|
95
|
+
ACONFIGURATION_DENSITY = 0x0100
|
96
|
+
ACONFIGURATION_SCREEN_SIZE = 0x0200
|
97
|
+
ACONFIGURATION_VERSION = 0x0400
|
98
|
+
ACONFIGURATION_SCREEN_LAYOUT = 0x0800
|
99
|
+
ACONFIGURATION_UI_MODE = 0x1000
|
100
|
+
ACONFIGURATION_SMALLEST_SCREEN_SIZE = 0x2000
|
101
|
+
|
102
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apktools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dave Smith
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rubyzip
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Library to assist reading resource data out of Android APKs
|
31
|
+
email: dave@xcellentcreations.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- lib/apktools/apkresources.rb
|
37
|
+
- lib/apktools/apkxml.rb
|
38
|
+
- lib/apktools/resconfiguration.rb
|
39
|
+
homepage: http://github.com/devunwired/apktools
|
40
|
+
licenses: []
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.8.22
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: APKTools
|
63
|
+
test_files: []
|