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 }