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 }