ffi-pcap 0.2.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/.gitignore +10 -0
- data/ChangeLog.rdoc +27 -0
- data/LICENSE.txt +23 -0
- data/README.rdoc +30 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/examples/ipfw_divert.rb +49 -0
- data/examples/print_bytes.rb +17 -0
- data/lib/ffi-pcap.rb +1 -0
- data/lib/ffi/pcap.rb +42 -0
- data/lib/ffi/pcap/addr.rb +21 -0
- data/lib/ffi/pcap/bpf.rb +106 -0
- data/lib/ffi/pcap/bsd.rb +98 -0
- data/lib/ffi/pcap/capture_wrapper.rb +289 -0
- data/lib/ffi/pcap/common_wrapper.rb +175 -0
- data/lib/ffi/pcap/copy_handler.rb +38 -0
- data/lib/ffi/pcap/crt.rb +15 -0
- data/lib/ffi/pcap/data_link.rb +173 -0
- data/lib/ffi/pcap/dead.rb +37 -0
- data/lib/ffi/pcap/dumper.rb +55 -0
- data/lib/ffi/pcap/error_buffer.rb +44 -0
- data/lib/ffi/pcap/exceptions.rb +21 -0
- data/lib/ffi/pcap/file_header.rb +26 -0
- data/lib/ffi/pcap/in_addr.rb +9 -0
- data/lib/ffi/pcap/interface.rb +29 -0
- data/lib/ffi/pcap/live.rb +303 -0
- data/lib/ffi/pcap/offline.rb +53 -0
- data/lib/ffi/pcap/packet.rb +164 -0
- data/lib/ffi/pcap/packet_header.rb +24 -0
- data/lib/ffi/pcap/pcap.rb +252 -0
- data/lib/ffi/pcap/stat.rb +57 -0
- data/lib/ffi/pcap/time_val.rb +48 -0
- data/lib/ffi/pcap/typedefs.rb +27 -0
- data/lib/ffi/pcap/version.rb +6 -0
- data/spec/data_link_spec.rb +65 -0
- data/spec/dead_spec.rb +34 -0
- data/spec/dumps/http.pcap +0 -0
- data/spec/dumps/simple_tcp.pcap +0 -0
- data/spec/error_buffer_spec.rb +17 -0
- data/spec/file_header_spec.rb +28 -0
- data/spec/live_spec.rb +87 -0
- data/spec/offline_spec.rb +61 -0
- data/spec/packet_behaviors.rb +68 -0
- data/spec/packet_injection_spec.rb +38 -0
- data/spec/packet_spec.rb +111 -0
- data/spec/pcap_spec.rb +149 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/wrapper_behaviors.rb +124 -0
- data/tasks/rcov.rb +6 -0
- data/tasks/rdoc.rb +17 -0
- data/tasks/spec.rb +9 -0
- data/tasks/yard.rb +21 -0
- metadata +157 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
# Generic per-packet information, as supplied by libpcap. This structure
|
4
|
+
# is used to track only the libpcap header and just contains the timestamp
|
5
|
+
# and length information used by libpcap.
|
6
|
+
#
|
7
|
+
# See pcap_pkthdr struct in pcap.h
|
8
|
+
class PacketHeader < FFI::Struct
|
9
|
+
include FFI::DRY::StructHelper
|
10
|
+
|
11
|
+
dsl_layout do
|
12
|
+
struct :ts, ::FFI::PCap::TimeVal, :desc => 'time stamp'
|
13
|
+
field :caplen, :bpf_uint32, :desc => 'length of portion present'
|
14
|
+
field :len, :bpf_uint32, :desc => 'length of packet (off wire)'
|
15
|
+
end
|
16
|
+
|
17
|
+
alias timestamp ts
|
18
|
+
alias captured caplen
|
19
|
+
alias length len
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module PCap
|
5
|
+
DEFAULT_SNAPLEN = 65535 # Default snapshot length for packets
|
6
|
+
|
7
|
+
attach_function :pcap_lookupdev, [:pointer], :string
|
8
|
+
|
9
|
+
# Find the default device on which to capture.
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
# Name of default device
|
13
|
+
#
|
14
|
+
# @raise [LibError]
|
15
|
+
# On failure, an exception is raised with the relevant error
|
16
|
+
# message from libpcap.
|
17
|
+
#
|
18
|
+
def PCap.lookupdev
|
19
|
+
e = ErrorBuffer.create()
|
20
|
+
unless name = FFI::PCap.pcap_lookupdev(e)
|
21
|
+
raise(LibError, "pcap_lookupdev(): #{e.to_s}")
|
22
|
+
end
|
23
|
+
return name
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
attach_function :pcap_lookupnet, [:string, :pointer, :pointer, :pointer], :int
|
28
|
+
|
29
|
+
# Determine the IPv4 network number and mask relevant with a network
|
30
|
+
# device.
|
31
|
+
#
|
32
|
+
# @param [String] device
|
33
|
+
# The name of the device to look up.
|
34
|
+
#
|
35
|
+
# @yield [netp, maskp]
|
36
|
+
#
|
37
|
+
# @yieldparam [FFI::MemoryPointer] netp
|
38
|
+
# A pointer to the network return value.
|
39
|
+
#
|
40
|
+
# @yieldparam [FFI::MemoryPointer] maskp
|
41
|
+
# A pointer to the netmask return value.
|
42
|
+
#
|
43
|
+
# @return [nil, String]
|
44
|
+
# The IPv4 network number and mask presented as "n.n.n.n/m.m.m.m".
|
45
|
+
# nil is returned when a block is specified.
|
46
|
+
#
|
47
|
+
# @raise [LibError]
|
48
|
+
# On failure, an exception is raised with the relevant error message
|
49
|
+
# from libpcap.
|
50
|
+
#
|
51
|
+
def PCap.lookupnet(device)
|
52
|
+
netp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
|
53
|
+
maskp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
|
54
|
+
errbuf = ErrorBuffer.create()
|
55
|
+
unless FFI::PCap.pcap_lookupnet(device, netp, maskp, errbuf) == 0
|
56
|
+
raise(LibError, "pcap_lookupnet(): #{errbuf.to_s}")
|
57
|
+
end
|
58
|
+
if block_given?
|
59
|
+
yield(netp, maskp)
|
60
|
+
else
|
61
|
+
return( netp.get_array_of_uchar(0,4).join('.') << "/" <<
|
62
|
+
maskp.get_array_of_uchar(0,4).join('.') )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Opens a new Live device for capturing from the network. See Live.new()
|
68
|
+
# for arguments.
|
69
|
+
#
|
70
|
+
# If passed a block, the block is passed to Live.new() and the Live
|
71
|
+
# object is closed after completion of the block
|
72
|
+
def PCap.open_live(opts={},&block)
|
73
|
+
ret = Live.new(opts, &block)
|
74
|
+
return block_given? ? ret.close : ret
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Opens a new Dead pcap interface for compiling filters or opening
|
79
|
+
# a capture for output. See Dead.new() for arguments.
|
80
|
+
def PCap.open_dead(opts={}, &block)
|
81
|
+
ret = Dead.new(opts, &block)
|
82
|
+
return block_given? ? ret.close : ret
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Opens a saved capture file for reading. See Offline.new for arguments.
|
87
|
+
def PCap.open_offline(path, opts={}, &block)
|
88
|
+
ret = Offline.new(path, opts={}, &block)
|
89
|
+
return block_given? ? ret.close : ret
|
90
|
+
end
|
91
|
+
|
92
|
+
# Same as open_offline
|
93
|
+
def PCap.open_file(path, opts={}, &block)
|
94
|
+
open_offline(path, opts, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
attach_function :pcap_findalldevs, [:pointer, :pointer], :int
|
98
|
+
attach_function :pcap_freealldevs, [Interface], :void
|
99
|
+
|
100
|
+
# List all capture devices and yield them each to a block
|
101
|
+
#
|
102
|
+
# @yield [dev]
|
103
|
+
#
|
104
|
+
# @yieldparam [Interface] dev
|
105
|
+
# An Interface structure for each device.
|
106
|
+
#
|
107
|
+
# @return [nil]
|
108
|
+
#
|
109
|
+
# @raise [LibError]
|
110
|
+
# On failure, an exception is raised with the relevant error
|
111
|
+
# message from libpcap.
|
112
|
+
#
|
113
|
+
def PCap.each_device
|
114
|
+
devices = ::FFI::MemoryPointer.new(:pointer)
|
115
|
+
errbuf = ErrorBuffer.create()
|
116
|
+
|
117
|
+
FFI::PCap.pcap_findalldevs(devices, errbuf)
|
118
|
+
node = devices.get_pointer(0)
|
119
|
+
|
120
|
+
if node.null?
|
121
|
+
raise(LibError, "pcap_findalldevs(): #{errbuf.to_s}")
|
122
|
+
end
|
123
|
+
|
124
|
+
device = Interface.new(node)
|
125
|
+
|
126
|
+
while device
|
127
|
+
yield(device)
|
128
|
+
device = device.next
|
129
|
+
end
|
130
|
+
|
131
|
+
FFI::PCap.pcap_freealldevs(node)
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Returns an array of device name and network/netmask pairs for
|
137
|
+
# each interface found on the system.
|
138
|
+
#
|
139
|
+
# If an interface does not have an address assigned, its network/netmask
|
140
|
+
# value is returned as a nil value.
|
141
|
+
def PCap.dump_devices
|
142
|
+
FFI::PCap.enum_for(:each_device).map do |dev|
|
143
|
+
net = begin; FFI::PCap.lookupnet(dev.name); rescue LibError; end
|
144
|
+
[dev.name, net]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Returns an array of device names for each interface found on the system.
|
150
|
+
def PCap.device_names
|
151
|
+
FFI::PCap.enum_for(:each_device).map {|dev| dev.name }
|
152
|
+
end
|
153
|
+
|
154
|
+
attach_function :pcap_lib_version, [], :string
|
155
|
+
|
156
|
+
|
157
|
+
# Get the version information for libpcap.
|
158
|
+
#
|
159
|
+
# @return [String]
|
160
|
+
# Information about the version of the libpcap library being used;
|
161
|
+
# note that it contains more information than just a version number.
|
162
|
+
#
|
163
|
+
def PCap.lib_version
|
164
|
+
FFI::PCap.pcap_lib_version
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# Extract just the version number from the lib_version string.
|
169
|
+
#
|
170
|
+
# @return [String]
|
171
|
+
# Version number.
|
172
|
+
#
|
173
|
+
def PCap.lib_version_number
|
174
|
+
if lib_version() =~ /libpcap version (\d+\.\d+.\d+)/
|
175
|
+
return $1
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Unix Only:
|
181
|
+
begin
|
182
|
+
|
183
|
+
attach_function :pcap_get_selectable_fd, [:pcap_t], :int
|
184
|
+
|
185
|
+
|
186
|
+
# Drops privileges back to the uid of the SUDO_USER environment
|
187
|
+
# variable.
|
188
|
+
#
|
189
|
+
# Only available on Unix.
|
190
|
+
#
|
191
|
+
# This is useful for the paranoid when sudo is used to run a
|
192
|
+
# ruby pcap program as root.
|
193
|
+
#
|
194
|
+
# This method can generally be called right after a call to
|
195
|
+
# open_live() has returned a pcap handle or another privileged
|
196
|
+
# call has completed. Note, however, that once privileges are
|
197
|
+
# dropped, pcap functions that a require higher privilege will
|
198
|
+
# no longer work.
|
199
|
+
#
|
200
|
+
# @raise [StandardError]
|
201
|
+
# An error is raised if privileges cannot be dropped for
|
202
|
+
# some reason. This may be because the SUDO_USER environment
|
203
|
+
# variable is not set, because we already have a lower
|
204
|
+
# privilige and the SUDO_USER id is not the current uid,
|
205
|
+
# or because the SUDO_USER environment variable is not
|
206
|
+
# a valid user.
|
207
|
+
#
|
208
|
+
def PCap.drop_sudo_privs
|
209
|
+
if ENV["SUDO_USER"] and pwent=Etc.getpwnam(ENV["SUDO_USER"])
|
210
|
+
Process::Sys.setgid(pwent.gid)
|
211
|
+
Process::Sys.setegid(pwent.gid)
|
212
|
+
Process::Sys.setuid(pwent.uid)
|
213
|
+
Process::Sys.seteuid(pwent.uid)
|
214
|
+
return true if (
|
215
|
+
Process::Sys.getuid == pwent.uid and
|
216
|
+
Process::Sys.geteuid == pwent.uid and
|
217
|
+
Process::Sys.getgid == pwent.gid and
|
218
|
+
Process::Sys.getegid == pwent.gid )
|
219
|
+
end
|
220
|
+
raise(StandardError, "Unable to drop privileges")
|
221
|
+
end
|
222
|
+
|
223
|
+
rescue FFI::NotFoundError
|
224
|
+
$pcap_not_unix=true
|
225
|
+
end
|
226
|
+
|
227
|
+
# Win32 only:
|
228
|
+
begin
|
229
|
+
attach_function :pcap_setbuff, [:pcap_t, :int], :int
|
230
|
+
attach_function :pcap_setmode, [:pcap_t, :pcap_w32_modes_enum], :int
|
231
|
+
attach_function :pcap_setmintocopy, [:pcap_t, :int], :int
|
232
|
+
rescue FFI::NotFoundError
|
233
|
+
$pcap_not_win32=true
|
234
|
+
end if $pcap_not_unix
|
235
|
+
|
236
|
+
# MSDOS only???:
|
237
|
+
begin
|
238
|
+
attach_function :pcap_stats_ex, [:pcap_t, StatEx], :int
|
239
|
+
attach_function :pcap_set_wait, [:pcap_t, :pointer, :int], :void
|
240
|
+
attach_function :pcap_mac_packets, [], :ulong
|
241
|
+
rescue FFI::NotFoundError
|
242
|
+
end if $pcap_not_win32
|
243
|
+
|
244
|
+
|
245
|
+
#### XXX not sure if we even want FILE io stuff yet (or ever).
|
246
|
+
|
247
|
+
#attach_function :pcap_fopen_offline, [:FILE, :pointer], :pcap_t
|
248
|
+
#attach_function :pcap_file, [:pcap_t], :FILE
|
249
|
+
#attach_function :pcap_dump_fopen, [:pcap_t, :FILE], :pcap_dumper_t
|
250
|
+
#attach_function :pcap_fileno, [:pcap_t], :int
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
# As returned by pcap_stats()
|
4
|
+
#
|
5
|
+
# See pcap_stat struct in pcap.h.
|
6
|
+
class Stat < FFI::Struct
|
7
|
+
include FFI::DRY::StructHelper
|
8
|
+
|
9
|
+
dsl_layout do
|
10
|
+
field :ps_recv, :uint, :desc => "number of packets received"
|
11
|
+
field :ps_drop, :uint, :desc => "numer of packets dropped"
|
12
|
+
field :ps_ifdrop, :uint, :desc => "drops by interface (not yet supported)"
|
13
|
+
# bs_capt field intentionally left off (WIN32 only)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias received ps_recv
|
17
|
+
alias dropped ps_drop
|
18
|
+
alias interface_dropped ps_ifdrop
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
# As returned by pcap_stats_ex() (MSDOS only)
|
23
|
+
#
|
24
|
+
# See pcap_stat_ex struct in pcap.h
|
25
|
+
class StatEx < FFI::Struct
|
26
|
+
include FFI::DRY::StructHelper
|
27
|
+
|
28
|
+
dsl_layout do
|
29
|
+
field :rx_packets, :ulong, :desc => "total packets received"
|
30
|
+
field :tx_packets, :ulong, :desc => "total packets transmitted"
|
31
|
+
field :rx_bytes, :ulong, :desc => "total bytes received"
|
32
|
+
field :tx_bytes, :ulong, :desc => "total bytes transmitted"
|
33
|
+
field :rx_errors, :ulong, :desc => "bad packets received"
|
34
|
+
field :tx_errors, :ulong, :desc => "packet transmit problems"
|
35
|
+
field :rx_dropped, :ulong, :desc => "no space in Rx buffers"
|
36
|
+
field :tx_dropped, :ulong, :desc => "no space available for Tx"
|
37
|
+
field :multicast, :ulong, :desc => "multicast packets received"
|
38
|
+
field :collisions, :ulong
|
39
|
+
|
40
|
+
# detailed rx errors
|
41
|
+
field :rx_length_errors, :ulong
|
42
|
+
field :rx_over_errors, :ulong, :desc => "ring buff overflow"
|
43
|
+
field :rx_crc_errors, :ulong, :desc => "pkt with crc error"
|
44
|
+
field :rx_frame_errors, :ulong, :desc => "frame alignment errors"
|
45
|
+
field :rx_fifo_errors, :ulong, :desc => "fifo overrun"
|
46
|
+
field :rx_missed_errors, :ulong, :desc => "missed packet"
|
47
|
+
|
48
|
+
# detailed tx_errors
|
49
|
+
field :tx_aborted_errors, :ulong
|
50
|
+
field :tx_carrier_errors, :ulong
|
51
|
+
field :tx_fifo_errors, :ulong
|
52
|
+
field :tx_heartbeat_errors, :ulong
|
53
|
+
field :tx_window_errors, :ulong
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
class TimeVal < FFI::Struct
|
4
|
+
include FFI::DRY::StructHelper
|
5
|
+
|
6
|
+
dsl_layout do
|
7
|
+
field :tv_sec, :time_t
|
8
|
+
field :tv_usec, :suseconds_t
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
if args.size == 1 and (t=args[0]).kind_of?(Time)
|
13
|
+
self.time = t
|
14
|
+
else
|
15
|
+
super(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
alias sec tv_sec
|
20
|
+
alias usec tv_usec
|
21
|
+
|
22
|
+
# Returns the time value as a ruby Time object.
|
23
|
+
#
|
24
|
+
# @return [Time]
|
25
|
+
# A ruby time object derived from this TimeVal.
|
26
|
+
def time
|
27
|
+
Time.at(self.tv_sec, self.tv_usec)
|
28
|
+
end
|
29
|
+
|
30
|
+
alias to_time time
|
31
|
+
|
32
|
+
# Sets the time value from a ruby Time object
|
33
|
+
#
|
34
|
+
# @param [Time] t
|
35
|
+
# A ruby time object from which to set the time.
|
36
|
+
#
|
37
|
+
# @return [Time]
|
38
|
+
# Returns the same Time object supplied per convention.
|
39
|
+
#
|
40
|
+
def time=(t)
|
41
|
+
self.tv_sec = t.tv_sec
|
42
|
+
self.tv_usec = t.tv_usec
|
43
|
+
return t
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
typedef :pointer, :FILE
|
4
|
+
|
5
|
+
typedef :int, :bpf_int32
|
6
|
+
typedef :uint, :bpf_uint32
|
7
|
+
|
8
|
+
enum :pcap_direction_t, [
|
9
|
+
:pcap_d_inout,
|
10
|
+
:pcap_d_in,
|
11
|
+
:pcap_d_out
|
12
|
+
]
|
13
|
+
|
14
|
+
# For Win32-only pcap_setmode()
|
15
|
+
enum :pcap_w32_modes_enum, [ :capt, :stat, :mon ]
|
16
|
+
|
17
|
+
typedef :pointer, :pcap_t
|
18
|
+
typedef :pointer, :pcap_dumper_t
|
19
|
+
typedef :pointer, :pcap_addr_t
|
20
|
+
|
21
|
+
# add some of the more temperamental FFI types if needed
|
22
|
+
[ [:long, :time_t],
|
23
|
+
[:uint32, :suseconds_t],
|
24
|
+
].each {|t, d| begin; find_type(d); rescue TypeError; typedef t,d; end }
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataLink do
|
4
|
+
before(:all) do
|
5
|
+
@datalink = DataLink.new(0)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should map datalink names to datalink layer type values" do
|
9
|
+
DataLink.name_to_val(:en10mb).should == 1
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should map datalink layer type values to datalink names" do
|
13
|
+
DataLink.val_to_name(1).should == "EN10MB"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be initialized from a pcap datalink value" do
|
17
|
+
@datalink.name.should == 'NULL'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should support initialization from a pcap datalink name symbol" do
|
21
|
+
@datalink = DataLink.new(:null)
|
22
|
+
DataLink.should === @datalink
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should support initialization from a pcap datalink name string" do
|
26
|
+
dl = DataLink.new('en10mb')
|
27
|
+
DataLink.should === dl
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should allow equality comparison against numeric values" do
|
31
|
+
(@datalink == 0).should == true
|
32
|
+
(@datalink == 1).should == false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should allow equality comparison against String names" do
|
36
|
+
(@datalink == "null").should == true
|
37
|
+
(@datalink == "en10mb").should == false
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should allow equality comparison against Symbol names" do
|
41
|
+
(@datalink == :null).should == true
|
42
|
+
(@datalink == :en10mb).should == false
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should allow comparison against another DataLink" do
|
46
|
+
(@datalink == DataLink.new(0)).should == true
|
47
|
+
(@datalink == DataLink.new(1)).should == false
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should still compare correctly against any other object" do
|
51
|
+
(@datalink == Object.new).should == false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have a description" do
|
55
|
+
@datalink.description.should_not be_empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should convert to an Integer for the DLT value" do
|
59
|
+
@datalink.to_i.should == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should convert to a String for the DLT name" do
|
63
|
+
@datalink.to_s.should == 'NULL'
|
64
|
+
end
|
65
|
+
end
|