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 }