metasploit_data_models 0.18.1 → 0.19.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 (51) hide show
  1. checksums.yaml +8 -8
  2. data/app/models/mdm/host.rb +7 -0
  3. data/app/models/mdm/service.rb +30 -1
  4. data/app/models/mdm/tag.rb +10 -0
  5. data/app/models/metasploit_data_models/ip_address/v4/cidr.rb +14 -0
  6. data/app/models/metasploit_data_models/ip_address/v4/nmap.rb +14 -0
  7. data/app/models/metasploit_data_models/ip_address/v4/range.rb +12 -0
  8. data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list.rb +126 -0
  9. data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range.rb +12 -0
  10. data/app/models/metasploit_data_models/ip_address/v4/segment/single.rb +123 -0
  11. data/app/models/metasploit_data_models/ip_address/v4/segmented.rb +200 -0
  12. data/app/models/metasploit_data_models/ip_address/v4/single.rb +53 -0
  13. data/app/models/metasploit_data_models/search/operation/ip_address.rb +60 -0
  14. data/app/models/metasploit_data_models/search/operator/ip_address.rb +33 -0
  15. data/app/models/metasploit_data_models/search/visitor/attribute.rb +1 -0
  16. data/app/models/metasploit_data_models/search/visitor/includes.rb +1 -0
  17. data/app/models/metasploit_data_models/search/visitor/joins.rb +1 -0
  18. data/app/models/metasploit_data_models/search/visitor/where.rb +51 -0
  19. data/config/locales/en.yml +35 -4
  20. data/lib/metasploit_data_models/ip_address.rb +5 -0
  21. data/lib/metasploit_data_models/ip_address/cidr.rb +174 -0
  22. data/lib/metasploit_data_models/ip_address/range.rb +181 -0
  23. data/lib/metasploit_data_models/match/child.rb +48 -0
  24. data/lib/metasploit_data_models/match/parent.rb +103 -0
  25. data/lib/metasploit_data_models/version.rb +4 -4
  26. data/metasploit_data_models.gemspec +2 -1
  27. data/spec/app/models/mdm/cred_spec.rb +164 -31
  28. data/spec/app/models/mdm/service_spec.rb +33 -44
  29. data/spec/app/models/metasploit_data_models/ip_address/v4/cidr_spec.rb +121 -0
  30. data/spec/app/models/metasploit_data_models/ip_address/v4/nmap_spec.rb +151 -0
  31. data/spec/app/models/metasploit_data_models/ip_address/v4/range_spec.rb +300 -0
  32. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list_spec.rb +278 -0
  33. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range_spec.rb +304 -0
  34. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/segmented_spec.rb +29 -0
  35. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/single_spec.rb +315 -0
  36. data/spec/app/models/metasploit_data_models/ip_address/v4/single_spec.rb +183 -0
  37. data/spec/app/models/metasploit_data_models/search/operation/ip_address_spec.rb +182 -0
  38. data/spec/app/models/metasploit_data_models/search/operator/ip_address_spec.rb +19 -0
  39. data/spec/app/models/metasploit_data_models/search/visitor/relation_spec.rb +229 -36
  40. data/spec/dummy/config/application.rb +1 -1
  41. data/spec/dummy/db/structure.sql +3011 -0
  42. data/spec/factories/mdm/services.rb +3 -1
  43. data/spec/lib/metasploit_data_models/ip_address/cidr_spec.rb +350 -0
  44. data/spec/lib/metasploit_data_models/ip_address/range_spec.rb +77 -0
  45. data/spec/lib/metasploit_data_models/match/child_spec.rb +61 -0
  46. data/spec/lib/metasploit_data_models/match/parent_spec.rb +155 -0
  47. data/spec/support/matchers/match_regex_exactly.rb +28 -0
  48. data/spec/support/shared/contexts/rex/text.rb +15 -0
  49. data/spec/support/shared/examples/metasploit_data_models/search/operation/ipaddress/match.rb +109 -0
  50. metadata +58 -9
  51. data/spec/dummy/db/schema.rb +0 -609
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZmYwZGZmZjY0YTdlNzkxNzQyMTQzZTc0ZmNiNGI0NWMzZGFlYjg4Zg==
4
+ MTQ0Y2Q5YzA1YWYxNzViOTg0NGVjYTM1M2E5ZTE1NjJkNTJhZTc2Ng==
5
5
  data.tar.gz: !binary |-
