apktools 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/apktools/apkresources.rb +683 -623
  2. data/lib/apktools/apkxml.rb +422 -422
  3. metadata +10 -12
@@ -1,14 +1,14 @@
1
1
  # Copyright (C) 2014 Dave Smith
2
- #
2
+ #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
4
  # software and associated documentation files (the "Software"), to deal in the Software
5
5
  # without restriction, including without limitation the rights to use, copy, modify,
6
6
  # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
7
7
  # persons to whom the Software is furnished to do so, subject to the following conditions:
8
- #
8
+ #
9
9
  # The above copyright notice and this permission notice shall be included in all copies
10
10
  # or substantial portions of the Software.
11
- #
11
+ #
12
12
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13
13
  # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14
14
  # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
@@ -16,629 +16,689 @@
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
+
135
+ def initialize(apk_file)
136
+ data = nil
137
+ # Get resources.arsc from the APK file
138
+ Zip::File.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
+ puts "Could not find #{res_spec.types.id} ResType chunk" if DEBUG
244
+ return nil
245
+ end
246
+
247
+ if xml_format
248
+ return "@#{res_spec.id}/#{entry.values[0].key}"
249
+ else
250
+ return "R.#{res_spec.id}.#{entry.values[0].key}"
251
+ end
252
+ end
253
+
254
+ ##
255
+ # Obtain the default value for a given resource id
256
+ #
257
+ # res_id: ID values of a resources as a FixNum or String representation (i.e. 0x7F060001)
258
+ #
259
+ # Returns: The default ResTypeEntry to the given id, or nil if no default exists
260
+
261
+ def get_default_resource_value(res_id)
262
+ if res_id.is_a? String
263
+ res_id = res_id.hex
264
+ end
265
+
266
+ entries = get_resource_value(res_id)
267
+ if entries != nil
268
+ default = ResTypeConfig.new(0, 0, 0, 0, 0, 0, 0, 0)
269
+ default_entry = entries[default]
270
+ return default_entry
271
+ else
272
+ return nil
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Obtain the value(s) for a given resource id.
278
+ # A default resource is one defined in an unqualified directory.
279
+ #
280
+ # res_id: ID value of a resource as a FixNum or String representation (i.e. 0x7F060001)
281
+ #
282
+ # Returns: Hash of all entries matching this id, keyed by their matching ResTypeConfig
283
+ # or nil if the resource id cannot be found.
284
+
285
+ def get_resource_value(res_id)
286
+ if res_id.is_a? String
287
+ res_id = res_id.hex
288
+ end
289
+
290
+ # R.id integers are a concatenation of package_id, type_id, and entry index
291
+ res_package = (res_id >> 24) & 0xFF
292
+ res_type = (res_id >> 16) & 0xFF
293
+ res_index = res_id & 0xFFFF
294
+
295
+ package_element = @packages[res_package]
296
+ if package_element == nil
297
+ # This is not a resource we can parse
298
+ return nil
299
+ end
300
+
301
+ res_spec = package_element.type_data[res_type-1]
302
+ if res_spec == nil
303
+ puts "Could not find ResTypeSpec for #{res_package} #{res_type}" if DEBUG
304
+ return nil
305
+ end
306
+
307
+ entries = res_spec.types.entries[res_index]
308
+ if entries == nil
309
+ puts "Could not find #{res_spec.types.id} ResType chunk" if DEBUG
310
+ return nil
311
+ end
312
+
313
+ return entries
314
+ end
315
+
316
+ private # Private Helper Methods
317
+
318
+ # Type Constants
319
+ TYPENAME_ARRAY = "array" # :nodoc:
320
+ TYPENAME_ATTRIBUTE = "attr" # :nodoc:
321
+ TYPENAME_BOOLEAN = "bool" # :nodoc:
322
+ TYPENAME_COLOR = "color" # :nodoc:
323
+ TYPENAME_DIMENSION = "dimen" # :nodoc:
324
+ TYPENAME_DRAWABLE = "drawable" # :nodoc:
325
+ TYPENAME_FRACTION = "fraction" # :nodoc:
326
+ TYPENAME_INTEGER = "integer" # :nodoc:
327
+ TYPENAME_LAYOUT = "layout" # :nodoc:
328
+ TYPENAME_PLURALS = "plurals" # :nodoc:
329
+ TYPENAME_STRING = "string" # :nodoc:
330
+ TYPENAME_STYLE = "style" # :nodoc:
331
+
332
+ # Data Type Constants (mirrors ResourceTypes.h)
333
+ TYPE_NULL = 0x0 # :nodoc:
334
+ TYPE_REFERENCE = 0x1 # :nodoc:
335
+ TYPE_ATTRIBUTE = 0x2 # :nodoc:
336
+ TYPE_STRING = 0x3 # :nodoc:
337
+ TYPE_FLOAT = 0x4 # :nodoc:
338
+ TYPE_DIMENSION = 0x5 # :nodoc:
339
+ TYPE_FRACTION = 0x6 # :nodoc:
340
+ TYPE_DYNAMIC_DIMEN = 0x7 # :nodoc:
341
+ TYPE_INT_DEC = 0x10 # :nodoc:
342
+ TYPE_INT_HEX = 0x11 # :nodoc:
343
+ TYPE_BOOL = 0x12 # :nodoc:
344
+
345
+ TYPE_INT_COLOR_ARGB8 = 0x1C # :nodoc:
346
+ TYPE_INT_COLOR_RGB8 = 0x1D # :nodoc:
347
+ TYPE_INT_COLOR_ARGB4 = 0x1E # :nodoc:
348
+ TYPE_INT_COLOR_RGB4 = 0x1F # :nodoc:
349
+
350
+ COMPLEX_UNIT_PX = 0x0 # :nodoc:
351
+ COMPLEX_UNIT_DIP = 0x1 # :nodoc:
352
+ COMPLEX_UNIT_SP = 0x2 # :nodoc:
353
+ COMPLEX_UNIT_PT = 0x3 # :nodoc:
354
+ COMPLEX_UNIT_IN = 0x4 # :nodoc:
355
+ COMPLEX_UNIT_MM = 0x5 # :nodoc:
356
+
357
+ COMPLEX_UNIT_FRACTION = 0x0 # :nodoc:
358
+ COMPLEX_UNIT_FRACTION_PARENT = 0x1 # :nodoc:
359
+
360
+ # Data Constants
361
+ TYPE_BOOL_TRUE = 0xFFFFFFFF # :nodoc:
362
+ TYPE_BOOL_FALSE = 0x00000000 # :nodoc:
363
+
364
+ # Header Constants
365
+ CHUNKTYPE_TYPESPEC = 0x202 # :nodoc:
366
+ CHUNKTYPE_TYPE = 0x201 # :nodoc:
367
+ CHUNKTYPE_PACKAGE = 0x200 # :nodoc:
368
+
369
+ #Flag Constants
370
+ FLAG_UTF8 = 0x100 # :nodoc:
371
+ FLAG_COMPLEX = 0x0001 # :nodoc:
372
+ FLAG_PUBLIC = 0x0002 # :nodoc:
373
+
374
+ OFFSET_NO_ENTRY = 0xFFFFFFFF # :nodoc:
375
+ HEADER_START = 0 # :nodoc:
376
+
377
+ # Read a 32-bit word from a specific location in the data
378
+ def read_word(data, offset)
379
+ out = data[offset,4].unpack('V').first rescue 0
380
+ return out
381
+ end
382
+
383
+ # Read a 16-bit short from a specific location in the data
384
+ def read_short(data, offset)
385
+ out = data[offset,2].unpack('v').first rescue 0
386
+ return out
387
+ end
388
+
389
+ # Read a 8-bit byte from a specific location in the data
390
+ def read_byte(data, offset)
391
+ out = data[offset,1].unpack('C').first rescue 0
392
+ return out
393
+ end
394
+
395
+ # Read in length bytes in as a String
396
+ def read_string(data, offset, length, encoding)
397
+ if "UTF-16".casecmp(encoding) == 0
398
+ out = data[offset, length].unpack('v*').pack('U*')
399
+ else
400
+ out = data[offset, length].unpack('C*').pack('U*')
401
+ end
402
+ return out
403
+ end
404
+
405
+ # Return id as a hex string
406
+ def res_id_to_s(res_id)
407
+ return "0x#{res_id.to_s(16)}"
408
+ end
409
+
410
+ # Parse out a StringPool chunk
411
+ def parse_stringpool(data, offset)
412
+ pool_header = ChunkHeader.new( read_short(data, offset),
413
+ read_short(data, offset+2),
414
+ read_word(data, offset+4) )
415
+
416
+ pool_string_count = read_word(data, offset+8)
417
+ pool_style_count = read_word(data, offset+12)
418
+ pool_flags = read_word(data, offset+16)
419
+ format_utf8 = (pool_flags & FLAG_UTF8) != 0
420
+ puts 'StringPool format is %s' % [format_utf8 ? "UTF-8" : "UTF-16"] if DEBUG
421
+
422
+ pool_string_offset = read_word(data, offset+20)
423
+ pool_style_offset = read_word(data, offset+24)
424
+
425
+ values = Array.new()
426
+ i = 0
427
+ while i < pool_string_count
428
+ # Read the string value
429
+ index = i * 4 + (offset+28)
430
+ offset_addr = pool_string_offset + offset + read_word(data, index)
431
+ if format_utf8
432
+ length = read_byte(data, offset_addr)
433
+ if (length & 0x80) != 0
434
+ length = ((length & 0x7F) << 8) + read_byte(data, offset_addr+1)
435
+ end
436
+
437
+ values << read_string(data, offset_addr + 2, length, "UTF-8")
438
+ else
439
+ length = read_short(data, offset_addr)
440
+ if (length & 0x8000) != 0
441
+ #There is one more length value before the data
442
+ length = ((length & 0x7FFF) << 16) + read_short(data, offset_addr+2)
443
+ values << read_string(data, offset_addr + 4, length * 2, "UTF-16")
444
+ else
445
+ # Read the data
446
+ values << read_string(data, offset_addr + 2, length * 2, "UTF-16")
447
+ end
448
+ end
449
+
450
+ i += 1
451
+ end
452
+
453
+ return StringPool.new(pool_header, pool_string_count, pool_style_count, values)
454
+ end
455
+
456
+ # Parse out a Package Chunk
457
+ def parse_package(data, offset)
458
+ header = ChunkHeader.new( read_short(data, offset),
459
+ read_short(data, offset+2),
460
+ read_word(data, offset+4) )
461
+
462
+ package_id = read_word(data, offset+8)
463
+ package_name = read_string(data, offset+12, 256, "UTF-8")
464
+ package_type_strings = read_word(data, offset+268)
465
+ package_last_type = read_word(data, offset+272)
466
+ package_key_strings = read_word(data, offset+276)
467
+ package_last_key = read_word(data, offset+280)
468
+
469
+ package_header = PackageHeader.new(header, package_id, package_name, package_type_strings, package_key_strings)
470
+
471
+ ## typeStrings StringPool
472
+ startoffset_typestrings = offset + package_type_strings
473
+ puts "Parse typeStrings StringPool Chunk" if DEBUG
474
+ stringpool_typestrings = parse_stringpool(data, startoffset_typestrings)
475
+
476
+ ## keyStrings StringPool
477
+ startoffset_keystrings = offset + package_key_strings
478
+ puts "Parse keyStrings StringPool Chunk" if DEBUG
479
+ stringpool_keystrings = parse_stringpool(data, startoffset_keystrings)
480
+
481
+ ## typeSpec/type Chunks
482
+ type_data = Array.new()
483
+ current_spec = nil
484
+
485
+ current = startoffset_keystrings + stringpool_keystrings.header.chunk_size
486
+ puts "Parse Type/TypeSpec Chunks" if DEBUG
487
+ while current < data.length
488
+ ## Parse Header
489
+ header = ChunkHeader.new( read_short(data, current),
490
+ read_short(data, current+2),
491
+ read_word(data, current+4) )
492
+ ## Check Type
493
+ if header.type == CHUNKTYPE_TYPESPEC
494
+ typespec_id = read_byte(data, current+8)
495
+ typespec_entrycount = read_word(data, current+12)
496
+
497
+ ## Parse the config flags for each entry
498
+ typespec_entries = Array.new()
499
+ i=0
500
+ while i < typespec_entrycount
501
+ offset = i * 4 + (current+16)
502
+ typespec_entries << read_word(data, offset)
503
+
504
+ i += 1
505
+ end
506
+
507
+ typespec_name = stringpool_typestrings.values[typespec_id - 1]
508
+ current_spec = ResTypeSpec.new(header, typespec_name, typespec_entrycount, typespec_entries, nil)
509
+
510
+ type_data << current_spec
511
+ current += header.chunk_size
512
+ elsif header.type == CHUNKTYPE_TYPE
513
+ type_id = read_byte(data, current+8)
514
+ type_entrycount = read_word(data, current+12)
515
+ type_entryoffset = read_word(data, current+16)
516
+
517
+ ## The config flags set for this type chunk
518
+ ## TODO: Vary the size of the config structure based on size to accomodate for new flags
519
+ config_start = current+20
520
+ config_size = read_word(data, config_start) # Number of bytes in structure
521
+ type_config = ResTypeConfig.new( read_word(data, config_start+4),
522
+ read_word(data, config_start+8),
523
+ read_word(data, config_start+12),
524
+ read_word(data, config_start+16 ),
525
+ read_word(data, config_start+20),
526
+ read_word(data, config_start+24),
527
+ read_word(data, config_start+28),
528
+ read_word(data, config_start+32) )
529
+ ## TODO: This config structure is outdated. Update to latest aapt specs.
530
+
531
+ ## The end of the config structure marks the offsets table
532
+ offset_table_start = config_start + config_size
533
+ ## The remainder of the chunk is a list of the entry values for that type/configuration
534
+ type_name = stringpool_typestrings.values[type_id - 1]
535
+ if current_spec.types == nil
536
+ current_spec.types = ResType.new(header, type_name, type_config, type_entrycount, Array.new())
537
+ end
538
+
539
+ i=0
540
+ while i < type_entrycount
541
+ ## Ensure a hash exists for each type
542
+ if current_spec.types.entries[i] == nil
543
+ current_spec.types.entries[i] = Hash.new()
544
+ end
545
+ current_entry = current_spec.types.entries[i]
546
+
547
+ ## Get the start of the type from the offsets table
548
+ index_offset = i * 4 + offset_table_start
549
+ start_offset = read_word(data, index_offset)
550
+ if start_offset != OFFSET_NO_ENTRY
551
+ ## Set the index_offset to the start of the current entry
552
+ index_offset = current + type_entryoffset + start_offset
553
+
554
+ entry_flags = read_short(data, index_offset+2)
555
+ entry_key = read_word(data, index_offset+4)
556
+ entry_data_type = read_byte(data, index_offset+11)
557
+ entry_data = read_word(data, index_offset+12)
558
+
559
+ # Find the key in our strings index
560
+ key_name = stringpool_keystrings.values[entry_key]
561
+ # Parse the value into a string
562
+ case entry_data_type
563
+ when TYPE_NULL
564
+ data_value = nil
565
+ when TYPE_REFERENCE
566
+ ## TODO: Mark these here, and resolve after package is parsed
567
+ data_value = res_id_to_s(entry_data)
568
+ when TYPE_STRING
569
+ data_value = get_resource_string(entry_data_type, entry_data)
570
+ when TYPE_INT_COLOR_ARGB8..TYPE_INT_COLOR_RGB4
571
+ data_value = get_resource_color(entry_data_type, entry_data)
572
+ when TYPE_DIMENSION
573
+ data_value = get_resource_dimension(entry_data_type, entry_data)
574
+ when TYPE_INT_DEC, TYPE_INT_HEX
575
+ data_value = get_resource_integer(entry_data_type, entry_data)
576
+ when TYPE_BOOL
577
+ data_value = get_resource_bool(entry_data_type, entry_data)
578
+ when TYPE_FLOAT
579
+ data_value = get_resource_float(entry_data_type, entry_data)
580
+ when TYPE_FRACTION
581
+ data_value = get_resource_fraction(entry_data_type, entry_data)
582
+ else
583
+ puts "Complex Resource (%s,%d) not yet supported." % [type_name,entry_data_type] if DEBUG
584
+ data_value = entry_data.to_s
585
+ end
586
+ current_entry[type_config] = ResTypeEntry.new(entry_flags, key_name, entry_data_type, data_value)
587
+ end
588
+ i += 1
589
+ end
590
+
591
+ current += header.chunk_size
592
+ elsif header.type == CHUNKTYPE_PACKAGE
593
+ ## This is the next package chunk, move along
594
+ puts "Next Package Chunk Found...Ending" if DEBUG
595
+ current = data.length
596
+ else
597
+ puts "Unknown Chunk Found: #{header.type} #{header.size}" if DEBUG
598
+ ## End Immediately
599
+ current = data.length
600
+ end
601
+ end
602
+
603
+ return Package.new(package_header, stringpool_typestrings, stringpool_keystrings, type_data)
604
+ end
605
+
606
+ # Obtain string value for resource id
607
+ def get_resource_string(entry_datatype, entry_data)
608
+ result = @stringpool_main.values[entry_data]
609
+ return result
610
+ end
611
+
612
+ # Obtain boolean value for resource id
613
+ def get_resource_bool(entry_datatype, entry_data)
614
+ if entry_data == TYPE_BOOL_TRUE
615
+ return "true"
616
+ elsif entry_data == TYPE_BOOL_FALSE
617
+ return "false"
618
+ else
619
+ return "undefined"
620
+ end
621
+ end
622
+
623
+ # Obtain integer value for resource id
624
+ def get_resource_integer(entry_datatype, entry_data)
625
+ if entry_datatype == TYPE_INT_HEX
626
+ return "0x#{entry_data.to_s(16)}"
627
+ else
628
+ return entry_data.to_s
629
+ end
630
+ end
631
+
632
+ # Obtain color value for resource id
633
+ def get_resource_color(entry_datatype, entry_data)
634
+ case entry_datatype
635
+ when TYPE_INT_COLOR_RGB4
636
+ return "#" + ((entry_data >> 16) & 0xF).to_s(16) + ((entry_data >> 8) & 0xF).to_s(16) + (entry_data & 0xF).to_s(16)
637
+ when TYPE_INT_COLOR_ARGB4
638
+ 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)
639
+ when TYPE_INT_COLOR_RGB8
640
+ return "#" + ((entry_data >> 16) & 0xFF).to_s(16) + ((entry_data >> 8) & 0xFF).to_s(16) + (entry_data & 0xFF).to_s(16)
641
+ when TYPE_INT_COLOR_ARGB8
642
+ 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)
643
+ else
644
+ return "0x#{entry_data.to_s(16)}"
645
+ end
646
+ end
647
+
648
+
649
+ # Obtain a float value for resource id
650
+ def get_resource_float(entry_data_type, entry_data)
651
+ result = [entry_data].pack('I').unpack('F')
652
+ return result[0].to_s
653
+ end
654
+
655
+ # Obtain dimension value for resource id
656
+ def get_resource_dimension(entry_datatype, entry_data)
657
+ unit_type = (entry_data & 0xF)
658
+ case unit_type
659
+ when COMPLEX_UNIT_PX
660
+ unit_name = "px"
661
+ when COMPLEX_UNIT_DIP
662
+ unit_name = "dp"
663
+ when COMPLEX_UNIT_SP
664
+ unit_name = "sp"
665
+ when COMPLEX_UNIT_PT
666
+ unit_name = "pt"
667
+ when COMPLEX_UNIT_IN
668
+ unit_name = "in"
669
+ when COMPLEX_UNIT_MM
670
+ unit_name = "mm"
671
+ else
672
+ unit_name = ""
673
+ end
674
+
675
+ return complex_to_float(entry_data).to_s + unit_name
676
+ #return ((entry_data >> 8) & 0xFFFFFF).to_s + unit_name
677
+ end
678
+
679
+ # Obtain a fraction value for resource id
680
+ def get_resource_fraction(entry_data_type, entry_data)
681
+ unit_type = (entry_data & 0xF)
682
+ case unit_type
683
+ when COMPLEX_UNIT_FRACTION
684
+ unit_name = "%"
685
+ when COMPLEX_UNIT_FRACTION_PARENT
686
+ unit_name = "%p"
687
+ else
688
+ unit_name = ""
689
+ end
690
+
691
+ # Return float as a percentage
692
+ return (complex_to_float(entry_data) * 100).to_s + unit_name
693
+ end
694
+
695
+ def complex_to_float(complex)
696
+ mantissa_mult = 1.0 / (1 << 8)
697
+ multipliers = [1.0*mantissa_mult, 1.0/(1<<7)*mantissa_mult, 1.0/(1<<15)*mantissa_mult, 1.0/(1<<23)*mantissa_mult]
698
+
699
+ mantissa = complex & 0xFFFFFF00
700
+ radix = (complex >> 4) & 0x3
701
+
702
+ return (mantissa * multipliers[radix]).to_f.round(4)
703
+ end
704
+ end