1 /+
2     This file is part of Reloaded Vibes.
3     Copyright (c) 2019  0xEAB
4 
5     Distributed under the Boost Software License, Version 1.0.
6        (See accompanying file LICENSE_1_0.txt or copy at
7              https://www.boost.org/LICENSE_1_0.txt)
8  +/
9 module reloadedvibes.utils;
10 
11 import std.algorithm : canFind, count;
12 import std.ascii : isDigit;
13 import std.conv : to;
14 import std.string : indexOf, isNumeric;
15 
16 @safe pure:
17 
18 struct Socket
19 {
20 	string address;
21 	ushort port;
22 
23 	string toString() const @safe pure nothrow
24 	{
25 		// dfmt off
26 		return (this.address.isIPv6)
27 			? '[' ~ this.address ~ "]:" ~ this.port.to!string
28 			: this.address ~ ':' ~ this.port.to!string;
29 		// dfmt on
30 	}
31 }
32 
33 /++
34 	Determines whether the passed string is an IPv6 address (and not an IPv4 one).
35 	Use this function to differentiate between IPv4 and IPv6 addresses.
36 
37 	Limitation:
38 		This functions does only very basic and cheap testing.
39 		It does not validate the IPv6 address at all.
40 		Never pass any sockets to it - IPv4 ones will get detected as IPv6 addresses.
41 		Do not use it for anything else than differentiating IPv4/IPv6 addresses.
42 
43 	Returns:
44 		true if the passed string could looks like an IPv6 address
45  +/
46 bool isIPv6(string address) nothrow @nogc
47 {
48 	foreach (c; address)
49 	{
50 		if (c == ':')
51 		{
52 			return true;
53 		}
54 	}
55 
56 	return false;
57 }
58 
59 unittest
60 {
61 	assert("127.0.0.1".isIPv6 == false);
62 	assert("::1".isIPv6);
63 }
64 
65 /++
66 	Tries to parse a socket string
67 
68 	Supports both IPv4 and IPv6.
69 	Does limited validating.
70 
71 	Returns:
72 		true if parsing was successfull,
73 		false indicates bad/invalid input
74  +/
75 bool tryParseSocket(string s, out Socket socket)
76 {
77 	socket = Socket();
78 
79 	if ((s is null) && (s.length == 0)) // validate
80 	{
81 		return false;
82 	}
83 
84 	immutable possiblePortSep = s.indexOf(':');
85 
86 	size_t isIPv6 = 0;
87 
88 	if (s[0] == '[')
89 	{
90 		// IPv6
91 
92 		immutable ipv6end = s.indexOf(']');
93 		if (ipv6end < 3)
94 		{
95 			return false;
96 		}
97 
98 		socket.address = s[1 .. ipv6end];
99 
100 		isIPv6 = s.indexOf(':', ipv6end);
101 	}
102 	else if (possiblePortSep > -1)
103 	{
104 		// IPv4
105 		socket.address = s[0 .. possiblePortSep];
106 
107 		if (socket.address.count!(c => c == '.') != 3) // validate
108 		{
109 			return false;
110 		}
111 	}
112 	else
113 	{
114 		return false;
115 	}
116 
117 	immutable portSep = (isIPv6) ? isIPv6 : possiblePortSep;
118 
119 	if (portSep == 0) // validate
120 	{
121 		return false;
122 	}
123 
124 	string port = s[(portSep + 1) .. $];
125 
126 	if (port.canFind!(d => !d.isDigit)() || (port.length > 5) || (port[0] == '-')) // validate
127 	{
128 		return false;
129 	}
130 
131 	immutable portInt = port.to!int;
132 	if (portInt > ushort.max) // validate
133 	{
134 		return false;
135 	}
136 
137 	socket.port = cast(ushort)(portInt);
138 	return true;
139 }
140 
141 unittest
142 {
143 	import std.conv : to;
144 	import std.typecons : tuple;
145 
146 	auto sockets = [
147 		// dfmt off
148 		tuple("127.0.0.1:3001", true, Socket("127.0.0.1", 3001)),
149 		tuple("1.2.3.4:56", true, Socket("1.2.3.4", 56)),
150 		tuple("127.0.0.1:123456", false, Socket()),
151 		tuple("[::1]:80", true, Socket("::1", 80)),
152 		tuple("[1]]:3001", false, Socket()),
153 		tuple("10.0.0.1", false, Socket()),
154 		tuple("[2001:db8:1234:0000:0000:0000:0000:0000]:443", true, Socket("2001:db8:1234:0000:0000:0000:0000:0000", 443)),
155 		tuple("[2001:db8::1]", false, Socket()),
156 		tuple(":1", false, Socket()),
157 		tuple("::11", false, Socket()),
158 		tuple("12.1:10", false, Socket()),
159 		tuple("1.2.3.4:-56", false, Socket()),
160 		// dfmt on
161 	];
162 
163 	foreach (idx, s; sockets)
164 	{
165 		Socket x;
166 
167 		immutable r = tryParseSocket(s[0], x);
168 
169 		assert(r == s[1], "Unexpected parser result: [" ~ idx.to!string ~ "] " ~ s[0] ~ " -> " ~ r.to!string);
170 
171 		if (r)
172 		{
173 			assert(x == s[2], "Wrongly parsed: [" ~ idx.to!string ~ "] " ~ s[0]);
174 		}
175 	}
176 }