xmodem 0.1.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 +7 -0
- data/.gitignore +5 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +470 -0
- data/README.md +45 -0
- data/Rakefile +25 -0
- data/doc/xmodem.txt +102 -0
- data/doc/xmodem1k.txt +109 -0
- data/doc/xmodmcrc.txt +139 -0
- data/doc/ymodem.txt +2112 -0
- data/lib/xmodem.rb +323 -0
- data/lib/xmodem/version.rb +3 -0
- data/test/test_xmodem.rb +280 -0
- data/xmodem.gemspec +34 -0
- metadata +181 -0
data/lib/xmodem.rb
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
# Author:: Jonno Downes (jonno@jamtronix.com)
|
2
|
+
# Contrib:: Sten Feldman (exile@chamber.ee)
|
3
|
+
#
|
4
|
+
# License:: Mozilla Public License 1.1
|
5
|
+
#
|
6
|
+
# Doesn't seem to work for me using Ruby 2.0 - BORKEN unless fixed
|
7
|
+
#
|
8
|
+
# Public interface changes:
|
9
|
+
# - ModemProtocols renamed to XMODEM
|
10
|
+
# - xmodem_tx renamed to send
|
11
|
+
# - xmodem_rx renamed to receive
|
12
|
+
# - Logger outputs reflect the actors sender/receiver
|
13
|
+
|
14
|
+
require 'log4r'
|
15
|
+
include Log4r
|
16
|
+
|
17
|
+
module XMODEM
|
18
|
+
|
19
|
+
XMODEM_BLOCK_SIZE = 128 #how many bytes (ecluding header & checksum) in each block?
|
20
|
+
XMODEM_MAX_TIMEOUTS = 5 #how many timeouts in a row before the sender gives up?
|
21
|
+
XMODEM_MAX_ERRORS = 10 #how many errors on a single block before the receiver gives up?
|
22
|
+
|
23
|
+
XMODEM_CRC_ATTEMPTS = 3 #how many times should receiver attempt to use CRC?
|
24
|
+
LOG_NAME='XModem'
|
25
|
+
@timeout_seconds = 5.0 #default timeout period
|
26
|
+
|
27
|
+
#how long does the protocol wait before giving up?
|
28
|
+
def XMODEM::timeout_seconds
|
29
|
+
@timeout_seconds
|
30
|
+
end
|
31
|
+
|
32
|
+
def XMODEM::timeout_seconds=(val)
|
33
|
+
@timeout_seconds=val
|
34
|
+
end
|
35
|
+
|
36
|
+
# receive a file using XMODEM protocol (block size = 128 bytes)
|
37
|
+
# remote:: must be an IO object connected to an XMODEM sender
|
38
|
+
# local:: must be an IO object - the inbound file (trimmed of any final padding) will be written to this
|
39
|
+
# options:: hash of options. options are: values :crc (use 16bit CRC instead of 8 bit checksum)
|
40
|
+
# - :mode=> :crc or :checksum (default is 8 bit checksum)
|
41
|
+
#
|
42
|
+
def XMODEM::receive(remote,local,options=nil)
|
43
|
+
|
44
|
+
mode = ( (options.nil?) || options[:mode].nil? ) ? :checksum : options[:mode]
|
45
|
+
|
46
|
+
logger.debug "receiver: XMODEM - #{mode}"
|
47
|
+
#flush the input buffer
|
48
|
+
loop do
|
49
|
+
break if (select([remote],nil,nil,0.01).nil?)
|
50
|
+
remote.getc
|
51
|
+
end
|
52
|
+
|
53
|
+
#trigger sending
|
54
|
+
case mode
|
55
|
+
when :crc
|
56
|
+
XMODEM_CRC_ATTEMPTS.times do |attempt|
|
57
|
+
remote.putc('C')
|
58
|
+
break unless (select([remote],nil,nil,timeout_seconds).nil?)
|
59
|
+
#if we don't get a response, fall back to checksum mode
|
60
|
+
if attempt==XMODEM_CRC_ATTEMPTS-1 then
|
61
|
+
logger.warn "receiver: crc-16 request failed, falling back to checksum mode"
|
62
|
+
remote.putc(NAK)
|
63
|
+
mode=:checksum
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
remote.putc(NAK)
|
68
|
+
end
|
69
|
+
|
70
|
+
expected_block = 1
|
71
|
+
error_count = 0
|
72
|
+
last_block = ""
|
73
|
+
data = ""
|
74
|
+
loop do
|
75
|
+
|
76
|
+
begin
|
77
|
+
rx_cmd = receive_getbyte(remote).ord
|
78
|
+
|
79
|
+
if rx_cmd == EOT then
|
80
|
+
remote.putc(ACK)
|
81
|
+
trimmed_block=last_block.sub(/(\x1A+)\Z/,'')
|
82
|
+
local<< trimmed_block#trim any trailing FILLER characters in last block
|
83
|
+
break
|
84
|
+
end
|
85
|
+
|
86
|
+
if rx_cmd!=SOH then
|
87
|
+
logger.warn "receiver: expected SOH (0x#{SOH}) got 0x#{"%x" % rx_cmd} - pos = #{remote.pos}"
|
88
|
+
next
|
89
|
+
end
|
90
|
+
|
91
|
+
data=""
|
92
|
+
block= receive_getbyte(remote).ord
|
93
|
+
block_check = receive_getbyte(remote).ord
|
94
|
+
validity = :valid
|
95
|
+
validity = :invalid unless (block_check + block)==0xFF
|
96
|
+
|
97
|
+
logger.debug "receiver: #{validity} block number 0x#{"%02x" % block} / block number check 0x#{"%02x" % block_check}"
|
98
|
+
logger.debug "receiver: receiving block #{block} / expected block #{expected_block}"
|
99
|
+
|
100
|
+
raise RXSynchError if block != expected_block && block != ((expected_block-1) % 0x100)
|
101
|
+
|
102
|
+
if (block==expected_block) && (validity==:valid) then
|
103
|
+
local<<last_block
|
104
|
+
last_block=""
|
105
|
+
end
|
106
|
+
XMODEM_BLOCK_SIZE.times do
|
107
|
+
b=(receive_getbyte(remote))
|
108
|
+
data<<b
|
109
|
+
Thread.pass
|
110
|
+
end
|
111
|
+
|
112
|
+
check_ok=false
|
113
|
+
|
114
|
+
case mode
|
115
|
+
when :crc
|
116
|
+
rx_crc = ( receive_getbyte(remote).ord<<8) + receive_getbyte(remote).ord
|
117
|
+
crc = ccitt16_crc(data)
|
118
|
+
check_ok = (crc==rx_crc)
|
119
|
+
if !check_ok then
|
120
|
+
logger.warn "receiver: invalid crc-16 for block #{block}: calculated 0x#{'%04x' % crc}, got 0x#{'%02x' % rx_crc}"
|
121
|
+
end
|
122
|
+
else
|
123
|
+
rx_checksum = receive_getbyte(remote).ord
|
124
|
+
checksum = XMODEM::checksum(data)
|
125
|
+
check_ok = (checksum == rx_checksum)
|
126
|
+
if !check_ok then
|
127
|
+
logger.warn "receiver: invalid checksum for block #{block}: calculated 0x#{'%02x' % checksum}, got 0x#{'%02x' % rx_checksum}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
if (check_ok) then
|
132
|
+
last_block = data
|
133
|
+
logger.debug "receiver: #{mode} test passed for block #{block}"
|
134
|
+
remote.putc(ACK)
|
135
|
+
if (block == expected_block) then
|
136
|
+
expected_block = ((expected_block+1) % 0x100)
|
137
|
+
error_count=0 #reset the error count
|
138
|
+
end
|
139
|
+
else
|
140
|
+
remote.putc(NAK)
|
141
|
+
error_count+=1
|
142
|
+
logger.warn "receiver: checksum error # #{error_count} / max #{XMODEM_MAX_ERRORS}"
|
143
|
+
raise RXChecksumError.new("too many receive errors on block #{block}") if error_count>XMODEM_MAX_ERRORS
|
144
|
+
end
|
145
|
+
rescue RXTimeout
|
146
|
+
error_count+=1
|
147
|
+
logger.warn "receiver: timeout error # #{error_count} / max #{XMODEM_MAX_ERRORS}"
|
148
|
+
raise RXTimeout("too many receive errors on block #{block}").new if error_count>XMODEM_MAX_ERRORS
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
logger.info "receive complete"
|
153
|
+
end
|
154
|
+
|
155
|
+
# send a file using standard XMODEM protocol (block size = 128 bytes)
|
156
|
+
# will use CRC mode if requested by sender, else use 8-bit checksum
|
157
|
+
# remote:: must be an IO object connected to an XMODEM receiver
|
158
|
+
# local:: must be an IO object containing the data to be sent
|
159
|
+
def XMODEM::send(remote,local)
|
160
|
+
block_number=1
|
161
|
+
current_block=""
|
162
|
+
sent_eot=false
|
163
|
+
|
164
|
+
XMODEM_BLOCK_SIZE.times do
|
165
|
+
b=(local.eof? ? FILLER : local.getc)
|
166
|
+
current_block<<b.chr
|
167
|
+
Thread.pass
|
168
|
+
end
|
169
|
+
checksum = XMODEM::checksum(current_block)
|
170
|
+
mode=:checksum
|
171
|
+
loop do
|
172
|
+
logger.info "sender: waiting for ACK/NAK/CAN (eot_sent: #{sent_eot})"
|
173
|
+
if select([remote],nil,nil,timeout_seconds*XMODEM_MAX_TIMEOUTS).nil? then
|
174
|
+
raise RXTimeout.new("timeout waiting for input on tx (#{timeout_seconds*XMODEM_MAX_TIMEOUTS} seconds)") unless sent_eot
|
175
|
+
logger.info "sender: timeout waiting for ACK of EOT"
|
176
|
+
return
|
177
|
+
end
|
178
|
+
if remote.eof? then
|
179
|
+
logger.warn "sender: unexpected eof on input"
|
180
|
+
break
|
181
|
+
end
|
182
|
+
tx_cmd=remote.getc.ord
|
183
|
+
logger.debug "sendder: got 0x#{"%x" % tx_cmd}"
|
184
|
+
if tx_cmd==ACK then
|
185
|
+
if sent_eot then
|
186
|
+
logger.debug "sender: got ACK of EOT"
|
187
|
+
break
|
188
|
+
end
|
189
|
+
|
190
|
+
if local.eof? then
|
191
|
+
remote.putc(EOT)
|
192
|
+
logger.debug "sender: got ACK of last block"
|
193
|
+
sent_eot=true
|
194
|
+
next
|
195
|
+
end
|
196
|
+
block_number=((block_number+1)%0x100)
|
197
|
+
current_block=""
|
198
|
+
XMODEM_BLOCK_SIZE.times do
|
199
|
+
b=(local.eof? ? FILLER : local.getc)
|
200
|
+
current_block<<b
|
201
|
+
Thread.pass
|
202
|
+
end
|
203
|
+
|
204
|
+
elsif (block_number==1) && (tx_cmd==CRC_MODE) then
|
205
|
+
mode=:crc
|
206
|
+
logger.debug "sender: using crc-16 mode"
|
207
|
+
end
|
208
|
+
|
209
|
+
next unless [ACK,NAK,CRC_MODE].include?(tx_cmd.ord)
|
210
|
+
logger.info "sender: sending block #{block_number}"
|
211
|
+
remote.putc(SOH) #start of block
|
212
|
+
remote.putc(block_number) #block number
|
213
|
+
remote.putc(0xff-block_number) #1's complement of block number
|
214
|
+
current_block.each_byte {|b| remote.putc(b)}
|
215
|
+
case mode
|
216
|
+
when :crc then
|
217
|
+
crc = ccitt16_crc (current_block)
|
218
|
+
remote.putc(crc >> 8) #crc hi byte
|
219
|
+
remote.putc(crc & 0xFF) #crc lo byte
|
220
|
+
logger.debug "sender: crc-16 for block #{block_number}:#{ "%04x" % crc}"
|
221
|
+
else
|
222
|
+
checksum = XMODEM::checksum(current_block)
|
223
|
+
remote.putc(checksum)
|
224
|
+
logger.debug "sender: checksum for block #{block_number}:#{ "%02x" % checksum}"
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
logger.info "sending complete (eot_sent: #{sent_eot})"
|
229
|
+
end
|
230
|
+
|
231
|
+
#calculate an 8-bit XMODEM checksum
|
232
|
+
#this is just the sum of all bytes modulo 0x100
|
233
|
+
def XMODEM::checksum(block)
|
234
|
+
raise RXChecksumError.new("checksum requested of invalid block {size should be #{XMODEM_BLOCK_SIZE}, was #{block.length}") unless block.length==XMODEM_BLOCK_SIZE
|
235
|
+
checksum=0
|
236
|
+
block.each_byte do |b|
|
237
|
+
checksum = (checksum+b) % 0x100
|
238
|
+
end
|
239
|
+
checksum
|
240
|
+
end
|
241
|
+
|
242
|
+
#calculate a 16-bit CRC
|
243
|
+
def XMODEM::ccitt16_crc(block)
|
244
|
+
# cribbed from http://www.hadermann.be/blog/32/ruby-crc16-implementation/
|
245
|
+
raise RXChecksumError.new("checksum requested of invalid block {size should be #{XMODEM_BLOCK_SIZE}, was #{block.length}") unless block.length==XMODEM_BLOCK_SIZE
|
246
|
+
crc=0
|
247
|
+
block.each_byte{|x| crc = ((crc<<8) ^ CCITT_16[(crc>>8) ^ x])&0xffff}
|
248
|
+
crc
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
SOH = 0x01
|
254
|
+
STX = 0x02
|
255
|
+
EOT = 0x04
|
256
|
+
ACK = 0x06
|
257
|
+
NAK = 0x15
|
258
|
+
CAN = 0x18
|
259
|
+
CRC_MODE = 0x43 #'C'
|
260
|
+
FILLER = 0x1A
|
261
|
+
|
262
|
+
CCITT_16 = [
|
263
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
|
264
|
+
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
|
265
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
|
266
|
+
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
|
267
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
|
268
|
+
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
|
269
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
|
270
|
+
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
|
271
|
+
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
|
272
|
+
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
|
273
|
+
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
|
274
|
+
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
|
275
|
+
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
|
276
|
+
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
|
277
|
+
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
|
278
|
+
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
|
279
|
+
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
|
280
|
+
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
|
281
|
+
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
|
282
|
+
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
|
283
|
+
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
|
284
|
+
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
285
|
+
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
|
286
|
+
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
|
287
|
+
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
|
288
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
|
289
|
+
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
|
290
|
+
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
|
291
|
+
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
|
292
|
+
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
|
293
|
+
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
|
294
|
+
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
|
295
|
+
]
|
296
|
+
|
297
|
+
def XMODEM::receive_getbyte(remote)
|
298
|
+
|
299
|
+
if (select([remote],nil,nil,timeout_seconds).nil?) then
|
300
|
+
remote.putc(NAK)
|
301
|
+
raise RXTimeout
|
302
|
+
end
|
303
|
+
|
304
|
+
raise RXSynchError if remote.eof?
|
305
|
+
remote.getc
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
def XMODEM::logger
|
310
|
+
Logger.new(LOG_NAME) if Logger[LOG_NAME].nil?
|
311
|
+
Logger[LOG_NAME]
|
312
|
+
end
|
313
|
+
|
314
|
+
class RXTimeout < RuntimeError
|
315
|
+
end
|
316
|
+
|
317
|
+
class RXChecksumError < RuntimeError
|
318
|
+
end
|
319
|
+
|
320
|
+
class RXSynchError < RuntimeError
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
data/test/test_xmodem.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
#make sure the relevant folder with our libraries is in the require path
|
2
|
+
lib_path=File.expand_path(File.dirname(__FILE__)+"//..//lib")
|
3
|
+
$:.unshift(lib_path) unless $:.include?(lib_path)
|
4
|
+
|
5
|
+
require 'versioncheck'
|
6
|
+
require 'xmodem/version'
|
7
|
+
|
8
|
+
rb_vc = VersionCheck.rubyversion
|
9
|
+
if !rb_vc.have_version?(2,1)
|
10
|
+
require 'simplecov'
|
11
|
+
SimpleCov.command_name 'MiniTest'
|
12
|
+
SimpleCov.start
|
13
|
+
end
|
14
|
+
|
15
|
+
if ENV['TRAVIS'] == "true" && ENV['CI'] =="true"
|
16
|
+
require 'coveralls'
|
17
|
+
Coveralls.wear!
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'minitest/autorun'
|
21
|
+
require 'minitest/reporters'
|
22
|
+
|
23
|
+
MiniTest::Reporters.use!
|
24
|
+
|
25
|
+
require 'xmodem'
|
26
|
+
require 'socket'
|
27
|
+
require 'stringio'
|
28
|
+
|
29
|
+
Thread.abort_on_exception = true
|
30
|
+
|
31
|
+
XMODEM::logger.outputters = Outputter.stdout
|
32
|
+
|
33
|
+
XMODEM::timeout_seconds=0.4 #so we don't wait so long for retransmissions
|
34
|
+
|
35
|
+
LOCAL_PORT=9999
|
36
|
+
|
37
|
+
module CorruptIn
|
38
|
+
attr_accessor :real_getc,:corruption_frequency
|
39
|
+
|
40
|
+
def getc
|
41
|
+
@char_count=0 if @char_count.nil?
|
42
|
+
|
43
|
+
raise "real_getc not initialised" if @real_getc.nil?
|
44
|
+
b=@real_getc.call
|
45
|
+
@char_count+=1
|
46
|
+
if ((@char_count % @corruption_frequency)==0) then
|
47
|
+
corrupted_char=0xff-b.ord
|
48
|
+
$stdout.puts "corrupting : 0x%02x -> 0x%02x" % [b.ord,corrupted_char]
|
49
|
+
b=corrupted_char.chr
|
50
|
+
end
|
51
|
+
return b
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
module CorruptOut
|
57
|
+
attr_accessor :real_putc,:corruption_frequency
|
58
|
+
|
59
|
+
def putc (b)
|
60
|
+
@char_count=0 if @char_count.nil?
|
61
|
+
|
62
|
+
raise "real_putc not initialised" if @real_putc.nil?
|
63
|
+
@char_count+=1
|
64
|
+
if ((@char_count % @corruption_frequency)==0) then
|
65
|
+
corrupted_char=0xff-b
|
66
|
+
$stdout.puts "corrupting : 0x%02x -> 0x%02x" % [b,corrupted_char]
|
67
|
+
b=corrupted_char
|
68
|
+
end
|
69
|
+
@real_putc.call(b)
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
module DropIn
|
76
|
+
attr_accessor :real_getc,:drop_frequency
|
77
|
+
|
78
|
+
def getc
|
79
|
+
@char_count=0 if @char_count.nil?
|
80
|
+
|
81
|
+
raise "real_getc not initialised" if @real_getc.nil?
|
82
|
+
b=@real_getc.call
|
83
|
+
@char_count+=1
|
84
|
+
if ((@char_count % @drop_frequency)==0) then
|
85
|
+
$stdout.puts "dropping : 0x%02x " % [b.ord]
|
86
|
+
@real_getc.call
|
87
|
+
end
|
88
|
+
return b
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module DropOut
|
93
|
+
attr_accessor :real_putc,:drop_frequency
|
94
|
+
|
95
|
+
def putc (b)
|
96
|
+
@char_count=0 if @char_count.nil?
|
97
|
+
|
98
|
+
raise "real_putc not initialised" if @real_putc.nil?
|
99
|
+
@char_count+=1
|
100
|
+
if ((@char_count % @drop_frequency)==0) then
|
101
|
+
$stdout.puts "dropping : 0x%02x " % [b]
|
102
|
+
else
|
103
|
+
@real_putc.call(b)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class XmodemTests < MiniTest::Test
|
109
|
+
|
110
|
+
@@server=nil
|
111
|
+
@@unix_socket = "/tmp/xmodem-test.sock"
|
112
|
+
|
113
|
+
def sendfile(file)
|
114
|
+
if @@server.nil? then
|
115
|
+
if ENV['OS'] != "Windows_NT"
|
116
|
+
File.delete( @@unix_socket ) if FileTest.exists?( @@unix_socket )
|
117
|
+
@@server = UNIXServer.new(@@unix_socket)
|
118
|
+
else
|
119
|
+
@@server = TCPServer.new(LOCAL_PORT)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
puts "reusing existing server"
|
123
|
+
end
|
124
|
+
session = @@server.accept
|
125
|
+
session.sync=true
|
126
|
+
puts "Connected (sendfile)"
|
127
|
+
XMODEM::send(session,file)
|
128
|
+
session.close
|
129
|
+
end
|
130
|
+
|
131
|
+
def acceptfile(file,error_type=nil,error_frequency=nil,rx_options=nil)
|
132
|
+
if ENV['OS'] != "Windows_NT"
|
133
|
+
socket = UNIXSocket.open(@@unix_socket)
|
134
|
+
else
|
135
|
+
socket = TCPSocket.new('localhost', LOCAL_PORT)
|
136
|
+
end
|
137
|
+
socket.sync=true
|
138
|
+
puts "Connected (acceptfile)"
|
139
|
+
if !error_frequency.nil? then
|
140
|
+
real_getc=socket.method(:getc)
|
141
|
+
|
142
|
+
case error_type
|
143
|
+
when :corruption_in
|
144
|
+
real_getc=socket.method(:getc)
|
145
|
+
socket.extend(CorruptIn)
|
146
|
+
socket.corruption_frequency=error_frequency
|
147
|
+
socket.real_getc=real_getc
|
148
|
+
when :packet_loss_in
|
149
|
+
real_getc=socket.method(:getc)
|
150
|
+
socket.extend(DropIn)
|
151
|
+
socket.drop_frequency=error_frequency
|
152
|
+
socket.real_getc=real_getc
|
153
|
+
when :packet_loss_out
|
154
|
+
real_putc=socket.method(:putc)
|
155
|
+
socket.extend(DropOut)
|
156
|
+
socket.drop_frequency=error_frequency
|
157
|
+
socket.real_putc=real_putc
|
158
|
+
when :corruption_out
|
159
|
+
real_putc=socket.method(:putc)
|
160
|
+
socket.extend(CorruptOut)
|
161
|
+
socket.corruption_frequency=error_frequency
|
162
|
+
socket.real_putc=real_putc
|
163
|
+
else
|
164
|
+
raise "unknown error_type #{error_type}"
|
165
|
+
end
|
166
|
+
file.flush
|
167
|
+
end
|
168
|
+
XMODEM::receive(socket,file,rx_options)
|
169
|
+
loop {Thread.pass} until socket.closed?
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
def do_test(tx_file,rx_file,error_type=nil,error_frequency=nil,rx_options=nil)
|
174
|
+
test_description="test type: #{error_type}"
|
175
|
+
test_description+=" (freq=#{error_frequency})" unless error_frequency.nil?
|
176
|
+
made_tx = false
|
177
|
+
made_rx = false
|
178
|
+
if !(tx_file.respond_to?(:getc))
|
179
|
+
tx_filename=tx_file
|
180
|
+
tx_file=File.new(tx_filename,"rb")
|
181
|
+
made_tx = true
|
182
|
+
else
|
183
|
+
tx_filename=tx_file.class
|
184
|
+
tx_file.rewind
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
if !(rx_file.respond_to?(:putc))
|
189
|
+
rx_filename=rx_file
|
190
|
+
rx_file=File.new(rx_filename,"wb+")
|
191
|
+
made_rx = true
|
192
|
+
else
|
193
|
+
rx_filename=rx_file.class
|
194
|
+
rx_file.rewind
|
195
|
+
end
|
196
|
+
puts "#{test_description} : #{tx_filename}->#{rx_filename}"
|
197
|
+
|
198
|
+
|
199
|
+
tx_thread=Thread.new {sendfile(tx_file)}
|
200
|
+
sleep(0.1) # Time for the socket to be opened
|
201
|
+
rx_thread=Thread.new {acceptfile(rx_file,error_type,error_frequency,rx_options)}
|
202
|
+
loop do
|
203
|
+
break unless tx_thread.alive?
|
204
|
+
break unless rx_thread.alive?
|
205
|
+
sleep(0.01) #wake up occasionally to get keyboard input, so we break on ^C
|
206
|
+
end
|
207
|
+
tx_file.rewind
|
208
|
+
rx_file.rewind
|
209
|
+
rx_filecontents=rx_file.read
|
210
|
+
tx_filecontents=tx_file.read
|
211
|
+
assert_equal(tx_filecontents.length,rx_filecontents.length,"file length correct after round trip")
|
212
|
+
assert_equal(tx_filecontents,rx_filecontents,"file contents correct after round trip")
|
213
|
+
|
214
|
+
tx_file.close if made_tx
|
215
|
+
rx_file.close if made_rx
|
216
|
+
|
217
|
+
File.delete(rx_filename) if made_rx
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Teardown
|
222
|
+
def teardown
|
223
|
+
unless ENV['OS'] == "Windows_NT"
|
224
|
+
File.delete( @@unix_socket ) if FileTest.exist?( @@unix_socket )
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Test cases
|
229
|
+
def test_version
|
230
|
+
assert_equal("0.1.0", ::XMODEM::VERSION)
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_checksum
|
234
|
+
assert_equal(0, XMODEM::checksum( "\000"*128))
|
235
|
+
assert_equal(128, XMODEM::checksum("\001"*128))
|
236
|
+
assert_equal(0, XMODEM::checksum("\002"*128))
|
237
|
+
assert_equal(128, XMODEM::checksum("\003"*128))
|
238
|
+
end
|
239
|
+
|
240
|
+
def test_all
|
241
|
+
sample_text_file="sample.test.txt"
|
242
|
+
sample_bin_file="sample.test.bin"
|
243
|
+
f=File.new(sample_text_file,"w")
|
244
|
+
f<<File.new(__FILE__,"r").read
|
245
|
+
f.close
|
246
|
+
|
247
|
+
f = File.new(sample_bin_file,"wb")
|
248
|
+
2000.times { |i| f << (i % 0x100).chr}
|
249
|
+
f.close
|
250
|
+
|
251
|
+
txstring_io=StringIO.new("this is a test string")
|
252
|
+
rxstring_io=StringIO.new("")
|
253
|
+
|
254
|
+
do_test(sample_text_file, "crc-simple.test.txt", nil, nil, {:mode=>:crc})
|
255
|
+
do_test(sample_bin_file, "crc-simple.test.bin", nil, nil, {:mode=>:crc})
|
256
|
+
do_test(txstring_io,rxstring_io)
|
257
|
+
do_test(sample_text_file,"corrupt-crc.test.txt",:corruption_in,700,{:mode=>:crc})
|
258
|
+
|
259
|
+
do_test(txstring_io,rxstring_io)
|
260
|
+
do_test(sample_text_file,rxstring_io)
|
261
|
+
do_test(sample_text_file,"simple.test.txt")
|
262
|
+
do_test(sample_bin_file,"corrupt_out.test.bin",:corruption_out,4)
|
263
|
+
do_test(sample_text_file,"packet_loss_out.test.txt",:packet_loss_out,3)
|
264
|
+
do_test(sample_text_file,"packet_loss_in.test.txt",:packet_loss_in,200)
|
265
|
+
|
266
|
+
do_test(sample_text_file,"corrupt.test.txt",:corruption_in,700)
|
267
|
+
do_test(sample_text_file,"very_corrupt.test.txt",:corruption_in,200)
|
268
|
+
|
269
|
+
do_test(sample_bin_file,"simple.test.bin")
|
270
|
+
do_test(sample_bin_file,"corrupt.test.bin",:corruption_in,700)
|
271
|
+
|
272
|
+
bigstring=""
|
273
|
+
512.times {|i| bigstring<<((i%0x100).chr*128)}
|
274
|
+
do_test(StringIO.new(bigstring),"bigfile.test.txt")
|
275
|
+
|
276
|
+
File.delete(sample_text_file, sample_bin_file)
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|