risingwave_common/system_param/
state_store_url.rs1use std::convert::Infallible;
16use std::fmt;
17use std::str::FromStr;
18
19use serde::{Deserialize, Serialize};
20
21#[derive(Clone, Deserialize)]
22#[serde(transparent)]
23pub struct StateStoreUrl<T = String>(T);
24
25pub type StateStoreUrlRef<'a> = StateStoreUrl<&'a str>;
26
27impl<T> From<T> for StateStoreUrl<T> {
28 fn from(value: T) -> Self {
29 Self(value)
30 }
31}
32
33impl<T: AsRef<str>> StateStoreUrl<T> {
34 pub fn expose(&self) -> &str {
36 self.0.as_ref()
37 }
38}
39
40impl<A, B> PartialEq<StateStoreUrl<B>> for StateStoreUrl<A>
41where
42 A: AsRef<str>,
43 B: AsRef<str>,
44{
45 fn eq(&self, other: &StateStoreUrl<B>) -> bool {
46 self.expose() == other.expose()
47 }
48}
49
50impl<T: AsRef<str>> Eq for StateStoreUrl<T> {}
51
52impl<T: AsRef<str>> fmt::Display for StateStoreUrl<T> {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write_redacted_state_store_url(f, self.expose())
55 }
56}
57
58impl<T: AsRef<str>> fmt::Debug for StateStoreUrl<T> {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 fmt::Display::fmt(self, f)
61 }
62}
63
64impl<T: AsRef<str>> Serialize for StateStoreUrl<T> {
65 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
66 where
67 S: serde::Serializer,
68 {
69 serializer.collect_str(self)
70 }
71}
72
73impl FromStr for StateStoreUrl {
74 type Err = Infallible;
75
76 fn from_str(value: &str) -> Result<Self, Self::Err> {
77 Ok(Self(value.to_owned()))
78 }
79}
80
81impl<T: AsRef<str>> From<StateStoreUrl<T>> for String {
82 fn from(value: StateStoreUrl<T>) -> Self {
83 value.expose().to_owned()
84 }
85}
86
87impl From<StateStoreUrlRef<'_>> for StateStoreUrl {
88 fn from(value: StateStoreUrlRef<'_>) -> Self {
89 Self(value.expose().to_owned())
90 }
91}
92
93fn write_redacted_state_store_url(f: &mut fmt::Formatter<'_>, url: &str) -> fmt::Result {
94 let Some(scheme_end) = url.find("://") else {
95 return f.write_str(url);
96 };
97
98 let authority_start = scheme_end + 3;
99 let authority_end = url[authority_start..]
100 .find(['/', '?', '#'])
101 .map_or(url.len(), |offset| authority_start + offset);
102
103 let authority = &url[authority_start..authority_end];
104 let Some(user_info_end) = authority.rfind('@') else {
105 return f.write_str(url);
106 };
107
108 let user_info = &authority[..user_info_end];
109
110 f.write_str(&url[..authority_start])?;
111
112 if user_info.contains(':') {
113 f.write_str("****:****")?;
114 } else {
115 f.write_str("****")?;
116 }
117
118 f.write_str(&authority[user_info_end..])?;
119 f.write_str(&url[authority_end..])
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_redact_credentials() {
128 let url: StateStoreUrl = "hummock+minio://admin:secret@localhost:9000/bucket"
129 .parse()
130 .unwrap();
131
132 assert_eq!(
133 url.to_string(),
134 "hummock+minio://****:****@localhost:9000/bucket"
135 );
136 assert_eq!(
137 url.expose(),
138 "hummock+minio://admin:secret@localhost:9000/bucket"
139 );
140 }
141
142 #[test]
143 fn test_url_without_credentials_is_unchanged() {
144 let url: StateStoreUrl = "hummock+s3://bucket-name".parse().unwrap();
145
146 assert_eq!(url.to_string(), "hummock+s3://bucket-name");
147 }
148
149 #[test]
150 fn test_redact_username_without_password() {
151 let url: StateStoreUrl = "hummock+minio://admin@localhost/bucket".parse().unwrap();
152
153 assert_eq!(url.to_string(), "hummock+minio://****@localhost/bucket");
154 }
155
156 #[test]
157 fn test_redact_encoded_credentials() {
158 let url: StateStoreUrl = "hummock+minio://user:p%40ss@localhost/bucket"
159 .parse()
160 .unwrap();
161
162 assert_eq!(
163 url.to_string(),
164 "hummock+minio://****:****@localhost/bucket"
165 );
166 }
167}