browserino 3.0.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3c6f36b794e0bff81d17dbf38d47cbed631945a6
4
- data.tar.gz: 98d2b7537b25b4a93de0a7f277ca0d92eae46d5d
3
+ metadata.gz: 1bdd2af440a3b1476900a3cdd36d2905660053c6
4
+ data.tar.gz: 0c7a2151e6ec79341ba5dda81eceddef2e8128ed
5
5
  SHA512:
6
- metadata.gz: c4b0dac61fd41e1dbf75d57592cc6f12aeadfb80212be8c84012fb12bff41e44a9f0cfa97407fad98fe72ca9c41ceefc134cb379986e13a0dbf3eec9daf70b30
7
- data.tar.gz: 6c785f94a902086de7f5479ee59f4f468fd60dc2fa79cb6ef9bfd1c366441f38688d1fe62628982b87728d92c25735f48f6fcee74d4a54ae9adbd9b3289a2650
6
+ metadata.gz: fb4d367fb7cfd3bd9cf637998de4a41549e18f20aaa0f14e007cb28883aff70b66e4beb75f757d99fe299b61a77db0c4322b90da74691e30677c524428a1c3eb
7
+ data.tar.gz: c13adf9ce004e08c5504b07367c5f362446ca06dbc6abe372872f703cca9694e8b3dd6f8fd7900970dea265a917e5d37ca9e951aef94176f241afaa4eba3f82c
data/.rubocop.yml CHANGED
@@ -7,3 +7,13 @@ AllCops:
7
7
 
8
8
  Documentation:
9
9
  Enabled: false
10
+
11
+ Lint/AmbiguousRegexpLiteral:
12
+ Enabled: false
13
+
14
+ Style/RegexpLiteral:
15
+ Enabled: false
16
+
17
+ Metrics/MethodLength:
18
+ CountComments: false
19
+ Max: 15
data/.travis.yml CHANGED
@@ -1,10 +1,9 @@
1
1
  language: ruby
2
- cache: bundler
3
2
  rvm:
4
- - 2.3.1
5
- - 2.3.0
6
- - 2.2.1
7
- - 2.1.0
8
3
  - 2.0.0
9
- before_install: gem install bundler -v 1.10.5
10
- script: 'bundle exec rspec spec'
4
+ - 2.1.0
5
+ - 2.2.0
6
+ - 2.3.0
7
+ - 2.4.0
8
+ before_install: gem install bundler
9
+ script: bundle exec rspec spec
data/bin/browserino CHANGED
@@ -1,60 +1,28 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  require 'bundler/setup'
3
+ require 'pry'
4
4
  require_relative '../lib/browserino'
5
5
 
6
- aliasses = [:fb, :ddg, :ff, :internet_explorer, :win, :osx, :bb]
6
+ props = Browserino.parse(ARGV[0].dup).properties
7
7
 
8
- case ARGV[0]
9
- when 'parse', '-p'
10
- if ARGV[1]
11
- puts Browserino.parse(ARGV[1]).to_console_s
12
- else
13
- puts ' Usage: browserino parse [ua]'
14
- puts ' * ua: the user agent string you want to parse'
15
- end
16
- when 'list', '-l'
17
- ARGV[1] ||= 'info'
18
- type = (ARGV[1].to_s.gsub(/[\s\-]/, '_')).to_sym
19
- if type == :info
20
- puts ' Usage: browserino list [subject]'
21
- puts ' * subject: the type you want to display'
22
- puts ' * all - list everything that is supported'
23
- puts ' * bots - list supported bots'
24
- puts ' * browsers - list supported browsers'
25
- puts ' * social-media - list supported social media'
26
- puts ' * search-engines - list supported search engines'
27
- puts ' * consoles - list supported consoles'
28
- puts ' * libraries - list supported libraries'
29
- puts ' * operating-systems - list supported operating systems'
30
- else
31
- res = {}
32
- res[:bots] = Browserino::Core::SUPPORTED[:bots] - aliasses if type == :all || type == :bots
33
- res[:browsers] = Browserino::Core::SUPPORTED[:browsers] - aliasses if type == :all || type == :browsers
34
- res[:consoles] = Browserino::Core::SUPPORTED[:consoles] - aliasses if type == :all || type == :consoles
35
- res[:libraries] = Browserino::Core::SUPPORTED[:libraries] - aliasses if type == :all || type == :libraries
36
- res[:social_media] = Browserino::Core::SUPPORTED[:social_media] - aliasses if type == :all || type == :social_media
37
- res[:search_engines] = Browserino::Core::SUPPORTED[:search_engines] - aliasses if type == :all || type == :search_engines
38
- res[:operating_systems] = Browserino::Core::SUPPORTED[:operating_systems] - aliasses if type == :all || type == :operating_systems
8
+ parsed = props.each_with_object({}) do |(prop, val), store|
9
+ store[prop] = val.to_s
10
+ end
39
11
 
