ruby_apk 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format d
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem "rubyzip"
6
+
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem "rspec", "~> 2.11.0"
11
+ gem "bundler", "~> 1.1.5"
12
+ gem "jeweler", "~> 1.6.4"
13
+ gem "yard", require: false
14
+ gem "redcarpet"
15
+ gem "simplecov", require: false
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ git (1.2.5)
6
+ jeweler (1.6.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ multi_json (1.3.6)
11
+ rake (0.9.2.2)
12
+ redcarpet (2.2.2)
13
+ rspec (2.11.0)
14
+ rspec-core (~> 2.11.0)
15
+ rspec-expectations (~> 2.11.0)
16
+ rspec-mocks (~> 2.11.0)
17
+ rspec-core (2.11.1)
18
+ rspec-expectations (2.11.2)
19
+ diff-lcs (~> 1.1.3)
20
+ rspec-mocks (2.11.1)
21
+ rubyzip (0.9.9)
22
+ simplecov (0.6.4)
23
+ multi_json (~> 1.0)
24
+ simplecov-html (~> 0.5.3)
25
+ simplecov-html (0.5.3)
26
+ yard (0.8.2.1)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ bundler (~> 1.1.5)
33
+ jeweler (~> 1.6.4)
34
+ redcarpet
35
+ rspec (~> 2.11.0)
36
+ rubyzip
37
+ simplecov
38
+ yard
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2012 Securebrain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # ruby_apk
2
+ Android Apk static analysis library for Ruby.
3
+
4
+ ## Requirements
5
+ - ruby(>=1.9.x)
6
+ - rubyzip gem(>=0.9.9)
7
+
8
+ ## Install
9
+ $ gem install ruby_apk
10
+
11
+ ## Usage
12
+ ### Initialize
13
+ require 'ruby_apk'
14
+ apk = Android::Apk.new('sample.apk') # set apk file path
15
+
16
+ ### Apk
17
+ #### Listing files in Apk
18
+ # listing files in apk
19
+ apk = Android::Apk.new('sample.apk')
20
+ apk.each_file do |name, data|
21
+ puts "#{name}: #{data.size}bytes" # puts file name and data size
22
+ end
23
+
24
+ #### Find files in Apk
25
+ apk = Android::Apk.new('sample.apk')
26
+ elf_files = apk.find{|name, data| data[0..3] == [0x7f, 0x45, 0x4c, 0x46] } # ELF magic number
27
+
28
+ ### Manifest
29
+ #### Get readable xml
30
+ apk = Android::Apk.new('sample.apk')
31
+ manifest = apk.manifest
32
+ puts manifest.to_xml
33
+
34
+ #### Listing components and permissions
35
+ apk = Android::Apk.new('sample.apk')
36
+ manifest = apk.manifest
37
+ # listing components
38
+ manifest.components.each do |c| # 'c' is Android::Manifest::Component object
39
+ puts "#{c.type}: #{c.name}"
40
+ c.intent_filters.each do |filter|
41
+ puts "\t#{filter.type}"
42
+ end
43
+ end
44
+
45
+ # listing use-permission tag
46
+ manifest.use_permissions.each do |permission|
47
+ puts permission
48
+ end
49
+
50
+ ### Resource
51
+ #### Extract resource strings from apk
52
+ apk = Android::Apk.new('sample.apk')
53
+ rsc = apk.resource
54
+ rsc.strings.each do |str|
55
+ puts str
56
+ end
57
+
58
+ #### Parse resource file directly
59
+ rsc_data = File.open('resources.arsc', 'rb').read{|f| f.read }
60
+ rsc = Android::Resource.new(rsc_data)
61
+
62
+ ### Dex
63
+ #### Extract dex information
64
+ apk = Android::Apk.new('sample.apk')
65
+ dex = apk.dex
66
+ # listing string table in dex
67
+ dex.strings do |str|
68
+ puts str
69
+ end
70
+
71
+ # listing all class names
72
+ dex.classes do |cls| # cls is Android::Dex::ClassInfo
73
+ puts cls.name
74
+ end
75
+
76
+ #### Parse dex file directly
77
+ dex_data = File.open('classes.dex','rb').read{|f| f.read }
78
+ dex = Android::Dex.new(dex_data)
79
+
80
+
81
+ ## ChangeLog
82
+ ### 0.4.0
83
+ * add resource parser
84
+ * enhance dex parser
85
+
86
+ ### 0.3.0
87
+ * add and change name space
88
+ * add Android::Utils module and some util methods
89
+ * add Apk#entry, Apk#each_entry, and Apk#time methods,
90
+
91
+ ### 0.2.0
92
+ * update documents
93
+ * add Apk::Dex#each_strings, Apk::Dex#each_class_names
94
+
95
+ ### 0.1.2
96
+ * fix bug(improve android binary xml parser)
97
+
98
+ ### 0.1.1
99
+ * fix bug(failed to initialize Apk::Manifest::Meta class)
100
+ * replace iconv to String#encode(for ruby1.9)
101
+
102
+
103
+ ## Copyright
104
+
105
+ Copyright (c) 2012 SecureBrain. See LICENSE.txt for further details.
106
+
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "ruby_apk"
18
+ gem.homepage = "http://www.securebrain.co.jp/"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{static analysis tool for android apk}
21
+ gem.description = %Q{static analysis tool for android apk}
22
+ gem.email = "info@securebrain.co.jp"
23
+ gem.authors = ["SecureBrain"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+
35
+ task :default => :spec
36
+
37
+ require 'yard'
38
+ require 'yard/rake/yardoc_task'
39
+ YARD::Rake::YardocTask.new do |t|
40
+ t.files = ['lib/**/*.rb']
41
+ t.options = []
42
+ t.options << '--debug' << '--verbose' if $trace
43
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,155 @@
1
+ require 'zip/zip' # need rubyzip gem -> doc: http://rubyzip.sourceforge.net/
2
+ require 'digest/md5'
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+
6
+ module Android
7
+ class NotApkFileError < StandardError; end
8
+ class NotFoundError < StandardError; end
9
+
10
+ # apk object class
11
+ class Apk
12
+
13
+ # @return [String] apk file path
14
+ attr_reader :path
15
+ # @return [Android::Manifest] manifest instance
16
+ # @return [nil] when parsing manifest is failed.
17
+ attr_reader :manifest
18
+ # @return [Android::Dex] dex instance
19
+ # @return [nil] when parsing dex is failed.
20
+ attr_reader :dex
21
+ # @return [String] binary data of apk
22
+ attr_reader :bindata
23
+ # @return [Resource] resouce data
24
+ # @return [nil] when parsing resource is failed.
25
+ attr_reader :resource
26
+
27
+ # AndroidManifest file name
28
+ MANIFEST = 'AndroidManifest.xml'
29
+ # dex file name
30
+ DEX = 'classes.dex'
31
+ # resource file name
32
+ RESOURCE = 'resources.arsc'
33
+
34
+ # create new apk object
35
+ # @param [String] filepath apk file path
36
+ # @raise [Android::NotFoundError] path file does'nt exist
37
+ # @raise [Android::NotApkFileError] path file is not Apk file.
38
+ def initialize(filepath)
39
+ @path = filepath
40
+ raise NotFoundError, "'#{filepath}'" unless File.exist? @path
41
+ begin
42
+ @zip = Zip::ZipFile.open(@path)
43
+ rescue Zip::ZipError => e
44
+ raise NotApkFileError, e.message
45
+ end
46
+
47
+ @bindata = File.open(@path, 'rb') {|f| f.read }
48
+ @bindata.force_encoding(Encoding::ASCII_8BIT)
49
+ raise NotApkFileError, "manifest file is not found." if @zip.find_entry(MANIFEST).nil?
50
+ begin
51
+ @manifest = Android::Manifest.new(self.file(MANIFEST))
52
+ rescue => e
53
+ $stderr.puts "failed to parse manifest:#{e}"
54
+ #$stderr.puts e.backtrace
55
+ end
56
+ begin
57
+ @dex = Android::Dex.new(self.file(DEX))
58
+ rescue => e
59
+ $stderr.puts "failed to parse dex:#{e}"
60
+ #$stderr.puts e.backtrace
61
+ end
62
+ begin
63
+ @resource = Android::Resource.new(self.file(RESOURCE))
64
+ rescue => e
65
+ $stderr.puts "failed to parse resource:#{e}"
66
+ #$stderr.puts e.backtrace
67
+ end
68
+ end
69
+
70
+ # return apk file size
71
+ # @return [Integer] bytes
72
+ def size
73
+ @bindata.size
74
+ end
75
+
76
+ # return hex digest string of apk file
77
+ # @param [Symbol] type hash digest type(:sha1, sha256, :md5)
78
+ # @return [String] hex digest string
79
+ # @raise [ArgumentError] type is knknown type
80
+ def digest(type = :sha1)
81
+ case type
82
+ when :sha1
83
+ Digest::SHA1.hexdigest(@bindata)
84
+ when :sha256
85
+ Digest::SHA256.hexdigest(@bindata)
86
+ when :md5
87
+ Digest::MD5.hexdigest(@bindata)
88
+ else
89
+ raise ArgumentError
90
+ end
91
+ end
92
+
93
+ # returns date of AndroidManifest.xml as Apk date
94
+ # @return [Time]
95
+ def time
96
+ entry(MANIFEST).time
97
+ end
98
+
99
+ # @yield [name, data]
100
+ # @yieldparam [String] name file name in apk
101
+ # @yieldparam [String] data file data in apk
102
+ def each_file
103
+ @zip.each do |entry|
104
+ next unless entry.file?
105
+ yield entry.name, @zip.read(entry)
106
+ end
107
+ end
108
+
109
+ # find and return binary data with name
110
+ # @param [String] name file name in apk(fullpath)
111
+ # @return [String] binary data
112
+ # @raise [NotFoundError] when 'name' doesn't exist in the apk
113
+ def file(name) # get data by entry name(path)
114
+ @zip.read(entry(name))
115
+ end
116
+
117
+ # @yield [entry]
118
+ # @yieldparam [Zip::Entry] entry zip entry
119
+ def each_entry
120
+ @zip.each do |entry|
121
+ next unless entry.file?
122
+ yield entry
123
+ end
124
+ end
125
+
126
+ # find and return zip entry with name
127
+ # @param [String] name file name in apk(fullpath)
128
+ # @return [Zip::ZipEntry] zip entry object
129
+ # @raise [NotFoundError] when 'name' doesn't exist in the apk
130
+ def entry(name)
131
+ entry = @zip.find_entry(name)
132
+ raise NotFoundError, "'#{name}'" if entry.nil?
133
+ return entry
134
+ end
135
+
136
+ # find files which is matched with block condition
137
+ # @yield [name, data] find condition
138
+ # @yieldparam [String] name file name in apk
139
+ # @yieldparam [String] data file data in apk
140
+ # @yieldreturn [Array] Array of matched entry name
141
+ # @return [Array] Array of matched entry name
142
+ # @example
143
+ # apk = Apk.new(path)
144
+ # elf_files = apk.find { |name, data| data[0..3] == [0x7f, 0x45, 0x4c, 0x46] } # ELF magic number
145
+ def find(&block)
146
+ found = []
147
+ self.each_file do |name, data|
148
+ ret = block.call(name, data)
149
+ found << name if ret
150
+ end
151
+ found
152
+ end
153
+ end
154
+ end
155
+
@@ -0,0 +1,178 @@
1
+ require 'rexml/document'
2
+ require 'stringio'
3
+
4
+
5
+ module Android
6
+ # binary AXML parser
7
+ # @see https://android.googlesource.com/platform/frameworks/base.git Android OS frameworks source
8
+ # @note
9
+ # refer to Android OS framework code:
10
+ #
11
+ # /frameworks/base/include/androidfw/ResourceTypes.h,
12
+ #
13
+ # /frameworks/base/libs/androidfw/ResourceTypes.cpp
14
+ class AXMLParser
15
+ # axml parse error
16
+ class ReadError < StandardError; end
17
+
18
+ TAG_START_DOC = 0x00100100
19
+ TAG_END_DOC = 0x00100101
20
+ TAG_START = 0x00100102
21
+ TAG_END = 0x00100103
22
+ TAG_TEXT = 0x00100104
23
+ TAG_CDSECT = 0x00100105
24
+ TAG_ENTITY_REF= 0x00100106
25
+
26
+ VAL_TYPE_NULL =0
27
+ VAL_TYPE_REFERENCE =1
28
+ VAL_TYPE_ATTRIBUTE =2
29
+ VAL_TYPE_STRING =3
30
+ VAL_TYPE_FLOAT =4
31
+ VAL_TYPE_DIMENSION =5
32
+ VAL_TYPE_FRACTION =6
33
+ VAL_TYPE_INT_DEC =16
34
+ VAL_TYPE_INT_HEX =17
35
+ VAL_TYPE_INT_BOOLEAN =18
36
+ VAL_TYPE_INT_COLOR_ARGB8 =28
37
+ VAL_TYPE_INT_COLOR_RGB8 =29
38
+ VAL_TYPE_INT_COLOR_ARGB4 =30
39
+ VAL_TYPE_INT_COLOR_RGB4 =31
40
+
41
+ # @return [Array<String>] strings defined in axml
42
+ attr_reader :strings
43
+
44
+ # @param [String] axml binary xml data
45
+ def initialize(axml)
46
+ @io = StringIO.new(axml, "rb")
47
+ @strings = []
48
+ end
49
+
50
+ # parse binary xml
51
+ # @return [REXML::Document]
52
+ def parse
53
+ @doc = REXML::Document.new
54
+ @doc << REXML::XMLDecl.new
55
+
56
+ @num_str = word(4*4)
57
+ @xml_offset = word(3*4)
58
+
59
+ @parents = [@doc]
60
+ @ns = []
61
+ parse_strings
62
+ parse_tags
63
+ @doc
64
+ end
65
+
66
+
67
+ private
68
+ # read one word(4byte) as integer
69
+ # @param [Integer] offset offset from top position. current position is used if ofset is nil
70
+ # @return [Integer] little endian word value
71
+ def word(offset=nil)
72
+ @io.pos = offset unless offset.nil?
73
+ @io.read(4).unpack("V")[0]
74
+ end
75
+
76
+ # read 2byte as short integer
77
+ # @param [Integer] offset offset from top position. current position is used if ofset is nil
78
+ # @return [Integer] little endian unsign short value
79
+ def short(offset)
80
+ @io.pos = offset unless offset.nil?
81
+ @io.read(2).unpack("v")[0]
82
+ end
83
+
84
+ # parse string table
85
+ def parse_strings
86
+ sit_off = 0x24 # string index table offset
87
+ st_off = sit_off + @num_str * 4 # string table offset
88
+ @strings = []
89
+ @num_str.times do |i|
90
+ pos = st_off + word(sit_off + (4 * i)) # get position from string index table
91
+ len = short(pos) # read string length(not bytes)
92
+ str = @io.read(len*2) # read string(UTF-16LE)
93
+ str.force_encoding(Encoding::UTF_16LE)
94
+ @strings[i] = str.encode(Encoding::UTF_8)
95
+ end
96
+ end
97
+
98
+ # parse tag
99
+ def parse_tags
100
+ # skip until START_TAG
101
+ pos = @xml_offset
102
+ pos += 4 until (word(pos) == TAG_START) #ugh!
103
+ @io.pos -= 4
104
+
105
+ # read tags
106
+ #puts "start tag parse: %d(%#x)" % [@io.pos, @io.pos]
107
+ until @io.eof?
108
+ last_pos = @io.pos
109
+ tag, tag1, line, tag3, ns_id, name_id = @io.read(4*6).unpack("V*")
110
+ case tag
111
+ when TAG_START
112
+ tag6, num_attrs, tag8 = @io.read(4*3).unpack("V*")
113
+ elem = REXML::Element.new(@strings[name_id])
114
+ #puts "start tag %d(%#x): #{@strings[name_id]} attrs:#{num_attrs}" % [last_pos, last_pos]
115
+ @parents.last.add_element elem
116
+ num_attrs.times do
117
+ key, val = parse_attribute
118
+ elem.add_attribute(key, val)
119
+ end
120
+ @parents.push elem
121
+ when TAG_END
122
+ @parents.pop
123
+ when TAG_END_DOC
124
+ break
125
+ when TAG_TEXT
126
+ text = REXML::Text.new(@strings[ns_id])
127
+ @parents.last.text = text
128
+ dummy = @io.read(4*1).unpack("V*") # skip 4bytes
129
+ when TAG_START_DOC, TAG_CDSECT, TAG_ENTITY_REF
130
+ # not implemented yet.
131
+ else
132
+ raise ReadError, "pos=%d(%#x)[tag:%#x]" % [last_pos, last_pos, tag]
133
+ end
134
+ end
135
+ end
136
+
137
+ # parse attribute of a element
138
+ def parse_attribute
139
+ ns_id, name_id, val_str_id, flags, val = @io.read(4*5).unpack("V*")
140
+ key = @strings[name_id]
141
+ unless ns_id == 0xFFFFFFFF
142
+ ns = @strings[ns_id]
143
+ prefix = ns.sub(/.*\//,'')
144
+ unless @ns.include? ns
145
+ @ns << ns
146
+ @doc.root.add_namespace(prefix, ns)
147
+ end
148
+ key = "#{prefix}:#{key}"
149
+ end
150
+ value = convert_value(val_str_id, flags, val)
151
+ return key, value
152
+ end
153
+
154
+
155
+ def convert_value(val_str_id, flags, val)
156
+ unless val_str_id == 0xFFFFFFFF
157
+ value = @strings[val_str_id]
158
+ else
159
+ type = flags >> 24
160
+ case type
161
+ when VAL_TYPE_NULL
162
+ value = nil
163
+ when VAL_TYPE_REFERENCE
164
+ value = "@%#x" % val # refered resource id.
165
+ when VAL_TYPE_INT_DEC
166
+ value = val
167
+ when VAL_TYPE_INT_HEX
168
+ value = "%#x" % val
169
+ when VAL_TYPE_INT_BOOLEAN
170
+ value = val != 0xFFFFFFFE ? true : false # ugh! is it ok??
171
+ else
172
+ value = "[%#x, flag=%#x]" % [val, flags]
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ end
@@ -0,0 +1,74 @@
1
+
2
+ module Android
3
+ class Dex
4
+ # access flag object
5
+ class AccessFlag
6
+ # @return [Integer] flag value
7
+ attr_reader :flag
8
+ def initialize(flag)
9
+ @flag = flag
10
+ end
11
+ end
12
+
13
+ # access flag object for class in dex
14
+ class ClassAccessFlag < AccessFlag
15
+ ACCESSORS = [
16
+ {value:0x1, name:'public'},
17
+ {value:0x2, name:'private'},
18
+ {value:0x4, name:'protected'},
19
+ {value:0x8, name:'static'},
20
+ {value:0x10, name:'final'},
21
+ {value:0x20, name:'synchronized'},
22
+ {value:0x40, name:'volatile'},
23
+ {value:0x80, name:'transient'},
24
+ {value:0x100, name:'native'},
25
+ {value:0x200, name:'interface'},
26
+ {value:0x400, name:'abstract'},
27
+ {value:0x800, name:'strict'},
28
+ {value:0x1000, name:'synthetic'},
29
+ {value:0x2000, name:'annotation'},
30
+ {value:0x4000, name:'enum'},
31
+ #{value:0x8000, name:'unused'},
32
+ {value:0x10000, name:'constructor'},
33
+ {value:0x20000, name:'declared-synchronized'},
34
+ ]
35
+
36
+ # convert access flag to string
37
+ # @return [String]
38
+ def to_s
39
+ ACCESSORS.select{|e| ((e[:value] & @flag) != 0) }.map{|e| e[:name] }.join(' ')
40
+ end
41
+ end
42
+
43
+ # access flag object for method in dex
44
+ class MethodAccessFlag < AccessFlag
45
+ ACCESSORS = [
46
+ {value: 0x1, name:'public'},
47
+ {value: 0x2, name:'private'},
48
+ {value: 0x4, name:'protected'},
49
+ {value: 0x8, name:'static'},
50
+ {value: 0x10, name:'final'},
51
+ {value: 0x20, name:'synchronized'},
52
+ {value: 0x40, name:'bridge'},
53
+ {value: 0x80, name:'varargs'},
54
+ {value: 0x100, name:'native'},
55
+ {value: 0x200, name:'interface'},
56
+ {value: 0x400, name:'abstract'},
57
+ {value: 0x800, name:'strict'},
58
+ {value: 0x1000, name:'synthetic'},
59
+ {value: 0x2000, name:'annotation'},
60
+ {value: 0x4000, name:'enum'},
61
+ #{value: 0x8000, name:'unused'},
62
+ {value: 0x10000, name:'constructor'},
63
+ {value: 0x20000, name:'declared-synchronized'},
64
+ ]
65
+ # convert access flag to string
66
+ # @return [String]
67
+ def to_s
68
+ ACCESSORS.select{|e| ((e[:value] & @flag) != 0) }.map{|e| e[:name] }.join(' ')
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+