1 module xymodem.ymodem; 2 3 @safe: 4 5 import xymodem.exception; 6 import std.conv: to; 7 import std.typecons: Nullable; 8 9 /// Reads 1 octet from transmission line 10 /// Returns: isNull if no data was received for the specified time 11 alias RecvCallback = Nullable!ubyte delegate(uint timeoutMsecs); 12 13 /// Send octets into transmission line 14 /// Returns: true if send was successful 15 alias SendCallback = bool delegate(const ubyte[] data); 16 17 class YModemSender 18 { 19 private const RecvCallback recvData; 20 private const SendCallback sendData; 21 22 private ubyte currBlockNum; 23 private size_t currByte; 24 private bool isAborting; 25 26 private immutable Control[] ACK = [Control.ACK]; 27 28 /// Returns: bytes count for the current/latest file transfer 29 size_t bytesSent() const 30 { 31 return currByte; 32 } 33 34 // TODO: Implement it! 35 //~ /// Abort current transfer 36 //~ void abort() 37 //~ { 38 //~ isAborting = true; 39 //~ } 40 41 this 42 ( 43 RecvCallback recvCb, 44 SendCallback sendCb, 45 ) 46 { 47 recvData = recvCb; 48 sendData = sendCb; 49 } 50 51 52 void send 53 ( 54 in string filename, 55 in ubyte[] fileData 56 ) 57 { 58 // TODO: abort transfer support 59 60 // Waiting for initial C symbol 61 waitForSymbol(Control.ST_C, WAIT_FOR_RECEIVER_TIMEOUT); 62 63 currBlockNum = 0; 64 currByte = 0; 65 size_t currEndByte = 0; 66 ubyte recvErrCnt; 67 68 while(currByte < fileData.length) 69 { 70 if(currBlockNum == 0) 71 { 72 sendYModemHeaderBlock(filename, fileData.length); 73 74 // After ACK receiver shall immediately send 'C' 75 waitForSymbol(Control.ST_C, SEND_BLOCK_TIMEOUT); 76 } 77 else 78 { 79 const size_t remaining = fileData.length - currByte; 80 const size_t blockSize = remaining <= 128 ? 128 : 1024; 81 currEndByte = currByte + (remaining > blockSize ? blockSize : remaining); 82 83 const ubyte[] sliceToSend = fileData[currByte .. currEndByte]; 84 85 sendBlock(blockSize, sliceToSend, ACK); 86 87 currByte = currEndByte; 88 } 89 90 currBlockNum++; 91 } 92 93 // End of YMODEM batch transmission session 94 { 95 sendChunkWithConfirm([cast(ubyte) Control.EOT], ACK); 96 97 waitForSymbol(Control.ST_C, WAIT_FOR_RECEIVER_TIMEOUT); 98 99 ubyte[] b = new ubyte[128]; 100 currBlockNum = 0; 101 sendBlock(128, b, [Control.ACK]); 102 } 103 } 104 105 private void waitForSymbol(Control cntl, uint timeout) const 106 { 107 for(ubyte i = 0; i < MAXERRORS; i++) 108 { 109 if(recvConfirm([cntl], timeout)) 110 return; 111 } 112 113 throw new YModemException("Control symbol receiver reached maximum error count", __FILE__, __LINE__); 114 } 115 116 private void sendYModemHeaderBlock(string filename, size_t filesize) 117 { 118 import std.conv: to; 119 120 string blockContent = filename ~ '\x00' ~ filesize.to!string; 121 122 ubyte[] bytes = filenameString2ubytesz(blockContent); 123 124 const size_t blockSize = blockContent.length <= 128 ? 128 : 1024; 125 126 sendBlock(blockSize, bytes, ACK); 127 } 128 129 private static ubyte[] filenameString2ubytesz(in string str) pure 130 { 131 import std.ascii; 132 import std.exception; 133 134 ubyte[] bytes; 135 136 foreach(c; str) 137 { 138 enforce(c.isASCII, "Non-ASCII symbol is found in the filename"); 139 enforce(c != '\\', "'\' isn't allowed in the filename"); 140 141 bytes ~= c; 142 } 143 144 return bytes ~ 0x00; 145 } 146 147 /// Sends 128 or 1024 B block. 148 /// blockData without padding! 149 private void sendBlock(in size_t blockSize, in ubyte[] blockData, in Control[] validAnswers) 150 { 151 import xymodem.crc: crc16; 152 import std.bitmanip: nativeToBigEndian; 153 154 const ubyte[3] header = [ 155 blockSize == 1024 ? cast(ubyte) Control.STX : cast(ubyte) Control.SOH, 156 currBlockNum, 157 0xFF - currBlockNum 158 ]; 159 160 ubyte[] paddingBuff; 161 162 if(blockData.length != blockSize) 163 { 164 // need pading 165 paddingBuff = new ubyte[](cast(ubyte) Control.CPMEOF); 166 paddingBuff.length = blockSize - blockData.length; 167 } 168 169 ushort crc; 170 crc16(crc, blockData); 171 crc16(crc, paddingBuff); 172 ubyte[2] orderedCRC = nativeToBigEndian(crc); 173 174 sendChunkWithConfirm(header~blockData~paddingBuff~orderedCRC, validAnswers); 175 } 176 177 private void sendChunkWithConfirm(in ubyte[] data, in Control[] validAnswers) const 178 { 179 for(ubyte recvErrCnt = 0; recvErrCnt < MAXERRORS; recvErrCnt++) 180 { 181 sendChunk(data); 182 183 if(recvConfirm(validAnswers, SEND_BLOCK_TIMEOUT)) 184 return; 185 } 186 187 throw new YModemException("Confirmation symbol receiver reached maximum error count", __FILE__, __LINE__); 188 } 189 190 private void sendChunk(in ubyte[] data) const 191 { 192 for(ubyte errcnt = 0; errcnt < MAXERRORS; errcnt++) 193 { 194 if(sendData(data)) 195 return; 196 } 197 198 throw new YModemException("Transmission line sender reached maximum error count", __FILE__, __LINE__); 199 } 200 201 private bool recvConfirm(in Control[] validAnswers, uint timeout) const 202 { 203 try 204 receiveTheseControlSymbols(validAnswers, timeout); 205 catch(RecvException e) 206 return false; 207 208 return true; 209 } 210 211 private Control receiveTheseControlSymbols(in Control[] ctls, uint timeout) const 212 { 213 import std.algorithm.searching: canFind; 214 215 Nullable!ubyte r = recvData(timeout); 216 217 if(r.isNull) 218 { 219 throw new RecvException(RecvErrType.NO_REPLY, "Control symbol isn't received", __FILE__, __LINE__); 220 } 221 else 222 { 223 Control b = cast(Control) r.get; 224 225 if(!canFind(ctls, b)) 226 throw new RecvException(RecvErrType.NOT_EXPECTED, "Received "~b.to!string~", but expected "~ctls.to!string, __FILE__, __LINE__); 227 else 228 return b; 229 } 230 } 231 } 232 233 /// Protocol characters 234 private enum Control: ubyte 235 { 236 SOH = 0x01, /// Start Of Header 237 STX = 0x02, /// Start Of Text (used like SOH but means 1024 block size) 238 EOT = 0x04, /// End Of Transmission 239 ACK = 0x06, /// ACKnowlege 240 NAK = 0x15, /// Negative AcKnowlege 241 CAN = 0x18, /// CANcel character 242 CPMEOF = 0x1A, /// '^Z' 243 ST_C = 'C' /// Start XMODEM/CRC block 244 } 245 246 // Some useful constants 247 private immutable ubyte MAXERRORS = 10; 248 private immutable uint WAIT_FOR_RECEIVER_TIMEOUT = 60_000; 249 private immutable uint SEND_BLOCK_TIMEOUT = 10_000; 250 251 unittest 252 { 253 { 254 // Block CRC-16 creation check 255 256 import std.conv: parse; 257 import std.array: array; 258 import std.range: chunks; 259 import std.algorithm: map; 260 import xymodem.crc; 261 import std.bitmanip; 262 263 // Valid block captured from lrzsz interchange 264 immutable string textBlock = "01 08 f7 d7 85 78 59 20 01 d3 0d 19 96 57 55 71 2d e5 7d 52 16 b2 51 fe d3 72 7d 6c 3f 31 0f e1 ea 40 18 11 40 74 40 41 b4 22 c2 98 82 d8 70 34 45 8f a8 c7 ab 28 5d 05 24 89 ff f1 1b 27 62 41 4f 62 99 c1 64 bb b8 d1 df 65 d1 43 c1 f8 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 42 47"; 265 266 const ubyte[] block = textBlock 267 .chunks(3) 268 .map!(twoDigits => twoDigits.parse!ubyte(16)) 269 .array(); 270 271 ushort crc; 272 crc16(crc, block[3 .. $-2]); // header and crc is stripped 273 274 ubyte[2] validCRC = block[$-2 .. $]; 275 ubyte[2] ownCRC = nativeToBigEndian(crc); 276 277 assert(ownCRC == validCRC); 278 } 279 280 ubyte[] readFromFile() 281 { 282 auto b = new ubyte[1024]; 283 284 return b; 285 } 286 287 ubyte[] sended; 288 289 bool sendToLine(const ubyte[] toSend) 290 { 291 sended ~= toSend; 292 293 return true; 294 } 295 296 Nullable!ubyte receiveFromLine(uint timeout) 297 { 298 return Nullable!ubyte('C'); 299 } 300 301 auto sender = new YModemSender( 302 &receiveFromLine, 303 &sendToLine, 304 ); 305 }