1 /** 2 * Implements the Client-To-Client Protocol (CTCP). 3 * Specification: 4 * $(LINK http://www.irchelp.org/irchelp/rfc/ctcpspec.html) 5 */ 6 module irc.ctcp; 7 8 import std.algorithm; 9 import std.array; 10 import std.range; 11 import std.string; 12 13 import irc.util; 14 15 enum CtcpToken : char 16 { 17 delimiter = 0x01, 18 quote = 0x10, 19 } 20 21 private: 22 /** 23 * Low-level quote a message. 24 * Returns: 25 * Input range for lazily quoting the message 26 */ 27 auto lowQuote(Range)(Range payload) if(isInputRange!Range) 28 { 29 alias ubyte C; 30 31 static if(is(Range : const(char)[])) 32 alias const(ubyte)[] R; 33 else 34 alias Range R; 35 36 static struct Quoter 37 { 38 private: 39 R data; 40 C override_; 41 42 public: 43 bool empty() 44 { 45 return override_ == C.init && data.empty; 46 } 47 48 C front() 49 { 50 if(override_ != C.init) 51 return override_; 52 53 auto front = data.front; 54 if(front == '\0' || front == '\r' || front == '\n') 55 return CtcpToken.quote; 56 57 return front; 58 } 59 60 void popFront() 61 { 62 if(override_ != C.init) 63 { 64 override_ = C.init; 65 return; 66 } 67 68 char prev = data.front; 69 70 switch(prev) 71 { 72 case '\0': 73 override_ = '0'; 74 break; 75 case '\r': 76 override_ = 'r'; 77 break; 78 case '\n': 79 override_ = 'n'; 80 break; 81 case CtcpToken.quote: 82 override_ = CtcpToken.quote; 83 break; 84 default: 85 } 86 87 data.popFront(); 88 } 89 } 90 91 return Quoter(cast(R)payload); 92 } 93 94 /** 95 * Low-level dequote a message. 96 * Returns: 97 * Input range for lazily dequoting the message 98 */ 99 auto lowDequote(Range)(Range quoted) 100 { 101 static if(is(Range : const(char)[])) 102 alias const(ubyte)[] R; 103 else 104 alias Range R; 105 106 static struct Dequoter 107 { 108 private: 109 R remaining; 110 bool wasQuote = false; 111 112 public: 113 bool empty() const pure 114 { 115 return remaining.empty; 116 } 117 118 ubyte front() pure 119 { 120 auto front = remaining.front; 121 122 if(wasQuote) 123 { 124 switch(front) 125 { 126 case '0': 127 return '\0'; 128 case 'r': 129 return '\r'; 130 case 'n': 131 return '\n'; 132 default: 133 break; 134 } 135 } 136 137 return front; 138 } 139 140 private bool skipQuote() 141 { 142 if(!remaining.empty && remaining.front == CtcpToken.quote) 143 { 144 remaining.popFront(); 145 return !remaining.empty; 146 } 147 else 148 return false; 149 } 150 151 void popFront() 152 { 153 remaining.popFront(); 154 wasQuote = skipQuote(); 155 } 156 } 157 158 auto dequoter = Dequoter(cast(R)quoted); 159 dequoter.wasQuote = dequoter.skipQuote(); 160 return dequoter; 161 } 162 163 unittest 164 { 165 string plain, quoted; 166 167 plain = "hello, world"; 168 quoted = "hello, world"; 169 170 assert(plain.lowQuote().array() == quoted); 171 assert(quoted.lowDequote().array() == plain); 172 173 plain = "\rhello, \\t \n\r\0world\0"; 174 quoted = "\x10rhello, \\t \x10n\x10r\x100world\x100"; 175 176 assert(plain.lowQuote().array() == quoted); 177 assert(quoted.lowDequote().array() == plain); 178 } 179 180 /** 181 * Mid-level quote a message. 182 * Returns: 183 * Input range for lazily quoting the message 184 */ 185 auto ctcpQuote(Range)(Range payload) if(isInputRange!Range) 186 { 187 alias ubyte C; 188 189 static if(is(Range : const(char)[])) 190 alias const(ubyte)[] R; 191 else 192 alias Range R; 193 194 static struct Quoter 195 { 196 private: 197 R data; 198 C override_; 199 200 public: 201 bool empty() 202 { 203 return override_ == C.init && data.empty; 204 } 205 206 C front() 207 { 208 if(override_ != C.init) 209 return override_; 210 211 auto front = data.front; 212 if(front == CtcpToken.delimiter) 213 return '\\'; 214 215 return front; 216 } 217 218 void popFront() 219 { 220 if(override_ != C.init) 221 { 222 override_ = C.init; 223 return; 224 } 225 226 char prev = data.front; 227 228 switch(prev) 229 { 230 case '\\': 231 override_ = '\\'; 232 break; 233 case CtcpToken.delimiter: 234 override_ = 'a'; 235 break; 236 default: 237 } 238 239 data.popFront(); 240 } 241 } 242 243 return Quoter(cast(R)payload); 244 } 245 246 unittest 247 { 248 import std.array : array; 249 250 assert(ctcpQuote("hello, world").array() == "hello, world"); 251 assert(ctcpQuote("\\hello, \x01world\x01").array() == `\\hello, \aworld\a`); 252 assert(ctcpQuote(`hello, \world\`).array() == `hello, \\world\\`); 253 } 254 255 /** 256 * Mid-level dequote a message. 257 * Returns: 258 * Input range for lazily dequoting the message 259 */ 260 auto ctcpDequote(Range)(Range quoted) 261 { 262 static if(is(Range : const(char)[])) 263 alias const(ubyte)[] R; 264 else 265 alias Range R; 266 267 static struct Dequoter 268 { 269 private: 270 R remaining; 271 bool wasQuote = false; 272 273 public: 274 bool empty() const pure 275 { 276 return remaining.empty; 277 } 278 279 char front() pure 280 { 281 auto front = remaining.front; 282 283 if(wasQuote) 284 { 285 switch(front) 286 { 287 case 'a': 288 return CtcpToken.delimiter; 289 default: 290 break; 291 } 292 } 293 294 return front; 295 } 296 297 private bool skipQuote() 298 { 299 if(!remaining.empty && remaining.front == '\\') 300 { 301 remaining.popFront(); 302 return !remaining.empty; 303 } 304 else 305 return false; 306 } 307 308 void popFront() 309 { 310 remaining.popFront(); 311 wasQuote = skipQuote(); 312 } 313 } 314 315 auto dequoter = Dequoter(cast(R)quoted); 316 dequoter.wasQuote = dequoter.skipQuote(); 317 return dequoter; 318 } 319 320 unittest 321 { 322 import std.algorithm : equal; 323 324 auto example = "Hi there!\nHow are you? \\K?"; 325 326 auto ctcpQuoted = example.ctcpQuote(); 327 auto lowQuoted = ctcpQuoted.lowQuote(); 328 329 auto lowDequoted = lowQuoted.array().lowDequote(); 330 auto ctcpDequoted = lowDequoted.array().ctcpDequote(); 331 332 assert(cast(string)ctcpQuoted.array() == "Hi there!\nHow are you? \\\\K?"); 333 assert(cast(string)lowQuoted.array() == "Hi there!\x10nHow are you? \\\\K?"); 334 335 assert(lowDequoted.equal(ctcpQuoted)); 336 assert(ctcpDequoted.array() == example); 337 } 338 339 ubyte[] delimBuffer = [CtcpToken.delimiter]; 340 341 public: 342 /** 343 * Create a CTCP message with the given tag and data, 344 * or with the _tag and _data provided pre-combined. 345 * Returns: 346 * Input range for producing the message 347 */ 348 auto ctcpMessage(in char[] tag, in char[] data) 349 { 350 alias const(ubyte)[] Ascii; 351 352 auto message = values(cast(Ascii)tag, cast(Ascii)data) 353 .joiner(cast(Ascii)" ") 354 .ctcpQuote() 355 .lowQuote(); 356 357 return chain(delimBuffer, message, delimBuffer).castRange!char; 358 } 359 360 /// Ditto 361 auto ctcpMessage(in char[] contents) 362 { 363 return chain(delimBuffer, contents.ctcpQuote().lowQuote(), delimBuffer).castRange!char; 364 } 365 366 /// 367 unittest 368 { 369 char[] msg; 370 371 msg = ctcpMessage("ACTION", "test \n123").array(); 372 assert(msg == "\x01ACTION test \x10n123\x01"); 373 374 msg = ctcpMessage("FINGER").array(); 375 assert(msg == "\x01FINGER\x01"); 376 377 msg = ctcpMessage("TEST", "\\test \x01 \r\n\0\x10").array(); 378 assert(msg == "\x01TEST \\\\test \\a \x10r\x10n\x100\x10\x10\x01"); 379 } 380 381 /** 382 * Extract CTCP messages from an IRC message. 383 * Returns: 384 * Range of CTCP messages, where each element is a range for producing the _message. 385 */ 386 auto ctcpExtract(in char[] message) 387 { 388 static struct Extractor 389 { 390 const(char)[] remaining; 391 size_t frontLength; 392 393 bool empty() const pure 394 { 395 return remaining.empty; 396 } 397 398 auto front() const 399 { 400 return remaining[0 .. frontLength - 1] 401 .ctcpDequote() 402 .lowDequote(); 403 } 404 405 private size_t findStandaloneDelim() pure 406 { 407 foreach(i, char c; remaining) 408 { 409 if(c == CtcpToken.delimiter) 410 { 411 if((i > 0 && remaining[i - 1] == CtcpToken.delimiter) || 412 (i < remaining.length - 1 && remaining[i + 1] == CtcpToken.delimiter)) 413 continue; 414 415 return i; 416 } 417 } 418 419 return remaining.length; 420 } 421 422 void popFront() pure 423 { 424 remaining = remaining[frontLength .. $]; 425 426 auto even = findStandaloneDelim(); 427 if(even == remaining.length) 428 { 429 remaining = null; 430 return; 431 } 432 433 remaining = remaining[even + 1 .. $]; 434 435 auto odd = findStandaloneDelim(); 436 if(odd == remaining.length) 437 { 438 remaining = null; 439 return; 440 } 441 442 frontLength = odd + 1; 443 } 444 } 445 446 auto extractor = Extractor(message); 447 extractor.popFront(); 448 449 return extractor; 450 } 451 452 unittest 453 { 454 // Chain is useless... 455 auto first = ctcpMessage("FINGER").array(); 456 auto second = ctcpMessage("TEST", "one\r\ntwo").array(); 457 458 auto allMsgs = cast(string)("head" ~ first ~ "mid" ~ second ~ "tail"); 459 460 auto r = allMsgs.ctcpExtract(); 461 assert(!r.empty); 462 463 assert(r.front.array() == "FINGER"); 464 465 r.popFront(); 466 assert(!r.empty); 467 468 assert(r.front.array() == "TEST one\r\ntwo"); 469 470 r.popFront(); 471 assert(r.empty); 472 473 allMsgs = "test"; 474 r = allMsgs.ctcpExtract(); 475 assert(r.empty); 476 477 allMsgs = "\x01test"; 478 r = allMsgs.ctcpExtract(); 479 assert(r.empty); 480 }