1 /** 
2  * Message definition and tooling
3  */
4 module birchwood.protocol.messages;
5 
6 import dlog;
7 
8 import std.string;
9 import std.conv : to, ConvException;
10 import birchwood.protocol.constants : ReplyType;
11 
12 // TODO: Before release we should remove this import
13 import std.stdio : writeln;
14 
15 /* TODO: We could move these all to `package.d` */
16 
17 /* Static is redundant as module is always static , gshared needed */
18 /* Apparebky works without gshared, that is kinda sus ngl */
19 package __gshared Logger logger;
20 /**
21 * source/birchwood/messages.d(10,8): Error: variable `birchwood.messages.logger` is a thread-local class and cannot have a static initializer. Use `static this()` to initialize instead.
22 *
23 * It is complaining that it wopuld static init per thread, static this() for module is required but that would
24 * do a module init per thread, so __gshared static this() is needed, we want one global init - a single logger
25 * variable and also class init
26 */
27 
28 __gshared static this()
29 {
30     logger = new DefaultLogger();
31 }
32 
33 /**
34 * Encoding/decoding primitives
35 */
36 
37 /** 
38  * Encodes the provided message into a CRLF
39  * terminated byte array
40  *
41  * Params:
42  *   messageIn = the message to encode
43  * Returns: the encoded message
44  */
45 public ubyte[] encodeMessage(string messageIn)
46 {
47     ubyte[] messageOut = cast(ubyte[])messageIn;
48     messageOut~=[cast(ubyte)13, cast(ubyte)10];
49     return messageOut;
50 }
51 
52 public static string decodeMessage(ubyte[] messageIn)
53 {
54     /* TODO: We could do a chekc to ESNURE it is well encoded */
55 
56     return cast(string)messageIn[0..messageIn.length-2];
57     // return  null;
58 }
59 
60 /** 
61  * Checks if the provided message is valid (i.e.)
62  * does not contain any CR or LF characters in it
63  *
64  * Params:
65  *   message = the message to test
66  * Returns: <code>true</code> if the message is valid,
67  * <code>false</code> false otherwise
68  */
69  //TODO: Should we add an emptiness check here
70 public static bool isValidText(string message)
71 {
72     foreach(char character; message)
73     {
74         if(character == 13 || character == 10)
75         {
76             return false;
77         }
78     }
79 
80     return true;
81 }
82 
83 /**
84  * Message types
85  */
86 public final class Message
87 {
88     /* Message contents */
89     private string from;
90     private string command;
91     private string params;
92 
93     /* The numeric reply (as per Section 6 of RFC 1459) */
94     private bool isNumericResponse = false;
95     private ReplyType replyType = ReplyType.BIRCHWOOD_UNKNOWN_RESP_CODE;
96     private bool isError = false;
97 
98     /** 
99      * Constructs a new Message
100      *
101      * Params:
102      *   from = the from parameter
103      *   command = the command
104      *   params = any optional parameters to the command
105      */
106     this(string from, string command, string params = "")
107     {
108         this.from = from;
109         this.command = command;
110         this.params = params;
111 
112         /* Check if this is a command reply */
113         if(isNumeric(command))
114         {
115             isNumericResponse = true;
116             
117             //FIXME: SOmething is tripping it u, elts' see
118             try
119             {
120                 /* Grab the code */
121                 replyType = to!(ReplyType)(to!(ulong)(command));
122                 // TODO: Add validity check on range of values here, if bad throw exception
123                 // TODO: Add check for "6.3 Reserved numerics" or handling of SOME sorts atleast
124 
125                 /* Error codes are in range of [401, 502] */
126                 if(replyType >= 401 && replyType <= 502)
127                 {
128                     // TODO: Call error handler
129                     isError = true;
130                 }
131                 /* Command replies are in range of [259, 395] */
132                 else if(replyType >= 259 && replyType <= 395)
133                 {
134                     // TODO: Call command-reply handler
135                     isError = false;
136                 }
137             }
138             catch(ConvException e)
139             {
140                 logger.log("<<< Unsupported response code (Error below) >>>");
141                 logger.log(e);
142             }
143         }
144 
145         /* Parse the parameters into key-value pairs (if any) and trailing text (if any) */
146         parameterParse();
147     }
148 
149     /* TODO: Implement encoder function */
150     public string encode()
151     {
152         string fullLine = from~" "~command~" "~params;
153         return fullLine;
154     }
155 
156     public static Message parseReceivedMessage(string message)
157     {
158         /* TODO: testing */
159 
160         /* From */
161         string from;
162 
163         /* Command */
164         string command;
165 
166         /* Params */
167         string params;
168 
169 
170 
171         /* Check if there is a PREFIX (according to RFC 1459) */
172         if(message[0] == ':')
173         {
174             /* prefix ends after first space (we fetch servername, host/user) */
175             //TODO: make sure not -1
176             long firstSpace = indexOf(message, ' ');
177 
178             /* TODO: double check the condition */
179             if(firstSpace > 0)
180             {
181                 from = message[1..firstSpace];
182 
183                 // logger.log("from: "~from);
184 
185                 /* TODO: Find next space (what follows `from` is  `' ' { ' ' }`) */
186                 ulong i = firstSpace;
187                 for(; i < message.length; i++)
188                 {
189                     if(message[i] != ' ')
190                     {
191                         break;
192                     }
193                 }
194 
195                 // writeln("Yo");
196 
197                 string rem = message[i..message.length];
198                 // writeln("Rem: "~rem);
199                 long idx  = indexOf(rem, " "); //TOOD: -1 check
200 
201                 /* Extract the command */
202                 command = rem[0..idx];
203                 // logger.log("command: "~command);
204 
205                 /* Params are everything till the end */
206                 i = idx;
207                 for(; i < rem.length; i++)
208                 {
209                     if(rem[i] != ' ')
210                     {
211                         break;
212                     }
213                 }
214                 params = rem[i..rem.length];
215                 // logger.log("params: "~params);
216             }
217             else
218             {
219                 //TODO: handle
220                 logger.log("Malformed message start after :");
221                 assert(false);
222             }
223 
224             
225         }
226         /* In this case it is only `<command> <params>` */
227         else
228         {
229 
230             long firstSpace = indexOf(message, " "); //TODO: Not find check
231             
232             command = message[0..firstSpace];
233 
234             ulong pos = firstSpace;
235             for(; pos < message.length; pos++)
236             {
237                 if(message[pos] != ' ')
238                 {
239                     break;
240                 }
241             }
242 
243             params = message[pos..message.length];
244 
245         }
246 
247         return new Message(from, command, params);
248     }
249 
250     public override string toString()
251     {
252         return "(from: "~from~", command: "~command~", message: `"~params~"`)";
253     }
254 
255     /** 
256      * Returns the sender of the message
257      *
258      * Returns: The `from` field
259      */
260     public string getFrom()
261     {
262         return from;
263     }
264     
265     /** 
266      * Returns the command name
267      *
268      * Returns: The command itself
269      */
270     public string getCommand()
271     {
272         return command;
273     }
274 
275     /** 
276      * Returns the optional paremeters (if any)
277      *
278      * Returns: The parameters
279      */
280     public string getParams()
281     {
282         return params;
283     }
284 
285     /** 
286      * Retrieves the trailing text in the paramaters
287      * (if any)
288      *
289      * Returns: the trailing text
290      */
291     public string getTrailing()
292     {
293         return ppTrailing;
294     }
295 
296     /** 
297      * Returns the parameters excluding the trailing text
298      * which are seperated by spaces but only those
299      * which are key-value pairs
300      *
301      * Returns: the key-value pair parameters
302      */
303     public string[string] getKVPairs()
304     {
305         return ppKVPairs;
306     }
307 
308     /** 
309      * Returns the parameters excluding the trailing text
310      * which are seperated by spaces
311      *
312      * Returns: the parameters
313      */
314     public string[] getPairs()
315     {
316         return ppPairs;
317     }
318 
319     private string ppTrailing;
320     private string[string] ppKVPairs;
321     private string[] ppPairs;
322 
323 
324     version(unittest)
325     {
326         import std.stdio;
327     }
328 
329     unittest
330     {
331         string testInput = "A:=1 A=2 :Hello this is text";
332         writeln("Input: ", testInput);
333         
334         bool hasTrailer;
335         string[] splitted = splitting(testInput, hasTrailer);
336         writeln("Input (split): ", splitted);
337 
338         
339 
340         assert(cmp(splitted[0], "A:=1") == 0);
341         assert(cmp(splitted[1], "A=2") == 0);
342 
343         /* Trailer test */
344         assert(hasTrailer);
345         assert(cmp(splitted[2], "Hello this is text") == 0);
346     }
347 
348     unittest
349     {
350         string testInput = ":Hello this is text";
351         bool hasTrailer;
352         string[] splitted = splitting(testInput, hasTrailer);
353 
354         /* Trailer test */
355         assert(hasTrailer);
356         assert(cmp(splitted[0], "Hello this is text") == 0);
357     }
358 
359     /** 
360      * Imagine: `A:=1 A=2 :Hello` 
361      *
362      * Params:
363      *   input = 
364      * Returns: 
365      */
366     private static string[] splitting(string input, ref bool hasTrailer)
367     {
368         string[] splits;
369 
370         bool trailingMode;
371         string buildUp;
372         for(ulong idx = 0; idx < input.length; idx++)
373         {
374             /* Get current character */
375             char curCHar = input[idx];
376 
377 
378             if(trailingMode)
379             {
380                 buildUp ~= curCHar;
381                 continue;
382             }
383 
384             if(buildUp.length == 0)
385             {
386                 if(curCHar == ':')
387                 {
388                     trailingMode = true;
389                     continue;
390                 }
391             }
392             
393 
394             if(curCHar ==  ' ')
395             {
396                 /* Flush */
397                 splits ~= buildUp;
398                 buildUp = "";
399             }
400             else
401             {
402                 buildUp ~= curCHar;
403             }
404         }
405 
406         if(buildUp.length)
407         {
408             splits ~= buildUp;
409         }
410 
411         hasTrailer = trailingMode;
412 
413         return splits;
414     }
415 
416     /** 
417      * NOTE: This needs more work with trailing support
418      * we must make sure we only look for lastInex of `:`
419      * where it is first cyaracter after space but NOT within
420      * an active parameter
421      */
422     private void parameterParse()
423     {
424         /* Only parse if there are params */
425         if(params.length)
426         {
427             /* Trailing text */
428             string trailing;
429 
430             /* Split the `<params>` */
431             bool hasTrailer;
432             string[] paramsSplit = splitting(params, hasTrailer);
433 
434             // logger.debug_("ParamsSPlit direct:", paramsSplit);
435             
436             
437 
438             /* Extract the trailer as the last item in the array (if it exists) */
439             if(hasTrailer)
440             {
441                 trailing = paramsSplit[paramsSplit.length-1];
442 
443                 /* Remove it from the parameters */
444                 paramsSplit = paramsSplit[0..$-1];
445 
446                 // logger.debug_("GOt railer ", trailing);
447             }
448 
449             ppPairs = paramsSplit;
450 
451 
452             /* Generate the key-value pairs */
453             foreach(string pair; paramsSplit)
454             {
455                 /* Only do this if we have an `=` in the current pair */
456                 if(indexOf(pair, "=") > -1)
457                 {
458                     string key = split(pair, "=")[0];
459                     string value = split(pair, "=")[1];
460                     ppKVPairs[key] = value;
461                 }
462             }
463 
464             /* Save the trailing */
465             ppTrailing = trailing;
466 
467             // logger.debug_("ppTrailing: ", ppTrailing);
468         }
469     }
470 
471     /** 
472      * Returns whether or not this message was
473      * a numeric response
474      *
475      * Returns: `true` if numeric response
476      * `false` otherwise
477      */
478     public bool isResponseMessage()
479     {
480         return isNumericResponse;
481     }
482 
483     /** 
484      * Returns whether or not this message is
485      * an error kind-of numeric response
486      *
487      * Returns: `true` if numeric response
488      * is an error, `false` otherwise
489      */
490     public bool isResponseError()
491     {
492         return isError;
493     }
494 
495     /** 
496      * Returns the type of reply (if this message
497      * was a numeric response)
498      *
499      * Returns: the ReplyType
500      */
501     public ReplyType getReplyType()
502     {
503         return replyType;
504     }
505 }