tibba_util/
string.rs

1// Copyright 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 super::timestamp;
16use hex::encode;
17use nanoid::nanoid;
18use sha2::{Digest, Sha256};
19use std::time::{SystemTime, UNIX_EPOCH};
20use tibba_error::Error;
21use uuid::{NoContext, Timestamp, Uuid};
22
23type Result<T> = std::result::Result<T, Error>;
24
25const SIGNATURE_TTL_SECS: i64 = 5 * 60; // 5 minutes
26
27/// Generates a UUIDv7 string
28///
29/// Creates a time-based UUID (version 7) using the current system time
30/// Format: xxxxxxxx-xxxx-7xxx-xxxx-xxxxxxxxxxxx
31///
32/// # Returns
33/// * String containing the formatted UUID
34///
35/// # Note
36/// UUIDv7 provides:
37/// - Timestamp-based ordering
38/// - Monotonic ordering within the same timestamp
39/// - Standards compliance
40pub fn uuid() -> String {
41    let d = SystemTime::now()
42        .duration_since(UNIX_EPOCH)
43        .unwrap_or_default();
44    let ts = Timestamp::from_unix(NoContext, d.as_secs(), d.subsec_nanos());
45    Uuid::new_v7(ts).to_string()
46}
47
48/// Generates a NanoID string of specified length
49///
50/// Creates a URL-safe, unique string using NanoID algorithm
51///
52/// # Arguments
53/// * `size` - Length of the generated ID
54///
55/// # Returns
56/// * String containing the NanoID
57///
58/// # Note
59/// NanoID provides:
60/// - URL-safe characters
61/// - Configurable length
62/// - High collision resistance
63pub fn nanoid(size: usize) -> String {
64    nanoid!(size)
65}
66
67/// Formats a floating-point number with specified precision
68///
69/// Converts float to string with fixed number of decimal places
70/// Supports precision from 0 to 4 decimal places
71///
72/// # Arguments
73/// * `value` - Floating point number to format
74/// * `precision` - Number of decimal places (0-4)
75///
76/// # Returns
77/// * String containing formatted number
78///
79/// # Examples
80/// ```
81/// assert_eq!("1.12", float_to_fixed(1.123412, 2));
82/// assert_eq!("1", float_to_fixed(1.123412, 0));
83/// ```
84pub fn float_to_fixed(value: f64, precision: usize) -> String {
85    let p = precision.min(4);
86    format!("{value:.p$}")
87}
88
89fn sha256_multi(parts: &[&[u8]]) -> String {
90    let mut hasher = Sha256::new();
91    for part in parts {
92        hasher.update(part);
93    }
94    encode(hasher.finalize())
95}
96
97/// Computes the SHA-256 hash of the input data
98///
99/// # Arguments
100/// * `data` - Input data to hash
101///
102/// # Returns
103/// * String containing the SHA-256 hash
104pub fn sha256(data: &[u8]) -> String {
105    sha256_multi(&[data])
106}
107
108/// Computes the SHA-256 hash of the input data and signs it with the secret key
109///
110/// # Arguments
111/// * `value` - Input data to hash
112/// * `secret` - Secret key
113///
114/// # Returns
115/// * String containing the signed hash
116pub fn sign_hash(value: &str, secret: &str) -> String {
117    sha256_multi(&[value.as_bytes(), b":", secret.as_bytes()])
118}
119
120/// Computes the SHA-256 hash of the input data and signs it with the secret key
121///
122/// # Arguments
123/// * `value` - Input data to hash
124/// * `secret` - Secret key
125///
126/// # Returns
127/// * Tuple containing the timestamp and the signed hash
128pub fn timestamp_hash(value: &str, secret: &str) -> (i64, String) {
129    let ts = timestamp();
130    let ts_str = ts.to_string();
131    let hash = sha256_multi(&[
132        ts_str.as_bytes(),
133        b":",
134        value.as_bytes(),
135        b":",
136        secret.as_bytes(),
137    ]);
138    (ts, hash)
139}
140
141/// Validates the signature of the input data
142///
143/// # Arguments
144/// * `value` - Input data to hash
145/// * `hash` - Signature to validate
146/// * `secret` - Secret key
147///
148/// # Returns
149/// * Result containing the validation result
150pub fn validate_sign_hash(value: &str, hash: &str, secret: &str) -> Result<()> {
151    if sign_hash(value, secret) != hash {
152        return Err(Error::new("signature is invalid").with_category("sign_hash"));
153    }
154    Ok(())
155}
156
157/// Validates the signature of the input data
158///
159/// # Arguments
160/// * `ts` - Timestamp
161/// * `value` - Input data to hash
162/// * `hash` - Signature to validate
163/// * `secret` - Secret key
164///
165/// # Returns
166/// * Result containing the validation result
167pub fn validate_timestamp_hash(ts: i64, value: &str, hash: &str, secret: &str) -> Result<()> {
168    let category = "timestamp_hash";
169    if (timestamp() - ts).abs() > SIGNATURE_TTL_SECS {
170        return Err(Error::new("signature is expired").with_category(category));
171    }
172    let ts_str = ts.to_string();
173    let expected_hash = sha256_multi(&[
174        ts_str.as_bytes(),
175        b":",
176        value.as_bytes(),
177        b":",
178        secret.as_bytes(),
179    ]);
180
181    if expected_hash != hash {
182        return Err(Error::new("signature is invalid").with_category(category));
183    }
184    Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189    use super::float_to_fixed;
190    use pretty_assertions::assert_eq;
191
192    /// Tests float_to_fixed function with various precisions
193    #[test]
194    fn to_fixed() {
195        assert_eq!("1", float_to_fixed(1.123412, 0));
196        assert_eq!("1.1", float_to_fixed(1.123412, 1));
197        assert_eq!("1.12", float_to_fixed(1.123412, 2));
198        assert_eq!("1.123", float_to_fixed(1.123412, 3));
199        assert_eq!("1.1234", float_to_fixed(1.123412, 4));
200    }
201}