6
- OWIxOTY3ZDJkMmJlNGM3NDMzNDJkYThjNzdmMDI2MDBhNDIyZGFkYg==
6
+ YTY1NTEwYWNlZTYzOGEyY2Y5NWU3MmMyOTIzMzBlZDQxNjE4NTNiZA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZGE3MGQ5YmQ3MTE1NDU0NDFjZDE0MDFmMDk5YmE1MDU3NGFhZjEwN2Y4YTcy
10
- YWFmYWQ2NmRiNWNjNTM4NzQzYmVhYzUyM2VlMTVjNTA4NTA3ODA3YWFiZTU3
11
- NDExMGFlNTM4N2QzM2Q4NTY2M2Y1NjE2NWU1NmYwODE0MTZmNDE=
9
+ MDhiZjg1M2FhNzg2ZjdiYmUyNzFlOWNhYWQ1YzYzOGM4ZmJiMjY3MzdlMjdk
10
+ NzE2ODcwM2JjNWM3MzllZTE0MTNiYzc5Y2VhMjE5OWVkMTQ2MGYwZDdkMmIy
11
+ OGFmZTI4YmY0YjExMzQzMTQxYWU1YzVmZDg0MmU3MTI2ZTBmOWY=
12
12
  data.tar.gz: !binary |-
13
- NzQ3Zjc0MGVhYjFjY2Y1YTEyNzYzZDFhZjJlOTJlOTNmZDVhOGFkZTA0ZWU5
14
- NjRlZTVlZjNjOTJlN2IyZjFmOWExZjg0YjY0NGRlZWE4OGQwNzYzZTNhYTk5
15
- NjI5ZTEwYzJkMWEwYzlkYTVhMTAxZDU4NjA4NDg2MzJjNDcyM2U=
13
+ M2ZlZmRmY2E4Y2E4NmQzZjc3MmFmOTZjYzJhMWNjYmM4YWU3ODNhMTNkODI2
14
+ ZTNhMTdjMGUzYmI1N2EzMDRmYWMzMzg5MjA1MWI1OTZiNjdkZjBjNzdlNmVm
15
+ YmM1NmYyMzU0YjQwNWQ1ZGRjZjE5OGQ4OGYzZTk3MDA5ZTZjYmI=
@@ -500,6 +500,13 @@ class Mdm::Host < ActiveRecord::Base
500
500
  :os_sp
501
501
  ]
502
502
 
503
+ #
504
+ # Search Withs
505
+ #
506
+
507
+ search_with MetasploitDataModels::Search::Operator::IPAddress,
508
+ attribute: :address
509
+
503
510
  #
504
511
  # Instance Methods
505
512
  #
@@ -6,6 +6,9 @@ class Mdm::Service < ActiveRecord::Base
6
6
  # CONSTANTS
7
7
  #
8
8
 
9
+ # Valid values for {#proto}.
10
+ PROTOS = %w{tcp udp}
11
+
9
12
  # Valid values for {#state}.
10
13
  STATES = ['open', 'closed', 'filtered', 'unknown']
11
14
 
@@ -186,8 +189,14 @@ class Mdm::Service < ActiveRecord::Base
186
189
  # Search Attributes
187
190
  #
188
191
 
192
+ search_attribute :info,
193
+ type: :string
189
194
  search_attribute :name,
190
195
  type: :string
