apktools 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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