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 }