pio 0.18.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +40 -344
- data/Rakefile +32 -0
- data/bin/_guard-core +16 -0
- data/bin/byebug +16 -0
- data/bin/cc-tddium-post-worker +16 -0
- data/bin/cdiff +16 -0
- data/bin/coderay +16 -0
- data/bin/colortab +16 -0
- data/bin/coveralls +16 -0
- data/bin/cucumber +16 -0
- data/bin/decolor +16 -0
- data/bin/flay +16 -0
- data/bin/flog +16 -0
- data/bin/guard +16 -0
- data/bin/htmldiff +16 -0
- data/bin/inch +16 -0
- data/bin/ldiff +16 -0
- data/bin/listen +16 -0
- data/bin/minitar +16 -0
- data/bin/pry +16 -0
- data/bin/rake +16 -0
- data/bin/reek +16 -0
- data/bin/relish +16 -0
- data/bin/restclient +16 -0
- data/bin/rspec +16 -0
- data/bin/rubocop +16 -0
- data/bin/ruby-parse +16 -0
- data/bin/ruby-rewrite +16 -0
- data/bin/ruby_parse +16 -0
- data/bin/ruby_parse_extract_error +16 -0
- data/bin/sparkr +16 -0
- data/bin/term_display +16 -0
- data/bin/term_mandel +16 -0
- data/bin/thor +16 -0
- data/bin/unparser +16 -0
- data/bin/yard +16 -0
- data/bin/yardoc +16 -0
- data/bin/yri +16 -0
- data/features/arp.feature +61 -0
- data/features/dhcp.feature +4 -0
- data/features/icmp.feature +130 -0
- data/features/lldp.feature +47 -0
- data/features/open_flow10/echo_reply.feature +95 -0
- data/features/open_flow10/echo_request.feature +95 -0
- data/features/open_flow10/exact_match.feature +36 -0
- data/features/{features_read.feature → open_flow10/features_reply.feature} +54 -17
- data/features/open_flow10/features_request.feature +79 -0
- data/features/{flow_mod_read.feature → open_flow10/flow_mod.feature} +16 -21
- data/features/open_flow10/hello.feature +79 -0
- data/features/open_flow10/packet_in.feature +58 -0
- data/features/{packet_out_read.feature → open_flow10/packet_out.feature} +4 -5
- data/features/open_flow10/port_status.feature +23 -0
- data/features/open_flow13/echo_reply.feature +115 -0
- data/features/open_flow13/echo_request.feature +115 -0
- data/features/open_flow13/hello.feature +74 -0
- data/features/packet_data/echo13_reply_body.raw +0 -0
- data/features/packet_data/echo13_reply_no_body.raw +0 -0
- data/features/packet_data/echo13_request_body.raw +0 -0
- data/features/packet_data/echo13_request_no_body.raw +0 -0
- data/features/packet_data/hello13_no_version_bitmap.raw +0 -0
- data/features/packet_data/hello13_version_bitmap.raw +0 -0
- data/features/packet_data/udp_no_payload.raw +0 -0
- data/features/packet_data/udp_with_payload.raw +0 -0
- data/features/step_definitions/packet_data_steps.rb +49 -29
- data/features/support/env.rb +3 -0
- data/features/{udp_read.feature → udp.feature} +3 -4
- data/lib/pio.rb +1 -0
- data/lib/pio/echo.rb +67 -0
- data/lib/pio/hello13.rb +111 -0
- data/lib/pio/open_flow/message.rb +2 -1
- data/lib/pio/open_flow/open_flow_header.rb +21 -38
- data/lib/pio/open_flow/transaction_id.rb +25 -0
- data/lib/pio/version.rb +1 -1
- data/pio.gemspec +9 -14
- data/spec/pio/flow_mod_spec.rb +1 -1
- data/spec/pio/hello13_spec.rb +114 -0
- metadata +182 -138
- data/examples/arp_new.rb +0 -16
- data/examples/arp_read.rb +0 -4
- data/examples/dhcp_new.rb +0 -34
- data/examples/dhcp_read.rb +0 -4
- data/examples/echo_new.rb +0 -9
- data/examples/echo_read.rb +0 -4
- data/examples/features_new.rb +0 -28
- data/examples/features_read.rb +0 -4
- data/examples/flow_mod_new.rb +0 -13
- data/examples/flow_mod_read.rb +0 -6
- data/examples/hello_new.rb +0 -4
- data/examples/hello_read.rb +0 -4
- data/examples/icmp_new.rb +0 -21
- data/examples/icmp_read.rb +0 -4
- data/examples/lldp_new.rb +0 -4
- data/examples/lldp_read.rb +0 -4
- data/examples/packet_in_new.rb +0 -17
- data/examples/packet_in_read.rb +0 -5
- data/examples/packet_out_new.rb +0 -18
- data/examples/packet_out_read.rb +0 -6
- data/features/arp_read.feature +0 -10
- data/features/dhcp_read.feature +0 -6
- data/features/echo_read.feature +0 -29
- data/features/exact_match.feature +0 -38
- data/features/hello_read.feature +0 -14
- data/features/icmp_read.feature +0 -55
- data/features/lldp_read.feature +0 -26
- data/features/packet_in_read.feature +0 -22
- data/features/port_status_read.feature +0 -24
- data/features/step_definitions/pending_steps.rb +0 -3
- data/spec/pio/echo/reply_spec.rb +0 -135
- data/spec/pio/echo/request_spec.rb +0 -137
- data/spec/pio/features/reply_spec.rb +0 -137
- data/spec/pio/features/request_spec.rb +0 -112
- data/spec/pio/hello_spec.rb +0 -106
- data/spec/pio/lldp_spec.rb +0 -244
- data/spec/pio/packet_in_spec.rb +0 -146
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,42 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@raw = path
|
|
7
|
-
when '.pcap'
|
|
8
|
-
@pcap = path
|
|
9
|
-
else
|
|
10
|
-
fail "Unsupported file extension: #{name}"
|
|
1
|
+
When(/^I try to create a packet with:$/) do |ruby_code|
|
|
2
|
+
begin
|
|
3
|
+
@result = Pio.module_eval(ruby_code)
|
|
4
|
+
rescue
|
|
5
|
+
@last_error = $ERROR_INFO
|
|
11
6
|
end
|
|
12
7
|
end
|
|
13
8
|
|
|
14
|
-
When(/^I try to
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
fail 'Packet data file is not specified.'
|
|
9
|
+
When(/^I try to create an OpenFlow message with:$/) do |ruby_code|
|
|
10
|
+
step 'I try to create a packet with:', ruby_code
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# rubocop:disable LineLength
|
|
14
|
+
When(/^I try to parse a file named "(.*?)\.raw" with "(.*?)" class$/) do |file_name, klass|
|
|
15
|
+
path = File.expand_path(File.join(__dir__, '..', 'packet_data', file_name + '.raw'))
|
|
16
|
+
raw_data = IO.read(path)
|
|
17
|
+
parser_klass = Pio.const_get(klass)
|
|
18
|
+
begin
|
|
19
|
+
@result = parser_klass.read(raw_data)
|
|
20
|
+
rescue
|
|
21
|
+
@last_error = $ERROR_INFO
|
|
28
22
|
end
|
|
29
23
|
end
|
|
24
|
+
# rubocop:enable LineLength
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
# rubocop:disable LineLength
|
|
27
|
+
When(/^I try to parse a file named "(.*?)\.pcap" with "(.*?)" class$/) do |file_name, klass|
|
|
28
|
+
path = File.expand_path(File.join(__dir__, '..', 'packet_data', file_name + '.pcap'))
|
|
29
|
+
pcap = Pio::Pcap::Frame.read(IO.read(path))
|
|
30
|
+
parser_klass = Pio.const_get(klass)
|
|
31
|
+
begin
|
|
32
|
+
@result = pcap.records.each_with_object([]) do |each, result|
|
|
33
|
+
result << parser_klass.read(each.data)
|
|
34
|
+
end
|
|
35
|
+
rescue
|
|
36
|
+
@last_error = $ERROR_INFO
|
|
37
|
+
end
|
|
33
38
|
end
|
|
39
|
+
# rubocop:enable LineLength
|
|
34
40
|
|
|
35
41
|
Then(/^it should finish successfully$/) do
|
|
36
|
-
|
|
42
|
+
expect(@last_error).to be_nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Then(/^it should fail with "([^"]*)", "([^"]*)"$/) do |error, message|
|
|
46
|
+
expect(@last_error.class.to_s).to eq(error)
|
|
47
|
+
expect(@last_error.message).to eq(message)
|
|
37
48
|
end
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
When(/^I create an exact match from "(.*?)"$/) do |file_name|
|
|
51
|
+
path = File.expand_path(File.join(__dir__, '..', 'packet_data', file_name))
|
|
52
|
+
@result = Pio::ExactMatch.new(Pio::PacketIn.read(IO.read(path)))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Then(/^the packet have the following fields and values:$/) do |table|
|
|
40
56
|
table.hashes.each do |each|
|
|
41
57
|
output = each['field'].split('.').inject(@result) do |memo, method|
|
|
42
58
|
memo.__send__(method)
|
|
@@ -45,8 +61,12 @@ Then(/^the parsed data have the following field and value:$/) do |table|
|
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
|
|
64
|
+
Then(/^the message have the following fields and values:$/) do |table|
|
|
65
|
+
step 'the packet have the following fields and values:', table
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
# rubocop:disable LineLength
|
|
49
|
-
Then(/^the
|
|
69
|
+
Then(/^the message \#(\d+) have the following fields and values:$/) do |index, table|
|
|
50
70
|
table.hashes.each do |each|
|
|
51
71
|
output = each['field'].split('.').inject(@result[index.to_i - 1]) do |memo, method|
|
|
52
72
|
memo.__send__(method)
|
data/features/support/env.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
Feature:
|
|
1
|
+
Feature: UDP
|
|
2
2
|
Scenario: dhcp.pcap
|
|
3
|
-
|
|
4
|
-
When I try to parse the file with "Udp" class
|
|
3
|
+
When I try to parse a file named "dhcp.pcap" with "Udp" class
|
|
5
4
|
Then it should finish successfully
|
|
6
|
-
And the
|
|
5
|
+
And the message #1 have the following fields and values:
|
|
7
6
|
| field | value |
|
|
8
7
|
| class | Pio::Udp |
|
|
9
8
|
| destination_mac | ff:ff:ff:ff:ff:ff |
|
data/lib/pio.rb
CHANGED
data/lib/pio/echo.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'forwardable'
|
|
1
2
|
require 'pio/open_flow'
|
|
2
3
|
|
|
3
4
|
module Pio
|
|
@@ -11,4 +12,70 @@ module Pio
|
|
|
11
12
|
class Reply; end
|
|
12
13
|
OpenFlow::Message.factory(Reply, OpenFlow::ECHO_REPLY)
|
|
13
14
|
end
|
|
15
|
+
|
|
16
|
+
module Echo13
|
|
17
|
+
# Base class of Echo Request and Reply.
|
|
18
|
+
class Message
|
|
19
|
+
def self.message_name
|
|
20
|
+
name.split('::')[1..-1].map { |each| each.gsub(/\d+$/, '') }.join(' ')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.read(raw_data)
|
|
24
|
+
allocate.tap do |message|
|
|
25
|
+
message.instance_variable_set(:@format,
|
|
26
|
+
const_get(:Format).read(raw_data))
|
|
27
|
+
end
|
|
28
|
+
rescue BinData::ValidityError
|
|
29
|
+
raise Pio::ParseError, "Invalid #{message_name} 1.3 message."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(user_attrs = {})
|
|
33
|
+
unknown_attrs = user_attrs.keys - [:transaction_id, :xid, :body]
|
|
34
|
+
unless unknown_attrs.empty?
|
|
35
|
+
fail "Unknown keyword: #{unknown_attrs.first}"
|
|
36
|
+
end
|
|
37
|
+
header_options = OpenFlowHeader::Options.parse(user_attrs)
|
|
38
|
+
@format =
|
|
39
|
+
self.class.const_get(:Format).new(open_flow_header: header_options,
|
|
40
|
+
body: user_attrs[:body])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def method_missing(method, *args, &block)
|
|
44
|
+
@format.__send__ method, *args, &block
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Base class of Echo Request and Reply Format
|
|
49
|
+
class Format < BinData::Record
|
|
50
|
+
extend Forwardable
|
|
51
|
+
|
|
52
|
+
def_delegators :open_flow_header, :ofp_version
|
|
53
|
+
def_delegators :open_flow_header, :message_type
|
|
54
|
+
def_delegators :open_flow_header, :message_length
|
|
55
|
+
def_delegators :open_flow_header, :transaction_id
|
|
56
|
+
def_delegator :open_flow_header, :transaction_id, :xid
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# OpenFlow 1.3 Echo Request message.
|
|
60
|
+
class Request < Message
|
|
61
|
+
# OpenFlow 1.3 Echo Request message format.
|
|
62
|
+
class Format < Echo13::Format
|
|
63
|
+
endian :big
|
|
64
|
+
open_flow_header :open_flow_header,
|
|
65
|
+
ofp_version_value: 4, message_type_value: 2
|
|
66
|
+
string :body, read_length: -> { message_length - 8 }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# OpenFlow 1.3 Echo Reply message.
|
|
71
|
+
class Reply < Message
|
|
72
|
+
# OpenFlow 1.3 Echo Request message format.
|
|
73
|
+
class Format < Echo13::Format
|
|
74
|
+
endian :big
|
|
75
|
+
open_flow_header :open_flow_header,
|
|
76
|
+
ofp_version_value: 4, message_type_value: 3
|
|
77
|
+
string :body, read_length: -> { message_length - 8 }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
14
81
|
end
|
data/lib/pio/hello13.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'bindata'
|
|
2
|
+
require 'forwardable'
|
|
3
|
+
require 'pio/open_flow'
|
|
4
|
+
require 'pio/parse_error'
|
|
5
|
+
|
|
6
|
+
module Pio
|
|
7
|
+
# OpenFlow 1.3 Hello message parser and generator
|
|
8
|
+
class Hello13
|
|
9
|
+
# ofp_hello_elem_header and value
|
|
10
|
+
class Element < BinData::Record
|
|
11
|
+
VERSION_BITMAP = 1
|
|
12
|
+
|
|
13
|
+
endian :big
|
|
14
|
+
|
|
15
|
+
uint16 :element_type
|
|
16
|
+
uint16 :element_length
|
|
17
|
+
choice :element_value, selection: :chooser do
|
|
18
|
+
string 'unknown', read_length: -> { element_length - 4 }
|
|
19
|
+
uint32 'version_bitmap'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def version_bitmap?
|
|
23
|
+
element_type == VERSION_BITMAP
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def chooser
|
|
29
|
+
version_bitmap? ? 'version_bitmap' : 'unknown'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# OpenFlow 1.3 Hello message body.
|
|
34
|
+
class Body < BinData::Record
|
|
35
|
+
array :elements, type: :element, read_until: :eof
|
|
36
|
+
|
|
37
|
+
def length
|
|
38
|
+
if elements.empty?
|
|
39
|
+
0
|
|
40
|
+
else
|
|
41
|
+
elements.length * 4 + 4
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# OpenFlow 1.3 Hello message format
|
|
47
|
+
class Format < BinData::Record
|
|
48
|
+
extend Forwardable
|
|
49
|
+
|
|
50
|
+
endian :big
|
|
51
|
+
|
|
52
|
+
open_flow_header :open_flow_header,
|
|
53
|
+
ofp_version_value: 4, message_type_value: 0
|
|
54
|
+
body :body
|
|
55
|
+
|
|
56
|
+
def_delegators :open_flow_header, :ofp_version
|
|
57
|
+
def_delegators :open_flow_header, :message_type
|
|
58
|
+
def_delegators :open_flow_header, :message_length
|
|
59
|
+
def_delegators :open_flow_header, :transaction_id
|
|
60
|
+
def_delegator :open_flow_header, :transaction_id, :xid
|
|
61
|
+
def_delegators :body, :elements
|
|
62
|
+
|
|
63
|
+
def supported_versions
|
|
64
|
+
supported_versions_list.map do |each|
|
|
65
|
+
"open_flow1#{each - 1}".to_sym
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
alias_method :to_binary, :to_binary_s
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def supported_versions_list
|
|
74
|
+
(1..32).each_with_object([]) do |each, result|
|
|
75
|
+
result << each if (version_bitmap >> each & 1) == 1
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def version_bitmap
|
|
80
|
+
bitmap = elements.find(&:version_bitmap?)
|
|
81
|
+
bitmap ? bitmap.element_value : 0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.read(raw_data)
|
|
86
|
+
allocate.tap do |message|
|
|
87
|
+
message.instance_variable_set(:@format, Format.read(raw_data))
|
|
88
|
+
end
|
|
89
|
+
rescue BinData::ValidityError
|
|
90
|
+
raise Pio::ParseError, 'Invalid Hello 1.3 message.'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def initialize(user_attrs = {})
|
|
94
|
+
unknown_keywords = user_attrs.keys - [:transaction_id, :xid]
|
|
95
|
+
unless unknown_keywords.empty?
|
|
96
|
+
fail "Unknown keyword: #{unknown_keywords.first}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
header_attrs = OpenFlowHeader::Options.parse(user_attrs)
|
|
100
|
+
body_attrs = { elements: [{ element_type: 1,
|
|
101
|
+
element_length: 8,
|
|
102
|
+
element_value: 16 }] }
|
|
103
|
+
@format = Format.new(open_flow_header: header_attrs,
|
|
104
|
+
body: body_attrs)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def method_missing(method, *args, &block)
|
|
108
|
+
@format.__send__ method, *args, &block
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -1,46 +1,29 @@
|
|
|
1
1
|
require 'bindata'
|
|
2
|
+
require 'pio/open_flow/transaction_id'
|
|
2
3
|
|
|
3
4
|
module Pio
|
|
4
|
-
# OpenFlow
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class OpenFlowHeader < BinData::Record
|
|
8
|
-
# Transaction ID (uint32)
|
|
9
|
-
class TransactionId < BinData::Primitive
|
|
10
|
-
endian :big
|
|
11
|
-
uint32 :xid
|
|
5
|
+
# OpenFlow message header.
|
|
6
|
+
class OpenFlowHeader < BinData::Record
|
|
7
|
+
endian :big
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
9
|
+
uint8 :ofp_version, value: :ofp_version_value
|
|
10
|
+
virtual assert: -> { ofp_version == ofp_version_value }
|
|
11
|
+
uint8 :message_type, value: :message_type_value
|
|
12
|
+
virtual assert: -> { message_type == message_type_value }
|
|
13
|
+
uint16 :message_length, initial_value: -> { 8 + body.length }
|
|
14
|
+
transaction_id :transaction_id, initial_value: 0
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# parse header options
|
|
33
|
-
class Options
|
|
34
|
-
def self.parse(options)
|
|
35
|
-
xid = if options.respond_to?(:to_i)
|
|
36
|
-
options.to_i
|
|
37
|
-
elsif options.respond_to?(:fetch)
|
|
38
|
-
options[:transaction_id] || options[:xid] || 0
|
|
39
|
-
else
|
|
40
|
-
fail TypeError
|
|
41
|
-
end
|
|
42
|
-
{ transaction_id: xid }
|
|
43
|
-
end
|
|
16
|
+
# parse header options
|
|
17
|
+
class Options
|
|
18
|
+
def self.parse(options)
|
|
19
|
+
xid = if options.respond_to?(:to_i)
|
|
20
|
+
options.to_i
|
|
21
|
+
elsif options.respond_to?(:fetch)
|
|
22
|
+
options[:transaction_id] || options[:xid] || 0
|
|
23
|
+
else
|
|
24
|
+
fail TypeError
|
|
25
|
+
end
|
|
26
|
+
{ transaction_id: xid }
|
|
44
27
|
end
|
|
45
28
|
end
|
|
46
29
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'bindata'
|
|
2
|
+
require 'pio/monkey_patch/integer'
|
|
3
|
+
|
|
4
|
+
module Pio
|
|
5
|
+
module OpenFlow
|
|
6
|
+
# Transaction ID (uint32)
|
|
7
|
+
class TransactionId < BinData::Primitive
|
|
8
|
+
endian :big
|
|
9
|
+
|
|
10
|
+
uint32 :xid
|
|
11
|
+
|
|
12
|
+
def set(value)
|
|
13
|
+
unless value.unsigned_32bit?
|
|
14
|
+
fail(ArgumentError,
|
|
15
|
+
'Transaction ID should be an unsigned 32-bit integer.')
|
|
16
|
+
end
|
|
17
|
+
self.xid = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get
|
|
21
|
+
xid
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/pio/version.rb
CHANGED