prost_protovalidate/validators.rs
1//! Format validators for well-known string constraints.
2//!
3//! These validators implement the format checking logic used by `buf.validate`
4//! well-known string rules. They are exposed publicly so that generated
5//! validation code (from `prost-protovalidate-build`) can call them directly
6//! without reimplementing the validation logic.
7
8use crate::formats as internal;
9
10/// Returns `true` if `s` is a valid email address.
11///
12/// Checks for a non-empty local part and domain separated by `@`, with the
13/// domain being a valid hostname or IP address literal.
14#[inline]
15#[must_use]
16pub fn is_email(s: &str) -> bool {
17 internal::is_email(s)
18}
19
20/// Returns `true` if `s` is a valid hostname per RFC 1123.
21#[inline]
22#[must_use]
23pub fn is_hostname(s: &str) -> bool {
24 internal::is_hostname(s)
25}
26
27/// Returns `true` if `s` is a valid IPv4 or IPv6 address.
28#[inline]
29#[must_use]
30pub fn is_ip(s: &str) -> bool {
31 internal::is_ip(s)
32}
33
34/// Returns `true` if `s` is a valid IPv4 address.
35#[inline]
36#[must_use]
37pub fn is_ipv4(s: &str) -> bool {
38 internal::is_ipv4_strict(s)
39}
40
41/// Returns `true` if `s` is a valid IPv6 address (including zone IDs).
42#[inline]
43#[must_use]
44pub fn is_ipv6(s: &str) -> bool {
45 internal::is_ipv6(s)
46}
47
48/// Returns `true` if `s` is a valid absolute URI per RFC 3986.
49#[inline]
50#[must_use]
51pub fn is_uri(s: &str) -> bool {
52 internal::is_uri(s)
53}
54
55/// Returns `true` if `s` is a valid URI reference (absolute or relative) per RFC 3986.
56#[inline]
57#[must_use]
58pub fn is_uri_ref(s: &str) -> bool {
59 internal::is_uri_ref(s)
60}
61
62/// Returns `true` if `s` is a valid UUID in `8-4-4-4-12` hex format.
63#[inline]
64#[must_use]
65pub fn is_uuid(s: &str) -> bool {
66 internal::is_uuid(s)
67}
68
69/// Returns `true` if `s` is a valid trimmed UUID (32 hex digits, no hyphens).
70#[inline]
71#[must_use]
72pub fn is_tuuid(s: &str) -> bool {
73 internal::is_tuuid(s)
74}
75
76/// Returns `true` if `s` is a valid ULID (26 Crockford base32 characters).
77#[inline]
78#[must_use]
79pub fn is_ulid(s: &str) -> bool {
80 internal::is_ulid(s)
81}
82
83/// Returns `true` if `s` is a valid Protobuf fully-qualified name without a
84/// leading dot (dot-separated `[A-Za-z_][A-Za-z0-9_]*` identifiers, e.g.
85/// `foo.bar.Baz`).
86#[inline]
87#[must_use]
88pub fn is_protobuf_fqn(s: &str) -> bool {
89 internal::is_protobuf_fqn(s)
90}
91
92/// Returns `true` if `s` is a valid Protobuf fully-qualified name with a
93/// leading dot (e.g. `.foo.bar.Baz`).
94#[inline]
95#[must_use]
96pub fn is_protobuf_dot_fqn(s: &str) -> bool {
97 internal::is_protobuf_dot_fqn(s)
98}
99
100/// Returns `true` if `s` is a valid IPv4 or IPv6 CIDR prefix (e.g. `192.168.0.0/16`).
101///
102/// When `strict` is `true`, host bits beyond the prefix length must be zero.
103#[inline]
104#[must_use]
105pub fn is_ip_prefix(s: &str, strict: bool) -> bool {
106 internal::is_ipv4_prefix(s, strict) || internal::is_ipv6_prefix(s, strict)
107}
108
109/// Returns `true` if `s` is a valid IPv4 CIDR prefix (e.g. `10.0.0.0/8`).
110///
111/// When `strict` is `true`, host bits beyond the prefix length must be zero.
112#[inline]
113#[must_use]
114pub fn is_ipv4_prefix(s: &str, strict: bool) -> bool {
115 internal::is_ipv4_prefix(s, strict)
116}
117
118/// Returns `true` if `s` is a valid IPv6 CIDR prefix (e.g. `2001:db8::/32`).
119///
120/// When `strict` is `true`, host bits beyond the prefix length must be zero.
121#[inline]
122#[must_use]
123pub fn is_ipv6_prefix(s: &str, strict: bool) -> bool {
124 internal::is_ipv6_prefix(s, strict)
125}
126
127/// Returns `true` if `s` is a valid `host:port` pair.
128///
129/// When `port_required` is `true`, the port component must be present.
130#[inline]
131#[must_use]
132pub fn is_host_and_port(s: &str, port_required: bool) -> bool {
133 internal::is_host_and_port(s, port_required)
134}
135
136/// Returns `true` if `s` is a valid HTTP header name.
137///
138/// When `strict` is `true`, validates against RFC 7230 token characters.
139/// When `strict` is `false`, only rejects NUL, CR, and LF.
140#[inline]
141#[must_use]
142pub fn is_http_header_name(s: &str, strict: bool) -> bool {
143 if strict {
144 internal::is_valid_http_header_name_strict(s)
145 } else {
146 internal::is_valid_http_header_name_loose(s)
147 }
148}
149
150/// Returns `true` if `s` is a valid HTTP header value.
151///
152/// When `strict` is `true`, rejects NUL, control chars (except HT), and DEL.
153/// When `strict` is `false`, only rejects NUL, CR, and LF.
154#[inline]
155#[must_use]
156pub fn is_http_header_value(s: &str, strict: bool) -> bool {
157 if strict {
158 internal::is_valid_http_header_value_strict(s)
159 } else {
160 internal::is_valid_http_header_value_loose(s)
161 }
162}
163
164/// Returns `true` if `path` is covered by `candidate` under `FieldMask`
165/// path-coverage semantics — either `path` equals `candidate`, or
166/// `candidate` is a prefix of `path` at a path-segment boundary
167/// (i.e. `path == "{candidate}.{rest}"`).
168///
169/// Allocation-free; used by both the runtime evaluator and generated
170/// `field_mask.in` / `field_mask.not_in` checks.
171#[inline]
172#[must_use]
173pub fn fieldmask_covers(candidate: &str, path: &str) -> bool {
174 path == candidate
175 || (path.len() > candidate.len()
176 && path.starts_with(candidate)
177 && path.as_bytes()[candidate.len()] == b'.')
178}
179
180#[cfg(test)]
181mod tests {
182 use super::fieldmask_covers;
183
184 #[test]
185 fn fieldmask_covers_exact() {
186 assert!(fieldmask_covers("user.email", "user.email"));
187 }
188
189 #[test]
190 fn fieldmask_covers_subpath() {
191 assert!(fieldmask_covers("user", "user.email"));
192 assert!(fieldmask_covers("user.profile", "user.profile.name"));
193 }
194
195 #[test]
196 fn fieldmask_covers_rejects_partial_segment() {
197 // "user" does not cover "username" — the boundary character is `e`, not `.`.
198 assert!(!fieldmask_covers("user", "username"));
199 // Likewise for deeper segments.
200 assert!(!fieldmask_covers("user.email", "user.emailaddress"));
201 }
202
203 #[test]
204 fn fieldmask_covers_rejects_shorter_path() {
205 // candidate longer than path can never cover it.
206 assert!(!fieldmask_covers("user.email", "user"));
207 }
208
209 #[test]
210 fn fieldmask_covers_empty_strings() {
211 // Empty candidate covers any path that starts with `.` (degenerate but
212 // consistent with the algebra); equal-empty covers empty.
213 assert!(fieldmask_covers("", ""));
214 assert!(!fieldmask_covers("user", ""));
215 }
216}