40
- out = res.each_with_object([]) do |(type, supported_names), arr|
41
- arr << " \e[1;32;49m#{type.to_s.tr('_', ' ')}:\e[0m"
42
- supported_names.each { |name| arr << " \e[1;95;49m- #{name}\e[0m" }
43
- end
44
- puts out
45
- end
46
- when 'console', '-c'
47
- require 'pry'
48
- puts "Starting Browserino #{Browserino::VERSION} on Ruby #{RUBY_VERSION}"
49
- Pry.start
12
+ max_key_len = parsed.keys.map(&:size).max
13
+ max_val_len = parsed.values.map(&:size).max
14
+ spacing = 2
15
+ spacer = ' ' * spacing
50
16
 
51
- else
52
- puts ' Usage: browserino [command] [args]'
53
- puts "\n"
54
- puts ' Calling browserino with anything other than these options will show this help'
55
- puts ' Entering a [command] without [args] will show help for that command if it requires [args]'
56
- puts "\n"
57
- puts ' * parse or -p - parse a user agent'
58
- puts ' * list or -l - list supported entities'
59
- puts ' * console or -c - enter a console'
17
+ out = parsed.map do |pair|
18
+ key = [spacer, pair[0], spacer].join.ljust(max_key_len + (spacing * 2))
19
+ val = [spacer, pair[1], spacer].join.ljust(max_val_len + (spacing * 2))
20
+ tmp = [key, val].join '|'
21
+ "|#{tmp}|\n"
60
22
  end
23
+
24
+ row = '+' * (out.map(&:size).max - 1) + "\n"
25
+
26
+ out[0] = "#{row}#{out[0]}"
27
+ out[-1] = "#{out[-1]}#{row}"
28
+ puts out.join("#{row}")
data/bin/console CHANGED
@@ -1,15 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'browserino'
5
4
  require 'pry'
5
+ require 'browserino'
6
6
 
7
- ua = 'Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0'
8
- @agent = Browserino.parse(ua)
9
-
10
- puts "> @agent variable available parsed with: '#{ua}'\n\n"
11
- puts "> You can create a fresh object by using\n"
12
- puts "> Browserino.parse([string] ua)\n\n"
13
- puts "> ua - The user agent you'd actually want to parse as a string"
7
+ @client = Browserino.parse ARGV[0].dup || 'Mozilla/5.0 (Macintosh; Intel Mac\
8
+ OS X 10_11_2) AppleWebKit/601.3.9\
9
+ (KHTML, l ike Gecko) Version/9.0.\
10
+ 2 Safari/601.3.9'
14
11
 
15
12
  Pry.start
data/browserino.gemspec CHANGED
@@ -8,7 +8,6 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Browserino::VERSION
9
9
  spec.authors = ["Sidney Liebrand"]
10
10
  spec.email = ["sidneyliebrand@gmail.com"]
11
-
12
11
  spec.summary = %q{A browser identification gem with command line and Rails (>= 3.2.0) integration}
13
12
  spec.homepage = "https://sidofc.github.io/projects/browserino/"
14
13
  spec.license = "MIT"
@@ -18,12 +17,9 @@ Gem::Specification.new do |spec|
18
17
  spec.executables = ['browserino']
19
18
  spec.require_paths = ["lib"]
20
19
 
21
- spec.add_development_dependency "json"
22
- spec.add_development_dependency "bundler", "~> 1.10"
23
- spec.add_development_dependency "rake", "~> 10.0"
24
- spec.add_development_dependency "term-ansicolor"
20
+ spec.add_development_dependency "bundler", "~> 1.14"
21
+ spec.add_development_dependency "rake", "~> 12.0"
25
22
  spec.add_development_dependency "rspec"
