apktools 0.6.0 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '083dd99c7ce0853801d49f4eba60a55286494c024170c0a6653b15a3876efb74'
4
+ data.tar.gz: 21d19d9d422bbb521da8921e1467a7c1bf3317056a36ccd3718eff11ceb1a9bd
5
+ SHA512:
6
+ metadata.gz: 49b4283ad15199ea96d70b4c2eac209fd68ad88fb288e990e9ab3b86bbb68205e6de52ecefb14205638d775db0391e28ea792d667783e82ccafceb1b3f07a11d
7
+ data.tar.gz: cfdc0d50dcaf6e3510a60dbc359a6eb4d864d87b925607fd8542927dc043961329897b41299ae53766ac9d2c0a5966785cb67e79fda210af753784b0c8c0c341
@@ -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,629 +16,690 @@
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
 
21
21
  ##
22
22
  # Class to parse an APK's resources.arsc data and retrieve resource
23
23
  # data associated with a given R.id value
24
24
  class ApkResources
25
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 resource contents for a package chunk
59
- #
60
- # Package = Struct.new(:header, :stringpool_typestrings, :stringpool_keystrings, :type_data)
61
- #
62
- # * +package_header+ = PackageHeader
63
- # * +stringpool_typestrings+ = StringPool containing all type strings in the package
64
- # * +stringpool_keystrings+ = StringPool containing all key strings in the package
65
- # * +type_data+ = Array of ResTypeSpec chunks in the package
66
- Package = Struct.new(:package_header, :stringpool_typestrings, :stringpool_keystrings, :type_data)
67
-
68
- ##
69
- # Structure defining the flags for a block of common resources
70
- #
71
- # ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
72
- #
73
- # * +header+ = ChunkHeader
74
- # * +id+ = String value of the referenced type (e.g. "drawable")
75
- # * +entry_count+ = Number of type entries in this chunk
76
- # * +entries+ = Array of config flags for each type entry
77
- # * +types+ = The ResType associated with this spec
78
- ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
79
-
80
- ##
81
- # Structure that houses all the resources for a given type
82
- #
83
- # ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
84
- #
85
- # * +header+ = ChunkHeader
86
- # * +id+ = String value of the referenced type (e.g. "drawable")
87
- # * +config+ = ResTypeConfig defining the configuration for this type
88
- # * +entry_count+ = Number of entries in this chunk
89
- # * +entries+ = Array of Hashes of [ResTypeConfig, ResTypeEntry] in this chunk
90
- ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
91
-
92
- ##
93
- # Structure that houses the configuration flags for a given resource.
94
- #
95
- # ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
96
- #
97
- # * +imsi+ = Flags marking country code and network code
98
- # * +locale+ = Flags marking locale requirements (language)
99
- # * +screen_type+ = Flags/values for screen density
100
- # * +input+ = Flags marking input types and visibility status
101
- # * +screen_size+ = Flags marking screen size and length
102
- # * +version+ = Minimum API version
103
- # * +screen_config+ = Flags marking screen configuration (like orientation)
104
- # * +screen_size_dp+ = Flags marking smallest width constraints
105
- #
106
- # A default configuration is defined as ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
107
- ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
108
-
109
- ##
110
- # Structure that houses the data for a given resource entry
111
- #
112
- # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
113
- #
114
- # * +flags+ = Flags marking if the resource is complex or public
115
- # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
116
- # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
117
- # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
118
- #
119
- # A single resource key can have multiple entries depending on configuration, so these structs
120
- # are often returned in groups, keyed by a ResTypeConfig
121
- ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
122
-
123
- # PackageHeader containing information about all the type and key strings in the package
124
- attr_reader :package_header
125
- # StringPool containing all value strings in the package
126
- attr_reader :stringpool_main
127
- # Hash of Package chunks, keyed by package id
128
- attr_reader :packages
129
-
130
- ##
131
- # Create a new ApkResources instance from the specified +apk_file+
132
- #
133
- # This opens and parses the contents of the APK's resources.arsc file.
134
-
135
- def initialize(apk_file)
136
- data = nil
137
- # Get resources.arsc from the APK file
138
- Zip::ZipFile.foreach(apk_file) do |f|
139
- if f.name.match(/resources.arsc/)
140
- data = f.get_input_stream.read
141
- end
142
- end
143
-
144
- # Parse the Table Chunk
145
- ## Header
146
- header_type = read_short(data, HEADER_START)
147
- header_size = read_short(data, HEADER_START+2)
148
- header_chunk_size = read_word(data, HEADER_START+4)
149
- header_package_count = read_word(data, HEADER_START+8)
150
- puts "Resource Package Count = #{header_package_count}" if DEBUG
151
-
152
- # Parse the StringPool Chunk
153
- ## Header
154
- startoffset_pool = HEADER_START + header_size
155
- puts "Parse Main StringPool Chunk" if DEBUG
156
- @stringpool_main = parse_stringpool(data, startoffset_pool)
157
- puts "#{@stringpool_main.values.length} strings found" if DEBUG
158
-
159
- # Parse the Package Chunk
160
- ## Header
161
- startoffset_package = startoffset_pool + @stringpool_main.header.chunk_size
162
- @packages = Hash.new()
163
- i = 0
164
- while i < header_package_count
165
- package_element = parse_package(data, startoffset_package)
166
- puts "Package #{package_element.package_header.id}" if DEBUG
167
- startoffset_package = startoffset_package + package_element.package_header.header.chunk_size
168
- @packages[package_element.package_header.id] = package_element
169
-
170
- i += 1
171
- end
172
-
173
- end #initalize
174
-
175
- ##
176
- # Return array of all string values in the file
177
-
178
- def get_all_strings
179
- return @stringpool_main.values
180
- end
181
-
182
- ##
183
- # Return hash of all the type values in the file
184
- # keyed by package id
185
-
186
- def get_all_types
187
- types = Hash.new()
188
- @packages.each do |key, value|
189
- types[key] = value.stringpool_typestrings.values
190
- end
191
-
192
- return types
193
- end
194
-
195
- ##
196
- # Return hash of all the key values in the file
197
- # keyed by package id
198
-
199
- def get_all_keys
200
- keys = Hash.new()
201
- @packages.each do |key, value|
202
- keys[key] = value.stringpool_keystrings.values
203
- end
204
-
205
- return keys
206
- end
207
-
208
- ##
209
- # Obtain the key value for a given resource id
210
- #
211
- # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
212
- # xml_format: Optionally format return string for XML files.
213
- #
214
- # If xml_format is true, return value will be @<type>/<key>
215
- # If xml_format is false or missing, return value will be R.<type>.<key>
216
- # If the resource id does not exist, return value will be nil
217
-
218
- def get_resource_key(res_id, xml_format=false)
219
- if res_id.is_a? String
220
- res_id = res_id.hex
221
- end
222
-
223
- # R.id integers are a concatenation of package_id, type_id, and entry index
224
- res_package = (res_id >> 24) & 0xFF
225
- res_type = (res_id >> 16) & 0xFF
226
- res_index = res_id & 0xFFFF
227
-
228
- package_element = @packages[res_package]
229
- if package_element == nil
230
- # This is not a resource we can parse
231
- return nil
232
- end
233
-
234
- res_spec = package_element.type_data[res_type-1]
235
- if res_spec == nil
236
- puts "Could not find ResTypeSpec for #{res_package} #{res_type}" if DEBUG
237
- return nil
238
- end
239
-
240
- entry = res_spec.types.entries[res_index]
241
- if entry == nil
242
- # There is no entry in our table for this resource
243
- return nil
244
- end
245
-
246
- if xml_format
247
- return "@#{res_spec.id}/#{entry.values[0].key}"
248
- else
249
- return "R.#{res_spec.id}.#{entry.values[0].key}"
250
- end
251
- end
252
-
253
- ##
254
- # Obtain the default value for a given resource id
255
- #
256
- # res_id: ID values of a resources as a FixNum or String representation (i.e. 0x7F060001)
257
- #
258
- # Returns: The default ResTypeEntry to the given id, or nil if no default exists
259
-
260
- def get_default_resource_value(res_id)
261
- if res_id.is_a? String
262
- res_id = res_id.hex
263
- end
264
-
265
- entries = get_resource_value(res_id)
266
- if entries != nil
267
- default = ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
268
- default_entry = entries[default]
269
- return default_entry
270
- else
271
- return nil
272
- end
273
- end
274
-
275
- ##
276
- # Obtain the value(s) for a given resource id.
277
- # A default resource is one defined in an unqualified directory.
278
- #
279
- # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
280
- #
281
- # Returns: Hash of all entries matching this id, keyed by their matching ResTypeConfig
282
- # or nil if the resource id cannot be found.
283
-
284
- def get_resource_value(res_id)
285
- if res_id.is_a? String
286
- res_id = res_id.hex
287
- end
288
-
289
- # R.id integers are a concatenation of package_id, type_id, and entry index
290
- res_package = (res_id >> 24) & 0xFF
291
- res_type = (res_id >> 16) & 0xFF
292
- res_index = res_id & 0xFFFF
293
-
294
- package_element = @packages[res_package]
295
- if package_element == nil
296
- # This is not a resource we can parse
297
- return nil
298
- end
299
-
300
- res_spec = package_element.type_data[res_type-1]
301
- if res_spec == nil
302
- puts "Could not find ResTypeSpec for #{res_package} #{res_type}" if DEBUG
303
- return nil
304
- end
305
-
306
- entries = res_spec.types.entries[res_index]
307
- if entries == nil
308
- puts "Could not find #{res_spec.types.id} ResType chunk" if DEBUG
309
- return nil
310
- end
311
-
312
- return entries
313
- end
314
-
315
- private # Private Helper Methods
316
-
317
- # Type Constants
318
- TYPE_ARRAY = "array" # :nodoc:
319
- TYPE_ATTRIBUTE = "attr" # :nodoc:
320
- TYPE_BOOLEAN = "bool" # :nodoc:
321
- TYPE_COLOR = "color" # :nodoc:
322
- TYPE_DIMENSION = "dimen" # :nodoc:
323
- TYPE_DRAWABLE = "drawable" # :nodoc:
324
- TYPE_FRACTION = "fraction" # :nodoc:
325
- TYPE_INTEGER = "integer" # :nodoc:
326
- TYPE_LAYOUT = "layout" # :nodoc:
327
- TYPE_PLURALS = "plurals" # :nodoc:
328
- TYPE_STRING = "string" # :nodoc:
329
- TYPE_STYLE = "style" # :nodoc:
330
-
331
- # Data Type Constants
332
- TYPE_INT_DEC = 0x10 # :nodoc:
333
- TYPE_INT_HEX = 0x11 # :nodoc:
334
- TYPE_BOOL = 0x12 # :nodoc:
335
- TYPE_INT_COLOR_RGB4 = 0x1F # :nodoc:
336
- TYPE_INT_COLOR_ARGB4 = 0x1E # :nodoc:
337
- TYPE_INT_COLOR_RGB8 = 0x1D # :nodoc:
338
- TYPE_INT_COLOR_ARGB8 = 0x1C # :nodoc:
339
- COMPLEX_UNIT_PX = 0x0 # :nodoc:
340
- COMPLEX_UNIT_DIP = 0x1 # :nodoc:
341
- COMPLEX_UNIT_SP = 0x2 # :nodoc:
342
- COMPLEX_UNIT_PT = 0x3 # :nodoc:
343
- COMPLEX_UNIT_IN = 0x4 # :nodoc:
344
- COMPLEX_UNIT_MM = 0x5 # :nodoc:
345
-
346
- # Data Constants
347
- TYPE_BOOL_TRUE = 0xFFFFFFFF # :nodoc:
348
- TYPE_BOOL_FALSE = 0x00000000 # :nodoc:
349
-
350
- # Header Constants
351
- CHUNKTYPE_TYPESPEC = 0x202 # :nodoc:
352
- CHUNKTYPE_TYPE = 0x201 # :nodoc:
353
- CHUNKTYPE_PACKAGE = 0x200 # :nodoc:
354
-
355
- #Flag Constants
356
- FLAG_UTF8 = 0x100 # :nodoc:
357
- FLAG_COMPLEX = 0x0001 # :nodoc:
358
- FLAG_PUBLIC = 0x0002 # :nodoc:
359
-
360
- OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
361
- HEADER_START = 0 # :nodoc:
362
-
363
- # Read a 32-bit word from a specific location in the data
364
- def read_word(data, offset)
365
- out = data[offset,4].unpack('V').first rescue 0
366
- return out
367
- end
368
-
369
- # Read a 16-bit short from a specific location in the data
370
- def read_short(data, offset)
371
- out = data[offset,2].unpack('v').first rescue 0
372
- return out
373
- end
374
-
375
- # Read a 8-bit byte from a specific location in the data
376
- def read_byte(data, offset)
377
- out = data[offset,1].unpack('C').first rescue 0
378
- return out
379
- end
380
-
381
- # Read in length bytes in as a String
382
- def read_string(data, offset, length, encoding)
383
- if "UTF-16".casecmp(encoding) == 0
384
- out = data[offset, length].unpack('v*').pack('U*')
385
- else
386
- out = data[offset, length].unpack('C*').pack('U*')
387
- end
388
- return out
389
- end
390
-
391
- # Return id as a hex string
392
- def res_id_to_s(res_id)
393
- return "0x#{res_id.to_s(16)}"
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
- # Parse out a Package Chunk
443
- def parse_package(data, offset)
444
- header = ChunkHeader.new( read_short(data, offset),
445
- read_short(data, offset+2),
446
- read_word(data, offset+4) )
447
-
448
- package_id = read_word(data, offset+8)
449
- package_name = read_string(data, offset+12, 256, "UTF-8")
450
- package_type_strings = read_word(data, offset+268)
451
- package_last_type = read_word(data, offset+272)
452
- package_key_strings = read_word(data, offset+276)
453
- package_last_key = read_word(data, offset+280)
454
-
455
- package_header = PackageHeader.new(header, package_id, package_name, package_type_strings, package_key_strings)
456
-
457
- ## typeStrings StringPool
458
- startoffset_typestrings = offset + package_type_strings
459
- puts "Parse typeStrings StringPool Chunk" if DEBUG
460
- stringpool_typestrings = parse_stringpool(data, startoffset_typestrings)
461
-
462
- ## keyStrings StringPool
463
- startoffset_keystrings = offset + package_key_strings
464
- puts "Parse keyStrings StringPool Chunk" if DEBUG
465
- stringpool_keystrings = parse_stringpool(data, startoffset_keystrings)
466
-
467
- ## typeSpec/type Chunks
468
- type_data = Array.new()
469
- current_spec = nil
470
-
471
- current = startoffset_keystrings + stringpool_keystrings.header.chunk_size
472
- puts "Parse Type/TypeSpec Chunks" if DEBUG
473
- while current < data.length
474
- ## Parse Header
475
- header = ChunkHeader.new( read_short(data, current),
476
- read_short(data, current+2),
477
- read_word(data, current+4) )
478
- ## Check Type
479
- if header.type == CHUNKTYPE_TYPESPEC
480
- typespec_id = read_byte(data, current+8)
481
- typespec_entrycount = read_word(data, current+12)
482
-
483
- ## Parse the config flags for each entry
484
- typespec_entries = Array.new()
485
- i=0
486
- while i < typespec_entrycount
487
- offset = i * 4 + (current+16)
488
- typespec_entries << read_word(data, offset)
489
-
490
- i += 1
491
- end
492
-
493
- typespec_name = stringpool_typestrings.values[typespec_id - 1]
494
- current_spec = ResTypeSpec.new(header, typespec_name, typespec_entrycount, typespec_entries, nil)
495
-
496
- type_data << current_spec
497
- current += header.chunk_size
498
- elsif header.type == CHUNKTYPE_TYPE
499
- type_id = read_byte(data, current+8)
500
- type_entrycount = read_word(data, current+12)
501
- type_entryoffset = read_word(data, current+16)
502
-
503
- ## The config flags set for this type chunk
504
- ## TODO: Vary the size of the config structure based on size to accomodate for new flags
505
- config_size = read_word(data, current+20) # Number of bytes in structure
506
- type_config = ResTypeConfig.new( read_word(data, current+24),
507
- read_word(data, current+28),
508
- read_word(data, current+32),
509
- read_word(data, current+36 ),
510
- read_word(data, current+40),
511
- read_word(data, current+44),
512
- read_word(data, current+48),
513
- read_word(data, current+52) )
514
-
515
- ## The remainder of the chunk is a list of the entry values for that type/configuration
516
- type_name = stringpool_typestrings.values[type_id - 1]
517
- if current_spec.types == nil
518
- current_spec.types = ResType.new(header, type_name, type_config, type_entrycount, Array.new())
519
- end
520
-
521
- i=0
522
- while i < type_entrycount
523
- ## Ensure a hash exists for each type
524
- if current_spec.types.entries[i] == nil
525
- current_spec.types.entries[i] = Hash.new()
526
- end
527
- current_entry = current_spec.types.entries[i]
528
-
529
- ## Get the start of the type from the offsets table
530
- index_offset = i * 4 + (current+56)
531
- start_offset = read_word(data, index_offset)
532
- if start_offset != OFFSET_NO_ENTRY
533
- ## Set the index_offset to the start of the current entry
534
- index_offset = current + type_entryoffset + start_offset
535
-
536
- entry_flags = read_short(data, index_offset+2)
537
- entry_key = read_word(data, index_offset+4)
538
- entry_data_type = read_byte(data, index_offset+11)
539
- entry_data = read_word(data, index_offset+12)
540
-
541
- # Find the key in our strings index
542
- key_name = stringpool_keystrings.values[entry_key]
543
- # Parse the value into a string
544
- data_value = nil
545
- case type_name
546
- when TYPE_STRING, TYPE_DRAWABLE
547
- data_value = get_resource_string(entry_data_type, entry_data)
548
- when TYPE_COLOR
549
- data_value = get_resource_color(entry_data_type, entry_data)
550
- when TYPE_DIMENSION
551
- data_value = get_resource_dimension(entry_data_type, entry_data)
552
- when TYPE_INTEGER
553
- data_value = get_resource_integer(entry_data_type, entry_data)
554
- when TYPE_BOOLEAN
555
- data_value = get_resource_bool(entry_data_type, entry_data)
556
- else
557
- puts "Complex Resources not yet supported." if DEBUG
558
- data_value = entry_data.to_s
559
- end
560
- current_entry[type_config] = ResTypeEntry.new(entry_flags, key_name, entry_data_type, data_value)
561
- end
562
- i += 1
563
- end
564
-
565
- current += header.chunk_size
566
- elsif header.type == CHUNKTYPE_PACKAGE
567
- ## This is the next package chunk, move along
568
- puts "Next Package Chunk Found...Ending" if DEBUG
569
- current = data.length
570
- else
571
- puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
572
- ## End Immediately
573
- current = data.length
574
- end
575
- end
576
-
577
- return Package.new(package_header, stringpool_typestrings, stringpool_keystrings, type_data)
578
- end
579
-
580
- # Obtain string value for resource id
581
- def get_resource_string(entry_datatype, entry_data)
582
- result = @stringpool_main.values[entry_data]
583
- return result
584
- end
585
-
586
- # Obtain boolean value for resource id
587
- def get_resource_bool(entry_datatype, entry_data)
588
- if entry_data == TYPE_BOOL_TRUE
589
- return "true"
590
- elsif entry_data == TYPE_BOOL_FALSE
591
- return "false"
592
- else
593
- return "undefined"
594
- end
595
- end
596
-
597
- # Obtain integer value for resource id
598
- def get_resource_integer(entry_datatype, entry_data)
599
- if entry_datatype == TYPE_INT_HEX
600
- return "0x#{entry_data.to_s(16)}"
601
- else
602
- return entry_data.to_s
603
- end
604
- end
605
-
606
- # Obtain color value for resource id
607
- def get_resource_color(entry_datatype, entry_data)
608
- case entry_datatype
609
- when TYPE_INT_COLOR_RGB4
610
- return "#" + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
611
- when TYPE_INT_COLOR_ARGB4
612
- 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)
613
- when TYPE_INT_COLOR_RGB8
614
- return "#" + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
615
- when TYPE_INT_COLOR_ARGB8
616
- 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)
617
- else
618
- return "0x#{entry_data.to_s(16)}"
619
- end
620
- end
621
-
622
- # Obtain dimension value for resource id
623
- def get_resource_dimension(entry_datatype, entry_data)
624
- unit_type = (entry_data & 0xFF)
625
- case unit_type
626
- when COMPLEX_UNIT_PX
627
- unit_name = "px"
628
- when COMPLEX_UNIT_DIP
629
- unit_name = "dp"
630
- when COMPLEX_UNIT_SP
631
- unit_name = "sp"
632
- when COMPLEX_UNIT_PT
633
- unit_name = "pt"
634
- when COMPLEX_UNIT_IN
635
- unit_name = "in"
636
- when COMPLEX_UNIT_MM
637
- unit_name = "mm"
638
- else
639
- unit_name = ""
640
- end
641
-
642
- return ((entry_data >> 8) & 0xFFFFFF).to_s + unit_name
643
- end
644
- end
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 resource contents for a package chunk
59
+ #
60
+ # Package = Struct.new(:header, :stringpool_typestrings, :stringpool_keystrings, :type_data)
61
+ #
62
+ # * +package_header+ = PackageHeader
63
+ # * +stringpool_typestrings+ = StringPool containing all type strings in the package
64
+ # * +stringpool_keystrings+ = StringPool containing all key strings in the package
65
+ # * +type_data+ = Array of ResTypeSpec chunks in the package
66
+ Package = Struct.new(:package_header, :stringpool_typestrings, :stringpool_keystrings, :type_data)
67
+
68
+ ##
69
+ # Structure defining the flags for a block of common resources
70
+ #
71
+ # ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
72
+ #
73
+ # * +header+ = ChunkHeader
74
+ # * +id+ = String value of the referenced type (e.g. "drawable")
75
+ # * +entry_count+ = Number of type entries in this chunk
76
+ # * +entries+ = Array of config flags for each type entry
77
+ # * +types+ = The ResType associated with this spec
78
+ ResTypeSpec = Struct.new(:header, :id, :entry_count, :entries, :types)
79
+
80
+ ##
81
+ # Structure that houses all the resources for a given type
82
+ #
83
+ # ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
84
+ #
85
+ # * +header+ = ChunkHeader
86
+ # * +id+ = String value of the referenced type (e.g. "drawable")
87
+ # * +config+ = ResTypeConfig defining the configuration for this type
88
+ # * +entry_count+ = Number of entries in this chunk
89
+ # * +entries+ = Array of Hashes of [ResTypeConfig, ResTypeEntry] in this chunk
90
+ ResType = Struct.new(:header, :id, :config, :entry_count, :entries)
91
+
92
+ ##
93
+ # Structure that houses the configuration flags for a given resource.
94
+ #
95
+ # ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
96
+ #
97
+ # * +imsi+ = Flags marking country code and network code
98
+ # * +locale+ = Flags marking locale requirements (language)
99
+ # * +screen_type+ = Flags/values for screen density
100
+ # * +input+ = Flags marking input types and visibility status
101
+ # * +screen_size+ = Flags marking screen size and length
102
+ # * +version+ = Minimum API version
103
+ # * +screen_config+ = Flags marking screen configuration (like orientation)
104
+ # * +screen_size_dp+ = Flags marking smallest width constraints
105
+ #
106
+ # A default configuration is defined as ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
107
+ ResTypeConfig = Struct.new(:imsi, :locale, :screen_type, :input, :screen_size, :version, :screen_config, :screen_size_dp)
108
+
109
+ ##
110
+ # Structure that houses the data for a given resource entry
111
+ #
112
+ # ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
113
+ #
114
+ # * +flags+ = Flags marking if the resource is complex or public
115
+ # * +key+ = Key string for the resource (e.g. "ic_launcher" of R.drawable.ic_launcher")
116
+ # * +data_type+ = Type identifier. The meaning of this value varies with the type of resource
117
+ # * +data+ = Resource value (e.g. "res/drawable/ic_launcher" for R.drawable.ic_launcher")
118
+ #
119
+ # A single resource key can have multiple entries depending on configuration, so these structs
120
+ # are often returned in groups, keyed by a ResTypeConfig
121
+ ResTypeEntry = Struct.new(:flags, :key, :data_type, :data)
122
+
123
+ # PackageHeader containing information about all the type and key strings in the package
124
+ attr_reader :package_header
125
+ # StringPool containing all value strings in the package
126
+ attr_reader :stringpool_main
127
+ # Hash of Package chunks, keyed by package id
128
+ attr_reader :packages
129
+
130
+ ##
131
+ # Create a new ApkResources instance from the specified +apk_file+
132
+ #
133
+ # This opens and parses the contents of the APK's resources.arsc file.
134
+ def initialize(apk_file)
135
+ data = nil
136
+ Zip.warn_invalid_date = false
137
+
138
+ # Get resources.arsc from the APK file
139
+ Zip::File.foreach(apk_file) do |f|
140
+ if f.name.match(/resources.arsc/)
141
+ data = f.get_input_stream.read.force_encoding('BINARY')
142
+ end
143
+ end
144
+
145
+ # Parse the Table Chunk
146
+ ## Header
147
+ header_type = read_short(data, HEADER_START)
148
+ header_size = read_short(data, HEADER_START+2)
149
+ header_chunk_size = read_word(data, HEADER_START+4)
150
+ header_package_count = read_word(data, HEADER_START+8)
151
+ puts "Resource Package Count = #{header_package_count}" if DEBUG
152
+
153
+ # Parse the StringPool Chunk
154
+ ## Header
155
+ startoffset_pool = HEADER_START + header_size
156
+ puts "Parse Main StringPool Chunk" if DEBUG
157
+ @stringpool_main = parse_stringpool(data, startoffset_pool)
158
+ puts "#{@stringpool_main.values.length} strings found" if DEBUG
159
+
160
+ # Parse the Package Chunk
161
+ ## Header
162
+ startoffset_package = startoffset_pool + @stringpool_main.header.chunk_size
163
+ @packages = Hash.new()
164
+ i = 0
165
+ while i < header_package_count
166
+ package_element = parse_package(data, startoffset_package)
167
+ puts "Package #{package_element.package_header.id}" if DEBUG
168
+ startoffset_package = startoffset_package + package_element.package_header.header.chunk_size
169
+ @packages[package_element.package_header.id] = package_element
170
+
171
+ i += 1
172
+ end
173
+
174
+ end #initalize
175
+
176
+ ##
177
+ # Return array of all string values in the file
178
+
179
+ def get_all_strings
180
+ return @stringpool_main.values
181
+ end
182
+
183
+ ##
184
+ # Return hash of all the type values in the file
185
+ # keyed by package id
186
+
187
+ def get_all_types
188
+ types = Hash.new()
189
+ @packages.each do |key, value|
190
+ types[key] = value.stringpool_typestrings.values
191
+ end
192
+
193
+ return types
194
+ end
195
+
196
+ ##
197
+ # Return hash of all the key values in the file
198
+ # keyed by package id
199
+
200
+ def get_all_keys
201
+ keys = Hash.new()
202
+ @packages.each do |key, value|
203
+ keys[key] = value.stringpool_keystrings.values
204
+ end
205
+
206
+ return keys
207
+ end
208
+
209
+ ##
210
+ # Obtain the key value for a given resource id
211
+ #
212
+ # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
213
+ # xml_format: Optionally format return string for XML files.
214
+ #
215
+ # If xml_format is true, return value will be @<type>/<key>
216
+ # If xml_format is false or missing, return value will be R.<type>.<key>
217
+ # If the resource id does not exist, return value will be nil
218
+
219
+ def get_resource_key(res_id, xml_format=false)
220
+ if res_id.is_a? String
221
+ res_id = res_id.hex
222
+ end
223
+
224
+ # R.id integers are a concatenation of package_id, type_id, and entry index
225
+ res_package = (res_id >> 24) & 0xFF
226
+ res_type = (res_id >> 16) & 0xFF
227
+ res_index = res_id & 0xFFFF
228
+
229
+ package_element = @packages[res_package]
230
+ if package_element == nil
231
+ # This is not a resource we can parse
232
+ return nil
233
+ end
234
+
235
+ res_spec = package_element.type_data[res_type-1]
236
+ if res_spec == nil
237
+ puts "Could not find ResTypeSpec for #{res_package} #{res_type}" if DEBUG
238
+ return nil
239
+ end
240
+
241
+ entry = res_spec.types.entries[res_index]
242
+ if entry == nil
243
+ # There is no entry in our table for this resource
244
+ puts "Could not find #{res_spec.types.id} ResType chunk" if DEBUG
245
+ return nil
246
+ end
247
+
248
+ if xml_format
249
+ return "@#{res_spec.id}/#{entry.values[0].key}"
250
+ else
251
+ return "R.#{res_spec.id}.#{entry.values[0].key}"
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Obtain the default value for a given resource id
257
+ #
258
+ # res_id: ID values of a resources as a FixNum or String representation (i.e. 0x7F060001)
259
+ #
260
+ # Returns: The default ResTypeEntry to the given id, or nil if no default exists
261
+
262
+ def get_default_resource_value(res_id)
263
+ if res_id.is_a? String
264
+ res_id = res_id.hex
265
+ end
266
+
267
+ entries = get_resource_value(res_id)
268
+ if entries != nil
269
+ default = ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
270
+ default_entry = entries[default]
271
+ return default_entry
272
+ else
273
+ return nil
274
+ end
275
+ end
276
+
277
+ ##
278
+ # Obtain the value(s) for a given resource id.
279
+ # A default resource is one defined in an unqualified directory.
280
+ #
281
+ # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
282
+ #
283
+ # Returns: Hash of all entries matching this id, keyed by their matching ResTypeConfig
284
+ # or nil if the resource id cannot be found.
285
+
286
+ def get_resource_value(res_id)
287
+ if res_id.is_a? String
288
+ res_id = res_id.hex
289
+ end
290
+
291
+ # R.id integers are a concatenation of package_id, type_id, and entry index
292
+ res_package = (res_id >> 24) & 0xFF
293
+ res_type = (res_id >> 16) & 0xFF
294
+ res_index = res_id & 0xFFFF
295
+
296
+ package_element = @packages[res_package]
297
+ if package_element == nil
298
+ # This is not a resource we can parse
299
+ return nil
300
+ end
301
+
302
+ res_spec = package_element.type_data[res_type-1]
303
+ if res_spec == nil
304
+ puts "Could not find ResTypeSpec for #{res_package} #{res_type}" if DEBUG
305
+ return nil
306
+ end
307
+
308
+ entries = res_spec.types.entries[res_index]
309
+ if entries == nil
310
+ puts "Could not find #{res_spec.types.id} ResType chunk" if DEBUG
311
+ return nil
312
+ end
313
+
314
+ return entries
315
+ end
316
+
317
+ private # Private Helper Methods
318
+
319
+ # Type Constants
320
+ TYPENAME_ARRAY = "array" # :nodoc:
321
+ TYPENAME_ATTRIBUTE = "attr" # :nodoc:
322
+ TYPENAME_BOOLEAN = "bool" # :nodoc:
323
+ TYPENAME_COLOR = "color" # :nodoc:
324
+ TYPENAME_DIMENSION = "dimen" # :nodoc:
325
+ TYPENAME_DRAWABLE = "drawable" # :nodoc:
326
+ TYPENAME_FRACTION = "fraction" # :nodoc:
327
+ TYPENAME_INTEGER = "integer" # :nodoc:
328
+ TYPENAME_LAYOUT = "layout" # :nodoc:
329
+ TYPENAME_PLURALS = "plurals" # :nodoc:
330
+ TYPENAME_STRING = "string" # :nodoc:
331
+ TYPENAME_STYLE = "style" # :nodoc:
332
+
333
+ # Data Type Constants (mirrors ResourceTypes.h)
334
+ TYPE_NULL = 0x0 # :nodoc:
335
+ TYPE_REFERENCE = 0x1 # :nodoc:
336
+ TYPE_ATTRIBUTE = 0x2 # :nodoc:
337
+ TYPE_STRING = 0x3 # :nodoc:
338
+ TYPE_FLOAT = 0x4 # :nodoc:
339
+ TYPE_DIMENSION = 0x5 # :nodoc:
340
+ TYPE_FRACTION = 0x6 # :nodoc:
341
+ TYPE_DYNAMIC_DIMEN = 0x7 # :nodoc:
342
+ TYPE_INT_DEC = 0x10 # :nodoc:
343
+ TYPE_INT_HEX = 0x11 # :nodoc:
344
+ TYPE_BOOL = 0x12 # :nodoc:
345
+
346
+ TYPE_INT_COLOR_ARGB8 = 0x1C # :nodoc:
347
+ TYPE_INT_COLOR_RGB8 = 0x1D # :nodoc:
348
+ TYPE_INT_COLOR_ARGB4 = 0x1E # :nodoc:
349
+ TYPE_INT_COLOR_RGB4 = 0x1F # :nodoc:
350
+
351
+ COMPLEX_UNIT_PX = 0x0 # :nodoc:
352
+ COMPLEX_UNIT_DIP = 0x1 # :nodoc:
353
+ COMPLEX_UNIT_SP = 0x2 # :nodoc:
354
+ COMPLEX_UNIT_PT = 0x3 # :nodoc:
355
+ COMPLEX_UNIT_IN = 0x4 # :nodoc:
356
+ COMPLEX_UNIT_MM = 0x5 # :nodoc:
357
+
358
+ COMPLEX_UNIT_FRACTION = 0x0 # :nodoc:
359
+ COMPLEX_UNIT_FRACTION_PARENT = 0x1 # :nodoc:
360
+
361
+ # Data Constants
362
+ TYPE_BOOL_TRUE = 0xFFFFFFFF # :nodoc:
363
+ TYPE_BOOL_FALSE = 0x00000000 # :nodoc:
364
+
365
+ # Header Constants
366
+ CHUNKTYPE_TYPESPEC = 0x202 # :nodoc:
367
+ CHUNKTYPE_TYPE = 0x201 # :nodoc:
368
+ CHUNKTYPE_PACKAGE = 0x200 # :nodoc:
369
+
370
+ #Flag Constants
371
+ FLAG_UTF8 = 0x100 # :nodoc:
372
+ FLAG_COMPLEX = 0x0001 # :nodoc:
373
+ FLAG_PUBLIC = 0x0002 # :nodoc:
374
+
375
+ OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
376
+ HEADER_START = 0 # :nodoc:
377
+
378
+ # Read a 32-bit word from a specific location in the data
379
+ def read_word(data, offset)
380
+ out = data[offset,4].unpack('V').first rescue 0
381
+ return out
382
+ end
383
+
384
+ # Read a 16-bit short from a specific location in the data
385
+ def read_short(data, offset)
386
+ out = data[offset,2].unpack('v').first rescue 0
387
+ return out
388
+ end
389
+
390
+ # Read a 8-bit byte from a specific location in the data
391
+ def read_byte(data, offset)
392
+ out = data[offset,1].unpack('C').first rescue 0
393
+ return out
394
+ end
395
+
396
+ # Read in length bytes in as a String
397
+ def read_string(data, offset, length, encoding)
398
+ if "UTF-16".casecmp(encoding) == 0
399
+ out = data[offset, length].unpack('v*').pack('U*')
400
+ else
401
+ out = data[offset, length].unpack('C*').pack('U*')
402
+ end
403
+ return out
404
+ end
405
+
406
+ # Return id as a hex string
407
+ def res_id_to_s(res_id)
408
+ return "0x#{res_id.to_s(16)}"
409
+ end
410
+
411
+ # Parse out a StringPool chunk
412
+ def parse_stringpool(data, offset)
413
+ pool_header = ChunkHeader.new( read_short(data, offset),
414
+ read_short(data, offset+2),
415
+ read_word(data, offset+4) )
416
+
417
+ pool_string_count = read_word(data, offset+8)
418
+ pool_style_count = read_word(data, offset+12)
419
+ pool_flags = read_word(data, offset+16)
420
+ format_utf8 = (pool_flags & FLAG_UTF8) != 0
421
+ puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
422
+
423
+ pool_string_offset = read_word(data, offset+20)
424
+ pool_style_offset = read_word(data, offset+24)
425
+
426
+ values = Array.new()
427
+ i = 0
428
+ while i < pool_string_count
429
+ # Read the string value
430
+ index = i * 4 + (offset+28)
431
+ offset_addr = pool_string_offset + offset + read_word(data, index)
432
+ if format_utf8
433
+ length = read_byte(data, offset_addr)
434
+ if (length & 0x80) != 0
435
+ length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
436
+ end
437
+
438
+ values << read_string(data, offset_addr + 2, length, "UTF-8")
439
+ else
440
+ length = read_short(data, offset_addr)
441
+ if (length & 0x8000) != 0
442
+ #There is one more length value before the data
443
+ length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
444
+ values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
445
+ else
446
+ # Read the data
447
+ values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
448
+ end
449
+ end
450
+
451
+ i += 1
452
+ end
453
+
454
+ return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
455
+ end
456
+
457
+ # Parse out a Package Chunk
458
+ def parse_package(data, offset)
459
+ header = ChunkHeader.new( read_short(data, offset),
460
+ read_short(data, offset+2),
461
+ read_word(data, offset+4) )
462
+
463
+ package_id = read_word(data, offset+8)
464
+ package_name = read_string(data, offset+12, 256, "UTF-8")
465
+ package_type_strings = read_word(data, offset+268)
466
+ package_last_type = read_word(data, offset+272)
467
+ package_key_strings = read_word(data, offset+276)
468
+ package_last_key = read_word(data, offset+280)
469
+
470
+ package_header = PackageHeader.new(header, package_id, package_name, package_type_strings, package_key_strings)
471
+
472
+ ## typeStrings StringPool
473
+ startoffset_typestrings = offset + package_type_strings
474
+ puts "Parse typeStrings StringPool Chunk" if DEBUG
475
+ stringpool_typestrings = parse_stringpool(data, startoffset_typestrings)
476
+
477
+ ## keyStrings StringPool
478
+ startoffset_keystrings = offset + package_key_strings
479
+ puts "Parse keyStrings StringPool Chunk" if DEBUG
480
+ stringpool_keystrings = parse_stringpool(data, startoffset_keystrings)
481
+
482
+ ## typeSpec/type Chunks
483
+ type_data = Array.new()
484
+ current_spec = nil
485
+
486
+ current = startoffset_keystrings + stringpool_keystrings.header.chunk_size
487
+ puts "Parse Type/TypeSpec Chunks" if DEBUG
488
+ while current < data.length
489
+ ## Parse Header
490
+ header = ChunkHeader.new( read_short(data, current),
491
+ read_short(data, current+2),
492
+ read_word(data, current+4) )
493
+ ## Check Type
494
+ if header.type == CHUNKTYPE_TYPESPEC
495
+ typespec_id = read_byte(data, current+8)
496
+ typespec_entrycount = read_word(data, current+12)
497
+
498
+ ## Parse the config flags for each entry
499
+ typespec_entries = Array.new()
500
+ i=0
501
+ while i < typespec_entrycount
502
+ offset = i * 4 + (current+16)
503
+ typespec_entries << read_word(data, offset)
504
+
505
+ i += 1
506
+ end
507
+
508
+ typespec_name = stringpool_typestrings.values[typespec_id - 1]
509
+ current_spec = ResTypeSpec.new(header, typespec_name, typespec_entrycount, typespec_entries, nil)
510
+
511
+ type_data << current_spec
512
+ current += header.chunk_size
513
+ elsif header.type == CHUNKTYPE_TYPE
514
+ type_id = read_byte(data, current+8)
515
+ type_entrycount = read_word(data, current+12)
516
+ type_entryoffset = read_word(data, current+16)
517
+
518
+ ## The config flags set for this type chunk
519
+ ## TODO: Vary the size of the config structure based on size to accomodate for new flags
520
+ config_start = current+20
521
+ config_size = read_word(data, config_start) # Number of bytes in structure
522
+ type_config = ResTypeConfig.new( read_word(data, config_start+4),
523
+ read_word(data, config_start+8),
524
+ read_word(data, config_start+12),
525
+ read_word(data, config_start+16 ),
526
+ read_word(data, config_start+20),
527
+ read_word(data, config_start+24),
528
+ read_word(data, config_start+28),
529
+ read_word(data, config_start+32) )
530
+ ## TODO: This config structure is outdated. Update to latest aapt specs.
531
+
532
+ ## The end of the config structure marks the offsets table
533
+ offset_table_start = config_start + config_size
534
+ ## The remainder of the chunk is a list of the entry values for that type/configuration
535
+ type_name = stringpool_typestrings.values[type_id - 1]
536
+ if current_spec.types == nil
537
+ current_spec.types = ResType.new(header, type_name, type_config, type_entrycount, Array.new())
538
+ end
539
+
540
+ i=0
541
+ while i < type_entrycount
542
+ ## Ensure a hash exists for each type
543
+ if current_spec.types.entries[i] == nil
544
+ current_spec.types.entries[i] = Hash.new()
545
+ end
546
+ current_entry = current_spec.types.entries[i]
547
+
548
+ ## Get the start of the type from the offsets table
549
+ index_offset = i * 4 + offset_table_start
550
+ start_offset = read_word(data, index_offset)
551
+ if start_offset != OFFSET_NO_ENTRY
552
+ ## Set the index_offset to the start of the current entry
553
+ index_offset = current + type_entryoffset + start_offset
554
+
555
+ entry_flags = read_short(data, index_offset+2)
556
+ entry_key = read_word(data, index_offset+4)
557
+ entry_data_type = read_byte(data, index_offset+11)
558
+ entry_data = read_word(data, index_offset+12)
559
+
560
+ # Find the key in our strings index
561
+ key_name = stringpool_keystrings.values[entry_key]
562
+ # Parse the value into a string
563
+ case entry_data_type
564
+ when TYPE_NULL
565
+ data_value = nil
566
+ when TYPE_REFERENCE
567
+ ## TODO: Mark these here, and resolve after package is parsed
568
+ data_value = res_id_to_s(entry_data)
569
+ when TYPE_STRING
570
+ data_value = get_resource_string(entry_data_type, entry_data)
571
+ when TYPE_INT_COLOR_ARGB8..TYPE_INT_COLOR_RGB4
572
+ data_value = get_resource_color(entry_data_type, entry_data)
573
+ when TYPE_DIMENSION
574
+ data_value = get_resource_dimension(entry_data_type, entry_data)
575
+ when TYPE_INT_DEC, TYPE_INT_HEX
576
+ data_value = get_resource_integer(entry_data_type, entry_data)
577
+ when TYPE_BOOL
578
+ data_value = get_resource_bool(entry_data_type, entry_data)
579
+ when TYPE_FLOAT
580
+ data_value = get_resource_float(entry_data_type, entry_data)
581
+ when TYPE_FRACTION
582
+ data_value = get_resource_fraction(entry_data_type, entry_data)
583
+ else
584
+ puts "Complex Resource (%s,%d) not yet supported." % [type_name,entry_data_type] if DEBUG
585
+ data_value = entry_data.to_s
586
+ end
587
+ current_entry[type_config] = ResTypeEntry.new(entry_flags, key_name, entry_data_type, data_value)
588
+ end
589
+ i += 1
590
+ end
591
+
592
+ current += header.chunk_size
593
+ elsif header.type == CHUNKTYPE_PACKAGE
594
+ ## This is the next package chunk, move along
595
+ puts "Next Package Chunk Found...Ending" if DEBUG
596
+ current = data.length
597
+ else
598
+ puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
599
+ ## End Immediately
600
+ current = data.length
601
+ end
602
+ end
603
+
604
+ return Package.new(package_header, stringpool_typestrings, stringpool_keystrings, type_data)
605
+ end
606
+
607
+ # Obtain string value for resource id
608
+ def get_resource_string(entry_datatype, entry_data)
609
+ result = @stringpool_main.values[entry_data]
610
+ return result
611
+ end
612
+
613
+ # Obtain boolean value for resource id
614
+ def get_resource_bool(entry_datatype, entry_data)
615
+ if entry_data == TYPE_BOOL_TRUE
616
+ return "true"
617
+ elsif entry_data == TYPE_BOOL_FALSE
618
+ return "false"
619
+ else
620
+ return "undefined"
621
+ end
622
+ end
623
+
624
+ # Obtain integer value for resource id
625
+ def get_resource_integer(entry_datatype, entry_data)
626
+ if entry_datatype == TYPE_INT_HEX
627
+ return "0x#{entry_data.to_s(16)}"
628
+ else
629
+ return entry_data.to_s
630
+ end
631
+ end
632
+
633
+ # Obtain color value for resource id
634
+ def get_resource_color(entry_datatype, entry_data)
635
+ case entry_datatype
636
+ when TYPE_INT_COLOR_RGB4
637
+ return "#" + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
638
+ when TYPE_INT_COLOR_ARGB4
639
+ 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)
640
+ when TYPE_INT_COLOR_RGB8
641
+ return "#" + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
642
+ when TYPE_INT_COLOR_ARGB8
643
+ 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)
644
+ else
645
+ return "0x#{entry_data.to_s(16)}"
646
+ end
647
+ end
648
+
649
+
650
+ # Obtain a float value for resource id
651
+ def get_resource_float(entry_data_type, entry_data)
652
+ result = [entry_data].pack('I').unpack('F')
653
+ return result[0].to_s
654
+ end
655
+
656
+ # Obtain dimension value for resource id
657
+ def get_resource_dimension(entry_datatype, entry_data)
658
+ unit_type = (entry_data & 0xF)
659
+ case unit_type
660
+ when COMPLEX_UNIT_PX
661
+ unit_name = "px"
662
+ when COMPLEX_UNIT_DIP
663
+ unit_name = "dp"
664
+ when COMPLEX_UNIT_SP
665
+ unit_name = "sp"
666
+ when COMPLEX_UNIT_PT
667
+ unit_name = "pt"
668
+ when COMPLEX_UNIT_IN
669
+ unit_name = "in"
670
+ when COMPLEX_UNIT_MM
671
+ unit_name = "mm"
672
+ else
673
+ unit_name = ""
674
+ end
675
+
676
+ return complex_to_float(entry_data).to_s + unit_name
677
+ #return ((entry_data >> 8) & 0xFFFFFF).to_s + unit_name
678
+ end
679
+
680
+ # Obtain a fraction value for resource id
681
+ def get_resource_fraction(entry_data_type, entry_data)
682
+ unit_type = (entry_data & 0xF)
683
+ case unit_type
684
+ when COMPLEX_UNIT_FRACTION
685
+ unit_name = "%"
686
+ when COMPLEX_UNIT_FRACTION_PARENT
687
+ unit_name = "%p"
688
+ else
689
+ unit_name = ""
690
+ end
691
+
692
+ # Return float as a percentage
693
+ return (complex_to_float(entry_data) * 100).to_s + unit_name
694
+ end
695
+
696
+ def complex_to_float(complex)
697
+ mantissa_mult = 1.0 / (1 << 8)
698
+ multipliers = [1.0*mantissa_mult, 1.0/(1<<7)*mantissa_mult, 1.0/(1<<15)*mantissa_mult, 1.0/(1<<23)*mantissa_mult]
699
+
700
+ mantissa = complex & 0xFFFFFF00
701
+ radix = (complex >> 4) & 0x3
702
+
703
+ return (mantissa * multipliers[radix]).to_f.round(4)
704
+ end
705
+ end