196
+ search_attribute :proto,
197
+ type: {
198
+ set: :string
199
+ }
191
200
 
192
201
  #
193
202
  # Search Withs
@@ -202,7 +211,27 @@ class Mdm::Service < ActiveRecord::Base
202
211
  numericality: {
203
212
  only_integer: true
204
213
  }
205
- validates :proto, presence: true
214
+ validates :proto,
215
+ inclusion: {
216
+ in: PROTOS
217
+ }
218
+
219
+ #
220
+ # Class Methods
221
+ #
222
+
223
+ # Set of searchable values for {#proto}.
224
+ #
225
+ # @return [Set<String>] {PROTOS} as a `Set`.
226
+ # @see Metasploit::Model::Search::Operation::Set#membership
227
+ # @see Metasploit::Model::Search::Operator::Attribute#attribute_set
228
+ def self.proto_set
229
+ @proto_set ||= Set.new(PROTOS)
230
+ end
231
+
232
+ #
233
+ # Instance Methods
234
+ #
206
235
 
207
236
  # {Mdm::Host::OperatingSystemNormalization#normalize_os Normalizes the host operating system} whenever {#info} has
208
237
  # changed.
@@ -1,4 +1,6 @@
1
1
  class Mdm::Tag < ActiveRecord::Base
2
+ include Metasploit::Model::Search
3
+
2
4
  #
3
5
  # Relations
4
6
  #
@@ -26,6 +28,14 @@ class Mdm::Tag < ActiveRecord::Base
26
28
  # @return [ActiveRecord::Relation<Mdm::Host>]
27
29
  has_many :hosts, :through => :hosts_tags, :class_name => 'Mdm::Host'
28
30
 
31
+ #
32
+ # Search
33
+ #
34
+
35
+ search_attribute :desc,
36
+ type: :string
37
+ search_attribute :name,
38
+ type: :string
29
39
 
30
40
  #
31
41
  # Validations
