Skip to main content

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::validator::rules::string 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    s.parse::<std::net::Ipv4Addr>().is_ok()
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 IPv4 or IPv6 CIDR prefix (e.g. `192.168.0.0/16`).
84///
85/// When `strict` is `true`, host bits beyond the prefix length must be zero.
86#[inline]
87#[must_use]
88pub fn is_ip_prefix(s: &str, strict: bool) -> bool {
89    internal::is_ipv4_prefix(s, strict) || internal::is_ipv6_prefix(s, strict)
90}
91
92/// Returns `true` if `s` is a valid IPv4 CIDR prefix (e.g. `10.0.0.0/8`).
93///
94/// When `strict` is `true`, host bits beyond the prefix length must be zero.
95#[inline]
96#[must_use]
97pub fn is_ipv4_prefix(s: &str, strict: bool) -> bool {
98    internal::is_ipv4_prefix(s, strict)
99}
100
101/// Returns `true` if `s` is a valid IPv6 CIDR prefix (e.g. `2001:db8::/32`).
102///
103/// When `strict` is `true`, host bits beyond the prefix length must be zero.
104#[inline]
105#[must_use]
106pub fn is_ipv6_prefix(s: &str, strict: bool) -> bool {
107    internal::is_ipv6_prefix(s, strict)
108}
109
110/// Returns `true` if `s` is a valid `host:port` pair.
111///
112/// When `port_required` is `true`, the port component must be present.
113#[inline]
114#[must_use]
115pub fn is_host_and_port(s: &str, port_required: bool) -> bool {
116    internal::is_host_and_port(s, port_required)
117}
118
119/// Returns `true` if `s` is a valid HTTP header name.
120///
121/// When `strict` is `true`, validates against RFC 7230 token characters.
122/// When `strict` is `false`, only rejects NUL, CR, and LF.
123#[inline]
124#[must_use]
125pub fn is_http_header_name(s: &str, strict: bool) -> bool {
126    if strict {
127        internal::is_valid_http_header_name_strict(s)
128    } else {
129        internal::is_valid_http_header_name_loose(s)
130    }
131}
132
133/// Returns `true` if `s` is a valid HTTP header value.
134///
135/// When `strict` is `true`, rejects NUL, control chars (except HT), and DEL.
136/// When `strict` is `false`, only rejects NUL, CR, and LF.
137#[inline]
138#[must_use]
139pub fn is_http_header_value(s: &str, strict: bool) -> bool {
140    if strict {
141        internal::is_valid_http_header_value_strict(s)
142    } else {
143        internal::is_valid_http_header_value_loose(s)
144    }
145}
146
147/// Returns `true` if `path` is covered by `candidate` under `FieldMask`
148/// path-coverage semantics — either `path` equals `candidate`, or
149/// `candidate` is a prefix of `path` at a path-segment boundary
150/// (i.e. `path == "{candidate}.{rest}"`).
151///
152/// Allocation-free; used by both the runtime evaluator and generated
153/// `field_mask.in` / `field_mask.not_in` checks.
154#[inline]
155#[must_use]
156pub fn fieldmask_covers(candidate: &str, path: &str) -> bool {
157    path == candidate
158        || (path.len() > candidate.len()
159            && path.starts_with(candidate)
160            && path.as_bytes()[candidate.len()] == b'.')
161}
162
163#[cfg(test)]
164mod tests {
165    use super::fieldmask_covers;
166
167    #[test]
168    fn fieldmask_covers_exact() {
169        assert!(fieldmask_covers("user.email", "user.email"));
170    }
171
172    #[test]
173    fn fieldmask_covers_subpath() {
174        assert!(fieldmask_covers("user", "user.email"));
175        assert!(fieldmask_covers("user.profile", "user.profile.name"));
176    }
177
178    #[test]
179    fn fieldmask_covers_rejects_partial_segment() {
180        // "user" does not cover "username" — the boundary character is `e`, not `.`.
181        assert!(!fieldmask_covers("user", "username"));
182        // Likewise for deeper segments.
183        assert!(!fieldmask_covers("user.email", "user.emailaddress"));
184    }
185
186    #[test]
187    fn fieldmask_covers_rejects_shorter_path() {
188        // candidate longer than path can never cover it.
189        assert!(!fieldmask_covers("user.email", "user"));
190    }
191
192    #[test]
193    fn fieldmask_covers_empty_strings() {
194        // Empty candidate covers any path that starts with `.` (degenerate but
195        // consistent with the algebra); equal-empty covers empty.
196        assert!(fieldmask_covers("", ""));
197        assert!(!fieldmask_covers("user", ""));
198    }
199}