@@ -24,15 +24,82 @@ pub const ValidateError = error{
2424 InvalidHostName ,
2525};
2626
27+ /// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123)
2728pub fn validate (bytes : []const u8 ) ValidateError ! void {
28- if (bytes .len > max_len ) return error .NameTooLong ;
29- if (! std .unicode .utf8ValidateSlice (bytes )) return error .InvalidHostName ;
30- for (bytes ) | byte | {
31- if (! std .ascii .isAscii (byte ) or byte == '.' or byte == '-' or std .ascii .isAlphanumeric (byte )) {
32- continue ;
29+ if (bytes .len == 0 ) return error .InvalidHostName ;
30+ if (bytes [0 ] == '.' ) return error .InvalidHostName ;
31+
32+ // Ignore trailing dot (FQDN). It doesn't count toward our length.
33+ const end = if (bytes [bytes .len - 1 ] == '.' ) end : {
34+ if (bytes .len == 1 ) return error .InvalidHostName ;
35+ break :end bytes .len - 1 ;
36+ } else bytes .len ;
37+
38+ // The accepted maximum length of a hostname, including labels and dots.
39+ if (end > max_len ) return error .NameTooLong ;
40+
41+ // Hostnames are divided into dot-separated "labels", which:
42+ //
43+ // - Start with a letter or digit
44+ // - Can contain letters, digits, or hyphens
45+ // - Must end with a letter or digit
46+ // - Have a minimum of 1 character and a maximum of 63
47+ var label_start : usize = 0 ;
48+ var label_len : usize = 0 ;
49+ for (bytes [0.. end ], 0.. ) | c , i | {
50+ switch (c ) {
51+ '.' = > {
52+ if (label_len == 0 or label_len > 63 ) return error .InvalidHostName ;
53+ if (! std .ascii .isAlphanumeric (bytes [label_start ])) return error .InvalidHostName ;
54+ if (! std .ascii .isAlphanumeric (bytes [i - 1 ])) return error .InvalidHostName ;
55+
56+ label_start = i + 1 ;
57+ label_len = 0 ;
58+ },
59+ '-' = > {
60+ label_len += 1 ;
61+ },
62+ else = > {
63+ if (! std .ascii .isAlphanumeric (c )) return error .InvalidHostName ;
64+ label_len += 1 ;
65+ },
3366 }
34- return error .InvalidHostName ;
3567 }
68+
69+ // Validate the final label
70+ if (label_len == 0 or label_len > 63 ) return error .InvalidHostName ;
71+ if (! std .ascii .isAlphanumeric (bytes [label_start ])) return error .InvalidHostName ;
72+ if (! std .ascii .isAlphanumeric (bytes [end - 1 ])) return error .InvalidHostName ;
73+ }
74+
75+ test validate {
76+ // Valid hostnames
77+ try validate ("example" );
78+ try validate ("example.com" );
79+ try validate ("www.example.com" );
80+ try validate ("sub.domain.example.com" );
81+ try validate ("example.com." );
82+ try validate ("host-name.example.com." );
83+ try validate ("123.example.com." );
84+ try validate ("a-b.com" );
85+ try validate ("a.b.c.d.e.f.g" );
86+ try validate ("127.0.0.1" ); // Also a valid hostname
87+ try validate ("a" ** 63 ++ ".com" ); // Label exactly 63 chars (valid)
88+ try validate ("a." ** 127 ++ "a" ); // Total length 255 (valid)
89+
90+ // Invalid hostnames
91+ try std .testing .expectError (error .InvalidHostName , validate ("" ));
92+ try std .testing .expectError (error .InvalidHostName , validate (".example.com" ));
93+ try std .testing .expectError (error .InvalidHostName , validate ("example.com.." ));
94+ try std .testing .expectError (error .InvalidHostName , validate ("host..domain" ));
95+ try std .testing .expectError (error .InvalidHostName , validate ("-hostname" ));
96+ try std .testing .expectError (error .InvalidHostName , validate ("hostname-" ));
97+ try std .testing .expectError (error .InvalidHostName , validate ("a.-.b" ));
98+ try std .testing .expectError (error .InvalidHostName , validate ("host_name.com" ));
99+ try std .testing .expectError (error .InvalidHostName , validate ("." ));
100+ try std .testing .expectError (error .InvalidHostName , validate (".." ));
101+ try std .testing .expectError (error .InvalidHostName , validate ("a" ** 64 ++ ".com" )); // Label length 64 (too long)
102+ try std .testing .expectError (error .NameTooLong , validate ("a." ** 127 ++ "ab" )); // Total length 256 (too long)
36103}
37104
38105pub fn init (bytes : []const u8 ) ValidateError ! HostName {
0 commit comments