@@ -0,0 +1,14 @@
1
+ # An IPv4 CIDR (Classless InterDomain Routing) block composed of a
2
+ # {MetasploitDataModels::IPAddress::V4::Single IPv4} {MetasploitDataModels::IPAddress::CIDR#address address} and
3
+ # {MetasploitDataModels::IPAddress::CIDR#prefix_length prefix_length} written in the form `'a.b.c.d/prefix_length'`.
4
+ #
5
+ # @see https://en.wikipedia.org/wiki/Cidr#IPv6_CIDR_blocks
6
+ class MetasploitDataModels::IPAddress::V4::CIDR < Metasploit::Model::Base
7
+ include MetasploitDataModels::IPAddress::CIDR
8
+
9
+ #
10
+ # CIDR
11
+ #
12
+
13
+ cidr address_class: MetasploitDataModels::IPAddress::V4::Single
14
+ end
@@ -0,0 +1,14 @@
1
+ # Nmap's octet range format composed of segments of comma separated list of segment numbers and segment number ranges.
2
+ #
3
+ # @example Nmap octect range format
4
+ # # equivalent to ['1.5.6.7', '3.5.6.7', '4.5.6.7']
5
+ # '1,3-4.5.6.7'
6
+ #
7
+ # @see http://nmap.org/book/man-target-specification.html
8
+ class MetasploitDataModels::IPAddress::V4::Nmap < MetasploitDataModels::IPAddress::V4::Segmented
9
+ #
10
+ # Segments
11
+ #
12
+
13
+ segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Nmap::List'
14
+ end
@@ -0,0 +1,12 @@
1
+ # A range of complete IPv4 addresses, separated by a `-`.
2
+ class MetasploitDataModels::IPAddress::V4::Range < Metasploit::Model::Base
3
+ extend MetasploitDataModels::Match::Child
4
+
5
+ include MetasploitDataModels::IPAddress::Range
6
+
7
+ #
8
+ # Range Extremes
9
+ #
10
+
11
+ extremes class_name: 'MetasploitDataModels::IPAddress::V4::Single'
12
+ end
@@ -0,0 +1,126 @@
1
+ # A comma separated list of {MetasploitDataModels::IPAddress::V4::Segment::Single segment numbers} and
2
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range range of segment numbers} making up one segment of
3
+ # {MetasploitDataModels::IPAddress::V4::Nmap}.
4
+ class MetasploitDataModels::IPAddress::V4::Segment::Nmap::List < Metasploit::Model::Base
5
+ extend ActiveSupport::Autoload
6
+
7
+ include MetasploitDataModels::Match::Parent
8
+
9
+ #
10
+ # CONSTANTS
11
+ #
12
+
13
+ # Either an individual {MetasploitDataModels::IPAddress::V4::Segment::Single segment number} or a
14
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range segment range}.
15
+ RANGE_OR_NUMBER_REGEXP = %r{
16
+ (?<range>#{parent::Range.regexp})
17
+ |
18
+ # range first because it contains a segment and if the range isn't first only the first part of the range will
19
+ # match.
20
+ (?<number>#{MetasploitDataModels::IPAddress::V4::Segment::Single::REGEXP})
21
+ }x
22
+ # Separator between number or ranges
23
+ SEPARATOR = ','
24
+ # Segment of an NMAP address, composed of comma separated {RANGE_OR_NUMBER_REGEXP segment numbers or ranges}.
25
+ REGEXP = /#{RANGE_OR_NUMBER_REGEXP}(#{SEPARATOR}#{RANGE_OR_NUMBER_REGEXP})*/
26
+
27
+ # Matches exactly an Nmap comma separated list of segment numbers and ranges.
28
+ MATCH_REGEXP = /\A#{REGEXP}\z/
29
+
30
+ #
31
+ # Attributes
32
+ #
33
+
34
+ # @!attribute value
35
+ # The NMAP IPv4 octect range.
36
+ #
37
+ # @return [Array<MetasploitDataModels::IPAddress::V4::Segment::Number, MetasploitDataModels::IPAddress::V4::Segment::Range>]
38
+ # number and range in the order they appeared in formatted value.
39
+ attr_reader :value
40
+
41
+ #
42
+ # Match Children
43
+ #
44
+
45
+ match_children_named %w{
46
+ MetasploitDataModels::IPAddress::V4::Segment::Single
47
+ MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range
48
+ }
49
+
50
+ #
51
+ #
52
+ # Validations
53
+ #
54
+ #
55
+
56
+ #
57
+ # Method Validations
58
+ #
59
+
60
+ validate :value_elements_valid
61
+ validate :value_is_array
62
+
63
+ #
64
+ # Attribute Validations
65
+ #
66
+
67
+ validates :value,
68
+ presence: true
69
+
70
+ #
71
+ # Instance Methods
72
+ #
73
+
74
+ # @return [String]
75
+ def to_s
76
+ if value.is_a? Array
77
+ value.map(&:to_s).join(SEPARATOR)
78
+ else
79
+ value.to_s
80
+ end
81
+ end
82
+
83
+ # Set {#value} to an `Array` of segment numbers and ranges.
84
+ #
85
+ # @param formatted_value [#to_s]
86
+ # @return [Array<MetasploitDataModels::IPAddress::V4::Segment::Single, MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range>] a parsed `Array` of segment numbers and ranges.
87
+ # @return [#to_s] if `formatted_value` does not match {MATCH_REGEXP}.
88
+ def value=(formatted_value)
89
+ string = formatted_value.to_s
90
+ match = MATCH_REGEXP.match(string)
91
+
92
+ if match
93
+ ranges_or_numbers = string.split(SEPARATOR)
94
+
95
+ @value = ranges_or_numbers.map { |range_or_number|
96
+ match_child(range_or_number) || range_or_number
97
+ }
98
+ else
99
+ @value = formatted_value
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Validates that {#value}'s elements are all valid.
106
+ #
107
+ # @return [void]
108
+ def value_elements_valid
109
+ if value.is_a? Array
110
+ value.each_with_index do |element, index|
111
+ unless element.valid?
112
+ errors.add(:value, :element, element: element, index: index)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Validates that {#value} is an `Array`.
119
+ #
120
+ # @return [void]
121
+ def value_is_array
122
+ unless value.is_a? Array
123
+ errors.add(:value, :array)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,12 @@
1
+ # A range of segment number composed of a {#begin} and {#end} segment number, separated by a `-`.
2
+ class MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range < Metasploit::Model::Base
3
+ extend MetasploitDataModels::Match::Child
4
+
5
+ include MetasploitDataModels::IPAddress::Range
6
+
7
+ #
8
+ # Range Extremes
9
+ #
10
+
11
+ extremes class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
12
+ end
@@ -0,0 +1,123 @@
1
+ # A segment number in an IPv4 address or the
2
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range#begin} or
3
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range#send}.
4
+ class MetasploitDataModels::IPAddress::V4::Segment::Single < Metasploit::Model::Base
5
+ extend MetasploitDataModels::Match::Child
6
+
7
+ include Comparable
8
+
9
+ #
10
+ # CONSTANTS
11
+ #
12
+
13
+ # Number of bits in a IPv4 segment
14
+ BITS = 8
15
+
16
+ # Limit that {#value} can never reach
17
+ LIMIT = 1 << BITS
18
+
19
+ # Maximum segment {#value}
20
+ MAXIMUM = LIMIT - 1
21
+
22
+ # Minimum segment {#value}
23
+ MINIMUM = 0
24
+
25
+ # Regular expression for a segment (octet) of an IPv4 address in decimal dotted notation.
26
+ #
27
+ # @see http://stackoverflow.com/a/17871737/470451
28
+ REGEXP = /(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])/
29
+
30
+ #
31
+ # Attributes
32
+ #
33
+
34
+ # @!attribute value
35
+ # The segment number.
36
+ #
37
+ # @return [Integer, String]
38
+ attr_reader :value
39
+
40
+ #
41
+ # Validations
42
+ #
43
+
44
+ validates :value,
45
+ numericality: {
46
+ greater_than_or_equal_to: MINIMUM,
47
+ less_than_or_equal_to: MAXIMUM,
48
+ only_integer: true
49
+ }
50
+
51
+ #
52
+ # Class Methods
53
+ #
54
+
55
+ # (see BITS)
56
+ #
57
+ # @return [Integer] {BITS}
58
+ def self.bits
59
+ BITS
60
+ end
61
+
62
+ #
63
+ # Instance Methods
64
+ #
65
+
66
+ # Compare this segment to `other`.
67
+ #
68
+ # @param other [#value] another segent to compare against.
69
+ # @return [1] if this segment is greater than `other`.
70
+ # @return [0] if this segment is equal to `other`.
71
+ # @return [-1] if this segment is less than `other`.
72
+ def <=>(other)
73
+ value <=> other.value
74
+ end
75
+
76
+ # Full add (as in [full adder](https://en.wikipedia.org/wiki/Full_adder)) two (this segment and `other`) segments and
77
+ # a carry from the previous {#add_with_carry}.
78
+ #
79
+ # @param other [MetasploitDataModels:IPAddress::V4::Segment::Single] segment to add to this segment.
80
+ # @param carry [Integer] integer to add to this segment and other segment from a previous call to {#add_with_carry}
81
+ # for lower segments.
82
+ # @return [Array<(MetasploitDataModels::IPAddress::V4::Segment::Single, Integer)>] Array containing a proper segment
83
+ # (where {#value} is less than {LIMIT}) and a carry integer to pass to next call to {#add_with_carry}.
84
+ # @return (see #half_add)
85
+ def add_with_carry(other, carry=0)
86
+ improper_value = self.value + other.value + carry
87
+ proper_value = improper_value % LIMIT
88
+ carry = improper_value / LIMIT
89
+ segment = self.class.new(value: proper_value)
90
+
91
+ [segment, carry]
92
+ end
93
+
94
+ # The succeeding segment. Used in `Range`s when walking the `Range`.
95
+ #
96
+ # @return [MetasploitDataModels::IPAddress::V4::Segment::Single] if {#value} responds to `#succ`.
97
+ # @return [nil] otherwise
98
+ def succ
99
+ if value.respond_to? :succ
100
+ self.class.new(value: value.succ)
101
+ end
102
+ end
103
+
104
+ delegate :to_s,
105
+ to: :value
106
+
107
+ # Sets {#value} by type casting String to Integer.
108
+ #
109
+ # @param formatted_value [#to_s]
110
+ # @return [Integer] if `formatted_value` contains only an Integer#to_s
111
+ # @return [#to_s] `formatted_value` if it does not contain an Integer#to_s
112
+ def value=(formatted_value)
113
+ @value_before_type_cast = formatted_value
114
+
115
+ begin
116
+ # use Integer() instead of String#to_i as String#to_i will ignore trailing letters (i.e. '1two' -> 1) and turn all
117
+ # string without an integer in it to 0.
118
+ @value = Integer(formatted_value.to_s)
119
+ rescue ArgumentError
120
+ @value = formatted_value
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,200 @@
1
+ # @note {segment} must be called in subclasses to set the {segment_class_name}.
2
+ #
3
+ # An IPv4 address that is composed of {SEGMENT_COUNT 4} {#segments} separated by {SEPARATOR `'.'`}.
4
+ #
5
+ # @example Using single segments to make a single IPv4 address class
6
+ # class MetasploitDataModels::IPAddress::V4::Single < MetasploitDataModels::IPAddress::V4::Segmented
7
+ # #
8
+ # # Segments
9
+ # #
10
+ #
11
+ # segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
12
+ # end
13
+ #
14
+ class MetasploitDataModels::IPAddress::V4::Segmented < Metasploit::Model::Base
15
+ extend MetasploitDataModels::Match::Child
16
+
17
+ include Comparable
18
+
19
+ #
20
+ # CONSTANTS
21
+ #
22
+
23
+ # The number of {#segments}
24
+ SEGMENT_COUNT = 4
25
+ # Separator between segments
26
+ SEPARATOR = '.'
27
+
28
+ #
29
+ # Attributes
30
+ #
31
+
32
+ # @!attribute value
33
+ # Segments of IP address from high to low.
34
+ #
35
+ # @return [Array<MetasploitDataModels::IPAddress:V4::Segment::Nmap>]
36
+ attr_reader :value
37
+
38
+ #
39
+ #
40
+ # Validations
41
+ #
42
+ #
43
+
44
+ #
45
+ # Validation Methods
46
+ #
47
+
48
+ validate :segments_valid
49
+
50
+ #
51
+ # Attribute Validations
52
+ #
53
+
54
+ validates :segments,
55
+ length: {
56
+ is: SEGMENT_COUNT
57
+ }
58
+
59
+ #
60
+ # Class methods
61
+ #
62
+
63
+ # @note Call {segment} with the {segment_class_name} before calling this method, as it uses {segment_class} to look
64
+ # up the `REGEXP` of the {segment_class}.
65
+ #
66
+ # Regular expression that matches the part of a string that represents a IPv4 segmented IP address format.
67
+ #
68
+ # @return [Regexp]
69
+ def self.regexp
70
+ unless @regexp
71
+ separated_segment_count = SEGMENT_COUNT - 1
72
+
73
+ @regexp = %r{
74
+ (#{segment_class::REGEXP}#{Regexp.escape(SEPARATOR)}){#{separated_segment_count},#{separated_segment_count}}
75
+ #{segment_class::REGEXP}
76
+ }x
77
+ end
78
+
79
+ @regexp
80
+ end
81
+
82
+ # Sets up the {segment_class_name} for the subclass.
83
+ #
84
+ # @example Using {segment} to set {segment_class_name}
85
+ # segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
86
+ #
87
+ # @param options [Hash{Symbol => String}]
88
+ # @option options [String] :class_name a `Class#name` to use for {segment_class_name}.
89
+ # @return [void]
90
+ def self.segment(options={})
91
+ options.assert_valid_keys(:class_name)
92
+
93
+ @segment_class_name = options.fetch(:class_name)
94
+ end
95
+
96
+ # @note Call {segment} to set the {segment_class_name} before calling {segment_class}, which will attempt to
97
+ # String#constantize` {segment_class_name}.
98
+ #
99
+ # The `Class` used to parse each segment of the IPv4 address.
100
+ #
101
+ # @return [Class]
102
+ def self.segment_class
103
+ @segment_class = segment_class_name.constantize
104
+ end
105
+
106
+ # @note Call {segment} to set {segment_class_name}
107
+ #
108
+ # The name of {segment_class}
109
+ #
110
+ # @return [String] a `Class#name` for {segment_class}.
111
+ def self.segment_class_name
112
+ @segment_class_name
113
+ end
114
+
115
+ # (see SEGMENT_COUNT)
116
+ #
117
+ # @return [Integer]
118
+ def self.segment_count
119
+ SEGMENT_COUNT
120
+ end
121
+
122
+ #
123
+ # Instance methods
124
+ #
125
+
126
+ # Compare this segment IPv4 address to `other`.
127
+ #
128
+ # @return [1] if {#segments} are greater than {#segments} of `other`.
129
+ # @return [0] if {#segments} are equal to {#segments} of `other`.
130
+ # @return [-1] if {#segments} are less than {#segments} of `other`.
131
+ # @return [nil] if `other` isn't the same `Class`
132
+ def <=>(other)
133
+ if other.is_a? self.class
134
+ segments <=> other.segments
135
+ else
136
+ # The interface for <=> requires nil be returned if other is incomparable
137
+ nil
138
+ end
139
+ end
140
+
141
+ # Array of segments.
142
+ #
143
+ # @return [Array] if {#value} is an `Array`.
144
+ # @return [[]] if {#value} is not an `Array`.
145
+ def segments
146
+ if value.is_a? Array
147
+ value
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ # Set {#segments}.
154
+ #
155
+ # @param segments [Array] `Array` of {segment_class} instances
156
+ # @return [Array] `Array` of {segment_class} instances
157
+ def segments=(segments)
158
+ @value = segments
159
+ end
160
+
161
+ # Segments joined with {SEPARATOR}.
162
+ #
163
+ # @return [String]
164
+ def to_s
165
+ segments.map(&:to_s).join(SEPARATOR)
166
+ end
167
+
168
+ # @note Set {#segments} if value is not formatted, but already broken into an `Array` of {segment_class} instances.
169
+ #
170
+ # Sets {#value} by parsing its segments.
171
+ #
172
+ # @param formatted_value [#to_s]
173
+ def value=(formatted_value)
174
+ string = formatted_value.to_s
175
+ match = self.class.match_regexp.match(string)
176
+
177
+ if match
178
+ segments = string.split(SEPARATOR)
179
+
180
+ @value = segments.map { |segment|
181
+ self.class.segment_class.new(value: segment)
182
+ }
183
+ else
184
+ @value = formatted_value
185
+ end
186
+ end
187
+
188
+ private
189
+
190
+ # Validates that all segments in {#segments} are valid.
191
+ #
192
+ # @return [void]
193
+ def segments_valid
194
+ segments.each_with_index do |segment, index|
195
+ unless segment.valid?
196
+ errors.add(:segments, :segment_invalid, index: index, segment: segment)
197
+ end
198
+ end
199
+ end
200
+ end