apktools 0.6.0 → 0.7.4

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.
@@ -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