26
- spec.add_development_dependency "tins"
27
23
  spec.add_development_dependency "coveralls"
28
24
  spec.add_development_dependency "pry"
29
25
  end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+ module Browserino
3
+ class Client
4
+ attr_reader :property_names
5
+
6
+ def initialize(props = {}, like = nil)
7
+ @property_names = props.is_a?(Hash) && props.keys || []
8
+ @like = like if like
9
+ # properties are added as methods that will each be defined in a specific
10
+ # order below. First, seperate static value methods from procs,
11
+ # procs will be able to call methods in this instances' context
12
+ # therefore we need to define static methods before procs
13
+
14
+ # for each static property:
15
+ # -- define a method that returns it's property value
16
+ # -- define a question method that returns it's property value or
17
+ # checks version against supplied value if truthy
18
+ define_simple_methods! props
19
+
20
+ # for each proc property:
21
+ # -- define a method that returns it's instance evalled block value
22
+ # -- define a question method that returns it's instance evalled block
23
+ # value or checks version against supplied value if truthy
24
+ define_proc_methods! props
25
+
26
+ # for each of #name, #engine and #platform, use their results as
27
+ # methods names, this will create a method #firefox? for the output
28
+ # of a #name # => :firefox for example.
29
+ define_name_result_methods!
30
+
31
+ # finally, add labels and their aliasses into the mix
32
+ # none of these methods should override an existing method
33
+ # therefore we check it's existence using defined?
34
+ define_label_methods!
35
+ end
36
+
37
+ def properties
38
+ @properties ||= @property_names.each_with_object({}) do |prop, result|
39
+ result[prop] = send prop
40
+ end
41
+ end
42
+
43
+ def like
44
+ @like ||= self
45
+ end
46
+
47
+ def like?(sym = nil, opts = {})
48
+ opts = sym if sym.is_a? Hash
49
+ must = []
50
+ must << like.is?(sym) if sym
51
+ must << like.version?(opts[:version]) if opts[:version]
52
+
53
+ must.any? && must.all?
54
+ end
55
+
56
+ def x64?
57
+ @x64_cache ||= architecture == :x64
58
+ end
59
+
60
+ def x32?
61
+ @x32_cache ||= architecture == :x32
62
+ end
63
+
64
+ # check the type of a Client
65
+ def type?(sym)
66
+ return type == sym if sym.is_a? Symbol
67
+ type == sym.to_sym
68
+ end
69
+
70
+ # the catch all method, anything it can ask as question will respond
71
+ # otherwise nil is returned, this is also true when supplying a version
72
+ def is?(sym, opts = {})
73
+ return send "#{sym}?", opts[:version] if opts[:version]
74
+ send "#{sym}?"
75
+ end
76
+
77
+ def ===(other)
78
+ return false unless name
79
+
80
+ case other
81
+ when Regexp then other =~ name
82
+ when String then other.to_sym == name
83
+ when Symbol, Browserino::Client then other == name
84
+ else false
85
+ end
86
+ end
87
+
88
+ def ==(other)
89
+ self === other
90
+ end
91
+
92
+ def =~(other)
93
+ self === other
94
+ end
95
+
96
+ def to_str
97
+ to_s
98
+ end
99
+
100
+ def to_json(*args)
101
+ @json_cache ||= properties.map do |prop, val|
102
+ final = val.is_a?(Version) && val.full || val
103
+ [prop, final]
104
+ end.to_h.to_json(*args)
105
+ end
106
+
107
+ def to_s
108
+ @str_cache ||= [:name, :engine, :platform].map do |prop|
109
+ name = send prop
110
+ ver = version if prop == :name
111
+ ver = send "#{prop}_version" if ver.nil?
112
+
113
+ [[name], [name, (ver.major if ver > '0.0.0')].compact.join.to_sym]
114
+ end.flatten.uniq.join(' ').gsub(/\s{2,}/, ' ').strip
115
+ end
116
+
117
+ def to_hash
118
+ properties
119
+ end
120
+
121
+ def to_h
122
+ properties
123
+ end
124
+
125
+ def to_a
126
+ properties.to_a
127
+ end
128
+
129
+ # scary, I know, but a falsy value is all we need to return if some
130
+ # property isn't known or true as any property can be defined on the Client
131
+ def method_missing(_, *__, &___)
132
+ nil
133
+ end
134
+
135
+ # always respond to missing, read method_missing comment
136
+ def respond_to_missing?(_, *__, &___)
137
+ true
138
+ end
139
+
140
+ private
141
+
142
+ def define_simple_methods!(props)
143
+ props.reject { |val| val.respond_to? :call }.each do |name, value|
144
+ define_singleton_method(name) { value }
145
+ define_singleton_method("#{name}?") do |val = nil, **opts|
146
+ values = [value, *Browserino.config.aliasses[value]]
147
+ return values.include? val if val && !opts[:version]
148
+ if val && opts[:version]
149
+ ver_res = send(name == :name ? :version : "#{name}_version")
150
+ return (ver_res == opts[:version]) && values.include?(val)
151
+ end
152
+ return value > 0 if value.is_a? Version
153
+ value && true
154
+ end
155
+ end
156
+ end
157
+
158
+ def define_proc_methods!(props)
159
+ props.select { |_, val| val.respond_to? :call }.each do |name, value|
160
+ result = instance_eval(&value)
161
+ define_singleton_method(name) { result }
162
+ end
163
+ end
164
+
165
+ def define_name_result_methods!
166
+ [:name, :engine, :platform].each do |prop|
167
+ result = send prop
168
+ ver_res = version if prop == :name
169
+ ver_res = send("#{prop}_version") if ver_res.nil?
170
+
171
+ # for each of the props:
172
+ # -- define a question method using the value of prop
173
+ # (ex: name # => firefox # => "firefox?")
174
+ # -- when supplied with a value, check it against {prop_res}_version
175
+ # -- when called without argument, return result
176
+ define_singleton_method("#{result}?") do |value = nil|
177
+ return ver_res == value if value
178
+ result && true
179
+ end
180
+
181
+ # for each of the aliasses found:
182
+ # -- define a question method using the current alias
183
+ # -- when supplied with a value, check it against {prop_res}_version
184
+ # -- when called without argument, return result
185
+ Browserino.config.aliasses[result].each do |alt|
186
+ define_singleton_method("#{alt}?") do |value = nil|
187
+ return ver_res == value if value
188
+ result && true
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def define_label_methods!
195
+ property_names.select { |name| name =~ /label/i }.each do |prop|
196
+ next unless (result = send(prop))
197
+
198
+ ver_type = prop.to_s.gsub /_?label/, ''
199
+ ver_res = version if ver_type.empty?
200
+ ver_res = send("#{ver_type}_version") unless prop == :label
201
+
202
+ define_singleton_method("#{result}?") do |value = nil|
203
+ return ver_res == value if value
204
+ result && true
205
+ end
206
+
207
+ Browserino.config.aliasses[result].each do |alt|
208
+ define_singleton_method("#{alt}?") do |value = nil|
209
+ return ver_res == value if value
210
+ result && true
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ module Browserino
3
+ class Config < Options
4
+ def define(&block)
5
+ instance_eval(&block)
6
+ return unless identities.any? && identities.all?
7
+
8
+ identities.each do |identity|
9
+ properties << identity.properties.keys
10
+ types << identity.type
11
+ names << identity.name
12
+ end
13
+
14
+ properties.flatten!.uniq!
15
+ types.uniq!
16
+ names.uniq!
17
+ end
18
+
19
+ def label(name, **opts)
20
+ return false unless opts[:for]
21
+ opts[:name] ||= name
22
+ labels[opts[:for]] << opts
23
+ end
24
+
25
+ def filter(*props, &block)
26
+ props << :global unless props.any?
27
+ props.each { |prop| filters[prop] << block }
28
+ end
29
+
30
+ def smart_match(prop, **options)
31
+ smart_matchers[prop] = options if options[:with]
32
+ end
33
+
34
+ def match(rgxp = nil, **opts, &block)
35
+ rgxp, opts = [nil, rgxp] if rgxp.is_a? Hash
36
+ opts = @tmp_defaults.merge opts if @tmp_defaults.is_a? Hash
37
+
38
+ if rgxp && opts[:like]
39
+ identities.unshift with_alias(rgxp, opts, &block)
40
+ elsif rgxp
41
+ identities << Identity.new(rgxp, opts, &block)
42
+ else
43
+ global_identities.unshift Identity.new(&block)
44
+ end
45
+ end
46
+
47
+ def alias_for(name, *names)
48
+ aliasses[name] += names
49
+ end
50
+
51
+ def before_parse(&block)
52
+ @options[:before_parse] << block if block
53
+ @options[:before_parse]
54
+ end
55
+
56
+ def preset(props, &block)
57
+ @tmp_defaults = props
58
+ instance_eval(&block)
59
+ @tmp_defaults = nil
60
+ end
61
+
62
+ def like(tmp, &block)
63
+ preset like: tmp.to_sym, &block
64
+ end
65
+
66
+ def validators(&block)
67
+ preset type: :validator, &block
68
+ end
69
+
70
+ def browsers(&block)
71
+ preset type: :browser, &block
72
+ end
73
+
74
+ def bots(&block)
75
+ preset type: :bot, &block
76
+ end
77
+
78
+ def libraries(&block)
79
+ preset type: :library, &block
80
+ end
81
+
82
+ def with_alias(pattern, **opts, &block)
83
+ id = identities.select { |id| id == opts[:like] }.first
84
+
85
+ raise "No alias found for: #{opts[:like] || 'nil'}" unless id
86
+
87
+ Identity.new pattern, id.properties.merge(opts), &block
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ Browserino.config.define do
3
+ # aliasses will be defined after a Client has been initialized
4
+ # only the aliasses matching the Client will be defined
5
+ alias_for :firefox, :ff
6
+ alias_for :windows, :win
7
+ alias_for :macintosh, :mac, :osx, :macos
8
+ alias_for :blackberry, :bb
9
+ alias_for :ie, :internet_explorer
10
+ alias_for :facebook, :fb
11
+ alias_for :duckduckgo, :ddg
12
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ Browserino.config.define do
3
+ # executes before parsing the user agent, 's' in this case is the actual
4
+ # user agent string in full glory. Do manipulations as you wish, I'm
5
+ # using it to successfully strip lies from the user agent and to sometimes
6
+ # simplify a word e.g. 'AppleWebKit' => 'webkit'
7
+
8
+ # the returned result will be used as the user agent to parse so make
9
+ # sure to return the final ua at the end
10
+ before_parse do |ua|
11
+ ua = ua.gsub(%r{applewebkit}i, 'webkit').gsub %r{(Mozilla/[\d\.]+)}i, ''
12
+ ua = ua.gsub(%r{chrome|safari}i, '').gsub('OPR', 'opera') if ua =~ %r{OPR}
13
+ ua = ua.gsub %r{9\.80}, '' if ua =~ %r{opera}i
14
+ ua = ua.gsub %r{webkit/}i, '' if ua =~ %r{presto}i
15
+ ua = ua.gsub %r{(?:ms)?ie}i, '' if ua =~ %r{rv:}i
16
+ ua = ua.gsub %r{android|linux}i, '' if ua =~ %r{tizen}i
17
+ ua = ua.gsub %r{linux}i, '' if ua =~ %r{android|s(unos|olaris)|w(eb)?os}i
18
+ ua = ua.gsub %r{x11}i, '' if ua =~ %r{bsd|s(unos|olaris)}i
19
+ ua = ua.gsub %r{windows\snt}i, '' if ua =~ %r{windows\sphone}i
20
+ ua = ua.gsub %r{rv:}i, '' if ua =~ %r{servo}i
21
+ ua = ua.gsub %r{mac\sos\sx}i, '' if ua =~ %r{ip(?:[ao]d|hone)}i
22
+ ua = ua.gsub %r{msie}i, '' if ua =~ %r{huaweisymantecspider}i
23
+ ua
24
+ end
25
+
26
+ # after an identity is found, it's values are filtered in two stages
27
+ # first, filters will parse all statically defined values (e.g.) no regexp
28
+ # or block within matchers, after that, smart matcher patterns will be
29
+ # conditionally added and parsed with the previously collected values and
30
+ # then also filtered
31
+
32
+ # this is a global filter, multiple can be defined and they will all run
33
+ # before any named filters
34
+ filter do |value|
35
+ case value
36
+ when TrueClass, FalseClass, NilClass, Proc then value
37
+ when %r{\A[\d_\.]+\z}i then value.to_s.strip.tr('_', '.')
38
+ else value.to_s.downcase.strip.gsub(%r{[\s-]+}i, '_').to_sym
39
+ end
40
+ end
41
+
42
+ # this is a named filter, it defines the same filter for 3 properties
43
+ # multiple name filters for the same property can be created, they will be
44
+ # executed in order of addition
45
+ filter :version, :engine_version, :platform_version do |value|
46
+ Browserino::Version.new value
47
+ end
48
+
49
+ filter :platform do |value|
50
+ value = :ios if value =~ %r{ip(?:[ao]d|hone)}i
51
+ value = :webos if value =~ %r{w(?:eb)?os}i
52
+ value = :linux if value =~ %r{ubuntu|x11}i
53
+ value = :solaris if value =~ %r{s(?:unos|olaris)}i
54
+ value = :macintosh if value =~ %r{mac_os_x}i
55
+ value
56
+ end
57
+
58
+ filter :architecture do |value|
59
+ value = :x64 if value && value =~ %r{(?:x86_|amd|wow)?64|i686}i
60
+ value = :x32 if value && value != :x64
61
+ value
62
+ end
63
+
64
+ filter :mobile do |value|
65
+ !value.to_s.strip.empty?
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ Browserino.config.define do
3
+ # labels will be defined before a client has been initialized
4
+ # they will not be filtered before being injected into the Client
5
+
6
+ # labels will be matched against values of #name, #engine and #platform and
7
+ # will create #label, #engine_label and #platform_label properties
8
+ # respectively. These enjoy all the features that regular defined properties
9
+ # in for instance methods enjoy, such as automatic question methods like
10
+ # client.platform_label? :mavericks
11
+ label :android, for: :android, range: '1'..'1.4.9'
12
+ label :cupcake, for: :android, range: '1.5'..'1.9.9'
13
+ label :eclair, for: :android, range: '2'..'2.1'
14
+ label :froyo, for: :android, range: '2.2'..'2.2.3'
15
+ label :gingerbread, for: :android, range: '2.2.4'..'2.3.7'
16
+ label :honeycomb, for: :android, range: '3'..'3.2.6'
17
+ label :ice_cream_sandwich, for: :android, range: '4'..'4.0.4'
18
+ label :jelly_bean, for: :android, range: '4.1'..'4.3.1'
19
+ label :kitkat, for: :android, range: '4.4'..'4.4.4'
20
+ label :lollipop, for: :android, range: '5'..'5.1.1'
21
+ label :marshmallow, for: :android, range: '6'..'6.1.1'
22
+ label :nougat, for: :android, range: '7'..'7.1.1'
23
+
24
+ label :cheetah, for: :macintosh, range: '10.0'..'10.0.9'
25
+ label :puma, for: :macintosh, range: '10.1'..'10.1.9'
26
+ label :jaguar, for: :macintosh, range: '10.2'..'10.2.9'
27
+ label :panther, for: :macintosh, range: '10.3'..'10.3.9'
28
+ label :tiger, for: :macintosh, range: '10.4'..'10.4.9'
29
+ label :leopard, for: :macintosh, range: '10.5'..'10.5.9'
30
+ label :snow_leopard, for: :macintosh, range: '10.6'..'10.6.9'
31
+ label :lion, for: :macintosh, range: '10.7'..'10.7.9'
32
+ label :mountain_lion, for: :macintosh, range: '10.8'..'10.8.9'
33
+ label :mavericks, for: :macintosh, range: '10.9'..'10.9.9'
34
+ label :yosemite, for: :macintosh, range: '10.10'..'10.10.9'
35
+ label :el_capitan, for: :macintosh, range: '10.11'..'10.11.9'
36
+ label :sierra, for: :macintosh, range: '10.12'..'10.12.9'
37
+
38
+ label :windows_dos, for: :windows, range: '3.1'..'4.0'
39
+ label :windows_2000, for: :windows, range: '5.0'..'5.0.9'
40
+ label :windows_xp, for: :windows, range: '5.1'..'5.2.9'
41
+ label :windows_vista, for: :windows, range: '6.0'..'6.0.9'
42
+ label :windows_7, for: :windows, range: '6.1'..'6.1.9'
43
+ label :windows_8, for: :windows, range: '6.2'..'6.3'
44
+ label :windows_10, for: :windows, range: '10.0'..'10.0.9'
45
+ end