1 module nyinaa.validators; 2 3 import std.regex : matchFirst, ctRegex; 4 5 /** 6 * Checks whether a string has an email address format 7 * 8 * Params: 9 * email = the email address to do check on 10 */ 11 bool isEmail(string email) 12 { 13 auto ctr = ctRegex!r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"; 14 15 return email.matchFirst(ctr).length == 1; 16 } 17 18 /// 19 unittest 20 { 21 assert(isEmail("hello@example.com")); 22 assert(!isEmail("nah!")); 23 } 24 25 /** 26 * Checks whether a string has an IP address format (both IPv4 and IPv6) 27 * 28 * Params: 29 * ip = the IP address to do check on 30 */ 31 bool isIP(string ip) 32 { 33 return isIPv4(ip) || isIPv6(ip); 34 } 35 36 /// 37 unittest 38 { 39 assert(isIP("192.169.0.0")); 40 assert(isIP("2001:db8:0:1:1:1:1:1")); 41 assert(!isIP("192.169.0.500")); 42 assert(!isIP("2001:db8:0:1:1:1:1:1xxx")); 43 } 44 45 /** 46 * Checks whether a string has an IPv4 address format 47 * 48 * Params: 49 * ip = the IPv4 address to do check on 50 */ 51 bool isIPv4(string ip) 52 { 53 auto ctr = ctRegex!( 54 `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`); 55 return ip.matchFirst(ctr).length == 1; 56 } 57 58 /// 59 unittest 60 { 61 assert(isIPv4("127.0.0.1")); 62 assert(!isIPv4("127.0.0.500")); 63 } 64 65 /** 66 * Checks whether a string has an IPv6 address format 67 * 68 * Params: 69 * ip = the IPv6 address to do check on 70 */ 71 bool isIPv6(string ip) 72 { 73 import std : canFind, split, asUpperCase, to; 74 75 auto ctr = ctRegex!(`^[0-9A-F]{1,4}$`); 76 77 // initial or final :: 78 if (ip == "::") 79 return true; 80 81 // ipv6 addresses could have scoped architecture 82 // according to https://tools.ietf.org/html/rfc4007#section-11 83 if (ip.canFind("%")) 84 { 85 string[2] addressAndZone = ip.split("%"); 86 87 // it must be just two parts 88 if (addressAndZone.length != 2) 89 return false; 90 91 // the first part must be the address 92 if (!addressAndZone[0].canFind(':')) 93 return false; 94 95 // the second part must not be empty 96 if (addressAndZone[1] == "") 97 return false; 98 } 99 100 // At least some OS accept the last 32 bits of an IPv6 address 101 // (i.e. 2 of the blocks) in IPv4 notation, and RFC 3493 says 102 // that '::ffff:a.b.c.d' is valid for IPv4-mapped IPv6 addresses, 103 // and '::a.b.c.d' is deprecated, but also valid. 104 immutable foundIPv4TransitionBlock = isIPv4(ip[0 .. $ - 1]); 105 immutable expectedNumberOfBlocks = foundIPv4TransitionBlock ? 7 : 8; 106 107 // expects at most 8 blocks 108 string[] blocks = ip.split(":"); 109 if (blocks.length > expectedNumberOfBlocks) 110 return false; 111 112 bool foundOmissionBlock = false; // marker to indicate :: 113 114 // has :: at start of ip 115 if (ip[0 .. 2] == "::") 116 { 117 blocks = blocks[0 .. 2]; 118 foundOmissionBlock = true; 119 } 120 else if (ip[$ - 2 .. $] == "::") 121 { 122 // has :: at end of ip 123 blocks = blocks[$ - 2 .. $]; 124 foundOmissionBlock = true; 125 } 126 127 foreach (i, blk; blocks) 128 { 129 // test for a :: which can not be at the start/end of ip 130 // since those cases have been handled above 131 if (blk == "" && i > 0 && i < (blocks.length - 1)) 132 { 133 if (foundOmissionBlock) 134 { 135 continue; // multiple :: in address 136 } 137 138 foundOmissionBlock = true; 139 } 140 141 if (!blk.asUpperCase.to!string.matchFirst(ctr).length == 1) 142 return false; 143 144 } 145 146 if (foundOmissionBlock) 147 return blocks.length >= 1; 148 149 return blocks.length == expectedNumberOfBlocks; 150 } 151 152 /// 153 unittest 154 { 155 assert(isIPv6("2001:db8:0:1:1:1:1:1")); 156 assert(!isIPv6("2001:db8:0:1:1:1:1:1xxx")); 157 }