Skip to main content

risingwave_common/system_param/
state_store_url.rs

1// Copyright 2026 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Returns the original URL, including credentials.
35    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}