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