Skip to main content

pingap_util/
lib.rs

1// Copyright 2024-2025 Tree xie.
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 base64::{Engine, engine::general_purpose::STANDARD};
16use path_absolutize::*;
17use snafu::Snafu;
18use std::path::Path;
19use substring::Substring;
20
21mod crypto;
22mod format;
23mod ip;
24
25pub use crypto::{aes_decrypt, aes_encrypt};
26pub use format::*;
27pub use ip::IpRules;
28
29/// Error enum for various error types in the utility module
30#[derive(Debug, Snafu)]
31pub enum Error {
32    #[snafu(display("Encrypt error {message}"))]
33    Aes { message: String },
34    #[snafu(display("Base64 decode {source}"))]
35    Base64Decode { source: base64::DecodeError },
36    #[snafu(display("Invalid {message}"))]
37    Invalid { message: String },
38    #[snafu(display("Io error {source}, {file}"))]
39    Io {
40        source: std::io::Error,
41        file: String,
42    },
43}
44
45type Result<T, E = Error> = std::result::Result<T, E>;
46
47const VERSION: &str = env!("CARGO_PKG_VERSION");
48
49/// Gets the package version.
50pub fn get_pkg_version() -> &'static str {
51    VERSION
52}
53
54/// Get the rustc version.
55pub fn get_rustc_version() -> String {
56    rustc_version_runtime::version().to_string()
57}
58
59/// Resolves a path string to its absolute form.
60/// If the path starts with '~', it will be expanded to the user's home directory.
61/// Returns an empty string if the input path is empty.
62///
63/// # Arguments
64/// * `path` - The path string to resolve
65///
66/// # Returns
67/// The absolute path as a String
68pub fn resolve_path(path: &str) -> String {
69    if path.is_empty() {
70        return "".to_string();
71    }
72    let mut p = path.to_string();
73    if p.starts_with('~')
74        && let Some(home) = dirs::home_dir()
75    {
76        p = home.to_string_lossy().to_string() + p.substring(1, p.len());
77    }
78    if let Ok(p) = Path::new(&p).absolutize() {
79        p.to_string_lossy().to_string()
80    } else {
81        p
82    }
83}
84
85/// Checks if a string represents a PEM-formatted certificate/key
86/// by looking for the "-----" prefix.
87///
88/// # Arguments
89/// * `value` - The string to check
90///
91/// # Returns
92/// true if the string appears to be PEM-formatted, false otherwise
93pub fn is_pem(value: &str) -> bool {
94    let value = value.trim();
95    if let (Some(begin_idx), Some(end_idx)) =
96        (value.find("-----BEGIN "), value.find("-----END "))
97    {
98        begin_idx < end_idx && value.ends_with("-----")
99    } else {
100        false
101    }
102}
103
104/// Converts various certificate/key formats into bytes.
105/// Supports PEM format, file paths, and base64-encoded data.
106///
107/// # Arguments
108/// * `value` - The certificate/key data as a string
109///
110/// # Returns
111/// Result containing the certificate/key bytes or an error
112pub fn convert_pem(value: &str) -> Result<Vec<Vec<u8>>> {
113    let buf = if is_pem(value) {
114        value.as_bytes().to_vec()
115    } else if Path::new(&resolve_path(value)).is_file() {
116        std::fs::read(resolve_path(value)).map_err(|e| Error::Io {
117            source: e,
118            file: value.to_string(),
119        })?
120    } else {
121        base64_decode(value).map_err(|e| Error::Base64Decode { source: e })?
122    };
123    let pems = pem::parse_many(&buf).map_err(|e| Error::Invalid {
124        message: e.to_string(),
125    })?;
126    if pems.is_empty() {
127        return Err(Error::Invalid {
128            message: "pem data is empty".to_string(),
129        });
130    }
131    let mut data = vec![];
132    for pem in pems {
133        data.push(pem::encode(&pem).as_bytes().to_vec());
134    }
135
136    Ok(data)
137}
138
139/// Converts an optional certificate string into bytes.
140/// Handles PEM format, file paths, and base64-encoded data.
141///
142/// # Arguments
143/// * `value` - Optional string containing the certificate data
144///
145/// # Returns
146/// Optional vector of bytes containing the certificate data
147pub fn convert_certificate_bytes(value: Option<&str>) -> Option<Vec<Vec<u8>>> {
148    if let Some(value) = value {
149        if value.is_empty() {
150            return None;
151        }
152        return convert_pem(value).ok();
153    }
154    None
155}
156
157pub fn base64_encode<T: AsRef<[u8]>>(data: T) -> String {
158    STANDARD.encode(data)
159}
160
161pub fn base64_decode<T: AsRef<[u8]>>(
162    data: T,
163) -> Result<Vec<u8>, base64::DecodeError> {
164    STANDARD.decode(data)
165}
166
167/// Removes empty tables/sections from a TOML string
168///
169/// # Arguments
170/// * `value` - TOML string to process
171///
172/// # Returns
173/// Result containing the processed TOML string with empty sections removed
174pub fn toml_omit_empty_value(value: &str) -> Result<String, Error> {
175    let mut data =
176        toml::from_str::<toml::Table>(value).map_err(|e| Error::Invalid {
177            message: e.to_string(),
178        })?;
179    let mut omit_keys = vec![];
180    for (key, value) in data.iter() {
181        let Some(table) = value.as_table() else {
182            omit_keys.push(key.to_string());
183            continue;
184        };
185        if table.keys().len() == 0 {
186            omit_keys.push(key.to_string());
187            continue;
188        }
189    }
190    for key in omit_keys {
191        data.remove(&key);
192    }
193    toml::to_string_pretty(&data).map_err(|e| Error::Invalid {
194        message: e.to_string(),
195    })
196}
197
198/// Joins two path segments with a forward slash
199/// Handles cases where segments already include slashes
200///
201/// # Arguments
202/// * `value1` - First path segment
203/// * `value2` - Second path segment
204///
205/// # Returns
206/// Joined path as a String
207pub fn path_join(value1: &str, value2: &str) -> String {
208    let end_slash = value1.ends_with("/");
209    let start_slash = value2.starts_with("/");
210    if end_slash && start_slash {
211        format!("{value1}{}", value2.substring(1, value2.len()))
212    } else if end_slash || start_slash {
213        format!("{value1}{value2}")
214    } else {
215        format!("{value1}/{value2}")
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::base64_encode;
223    use pretty_assertions::assert_eq;
224    use std::io::Write;
225    use tempfile::NamedTempFile;
226
227    #[test]
228    fn test_get_pkg_info() {
229        assert_eq!(false, get_pkg_version().is_empty());
230    }
231
232    #[test]
233    fn test_resolve_path() {
234        assert_eq!(
235            dirs::home_dir().unwrap().to_string_lossy(),
236            resolve_path("~/")
237        );
238    }
239    #[test]
240    fn test_get_rustc_version() {
241        assert_eq!(false, get_rustc_version().is_empty());
242    }
243
244    #[test]
245    fn test_path_join() {
246        assert_eq!("a/b", path_join("a", "b"));
247        assert_eq!("a/b", path_join("a/", "b"));
248        assert_eq!("a/b", path_join("a", "/b"));
249        assert_eq!("a/b", path_join("a/", "/b"));
250    }
251
252    #[test]
253    fn test_toml_omit_empty_value() {
254        let data = r###"
255        [upstreams.charts]
256        addrs = ["127.0.0.1:5000", "127.0.0.1:5001 10"]
257        [locations]
258        "###;
259        let result = toml_omit_empty_value(data).unwrap();
260        assert_eq!(
261            result,
262            r###"[upstreams.charts]
263addrs = [
264    "127.0.0.1:5000",
265    "127.0.0.1:5001 10",
266]
267"###
268        );
269    }
270
271    #[test]
272    fn test_convert_certificate_bytes() {
273        // spellchecker:off
274        let pem = r###"-----BEGIN CERTIFICATE-----
275MIID/TCCAmWgAwIBAgIQJUGCkB1VAYha6fGExkx0KTANBgkqhkiG9w0BAQsFADBV
276MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFTATBgNVBAsMDHZpY2Fu
277c29AdHJlZTEcMBoGA1UEAwwTbWtjZXJ0IHZpY2Fuc29AdHJlZTAeFw0yNDA3MDYw
278MjIzMzZaFw0yNjEwMDYwMjIzMzZaMEAxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
279bWVudCBjZXJ0aWZpY2F0ZTEVMBMGA1UECwwMdmljYW5zb0B0cmVlMIIBIjANBgkq
280hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5dbylSPQNARrpT/Rn7qZf6JmH3cueMp
281YdOpctuPYeefT0Jdgp67bg17fU5pfyR2BWYdwyvHCNmKqLdYPx/J69hwTiVFMOcw
282lVQJjbzSy8r5r2cSBMMsRaAZopRDnPy7Ls7Ji+AIT4vshUgL55eR7ACuIJpdtUYm
283TzMx9PTA0BUDkit6z7bTMaEbjDmciIBDfepV4goHmvyBJoYMIjnAwnTFRGRs/QJN
284d2ikFq999fRINzTDbRDP1K0Kk6+zYoFAiCMs9lEDymu3RmiWXBXpINR/Sv8CXtz2
2859RTVwTkjyiMOPY99qBfaZTiy+VCjcwTGKPyus1axRMff4xjgOBewOwIDAQABo14w
286XDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgw
287FoAUhU5Igu3uLUabIqUhUpVXjk1JVtkwFAYDVR0RBA0wC4IJcGluZ2FwLmlvMA0G
288CSqGSIb3DQEBCwUAA4IBgQDBimRKrqnEG65imKriM2QRCEfdB6F/eP9HYvPswuAP
289tvQ6m19/74qbtkd6vjnf6RhMbj9XbCcAJIhRdnXmS0vsBrLDsm2q98zpg6D04F2E
290L++xTiKU6F5KtejXcTHHe23ZpmD2XilwcVDeGFu5BEiFoRH9dmqefGZn3NIwnIeD
291Yi31/cL7BoBjdWku5Qm2nCSWqy12ywbZtQCbgbzb8Me5XZajeGWKb8r6D0Nb+9I9
292OG7dha1L3kxerI5VzVKSiAdGU0C+WcuxfsKAP8ajb1TLOlBaVyilfqmiF457yo/2
293PmTYzMc80+cQWf7loJPskyWvQyfmAnSUX0DI56avXH8LlQ57QebllOtKgMiCo7cr
294CCB2C+8hgRNG9ZmW1KU8rxkzoddHmSB8d6+vFqOajxGdyOV+aX00k3w6FgtHOoKD
295Ztdj1N0eTfn02pibVcXXfwESPUzcjERaMAGg1hoH1F4Gxg0mqmbySAuVRqNLnXp5
296CRVQZGgOQL6WDg3tUUDXYOs=
297-----END CERTIFICATE-----"###;
298        // spellchecker:on
299        let result = convert_certificate_bytes(Some(pem));
300        assert_eq!(true, result.is_some());
301
302        let mut tmp = NamedTempFile::new().unwrap();
303
304        tmp.write_all(pem.as_bytes()).unwrap();
305
306        let result = convert_certificate_bytes(
307            Some(tmp.path().to_string_lossy()).as_deref(),
308        );
309        assert_eq!(true, result.is_some());
310
311        let data = base64_encode(pem.as_bytes());
312        assert_eq!(1924, data.len());
313        let result = convert_certificate_bytes(Some(data).as_deref());
314        assert_eq!(true, result.is_some());
315    }
316}