Skip to main content

vtcode_commons/
utils.rs

1#![expect(
2    clippy::string_slice,
3    reason = "Cargo manifest offsets come from ASCII markers and are therefore UTF-8 boundaries."
4)]
5
6//! Generic utility functions
7
8use anyhow::{Context, Result};
9use regex::Regex;
10use sha2::{Digest, Sha256};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// Get current Unix timestamp in seconds
14#[inline]
15pub fn current_timestamp() -> u64 {
16    current_timestamp_result().unwrap_or(0)
17}
18
19/// Get current Unix timestamp in seconds as a fallible operation.
20#[inline]
21fn current_timestamp_result() -> Result<u64> {
22    Ok(SystemTime::now()
23        .duration_since(UNIX_EPOCH)
24        .context("System clock is before UNIX_EPOCH while generating timestamp")?
25        .as_secs())
26}
27
28/// Calculate the SHA256 hash of `content` and return it as a 64-character
29/// lowercase hex string (the standard hex encoding of the 32-byte digest).
30///
31/// Use this helper whenever a caller needs a stable, ASCII-safe fingerprint
32/// of arbitrary bytes - for example, hashing file contents for change
33/// detection, config fingerprints, or cache keys.
34pub fn calculate_sha256(content: &[u8]) -> String {
35    let mut hasher = Sha256::new();
36    hasher.update(content);
37    let digest = hasher.finalize();
38    let mut output = String::with_capacity(digest.len() * 2);
39
40    for byte in digest {
41        output.push(nibble_to_hex(byte >> 4));
42        output.push(nibble_to_hex(byte & 0x0f));
43    }
44
45    output
46}
47
48#[allow(
49    clippy::unreachable,
50    reason = "Intentional compatibility, platform, or test-only suppression."
51)]
52fn nibble_to_hex(nibble: u8) -> char {
53    match nibble {
54        0..=9 => char::from(b'0' + nibble),
55        10..=15 => char::from(b'a' + (nibble - 10)),
56        _ => unreachable!("nibble must be in 0..=15"),
57    }
58}
59
60/// Extract a string value from a simple TOML key assignment within the `[package]` section
61pub fn extract_toml_str(content: &str, key: &str) -> Option<String> {
62    // Only consider the [package] section to avoid matching other tables
63    let pkg_section = if let Some(start) = content.find("[package]") {
64        let rest = &content[start + "[package]".len()..];
65        // Stop at next section header or end
66        if let Some(_next) = rest.find('\n') {
67            &content[start..]
68        } else {
69            &content[start..]
70        }
71    } else {
72        content
73    };
74
75    // Example target: name = "vtcode"
76    let pattern = format!(r#"(?m)^\s*{}\s*=\s*"([^"]+)"\s*$"#, regex::escape(key));
77    let re = Regex::new(&pattern).ok()?;
78    re.captures(pkg_section)
79        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_owned()))
80}
81
82/// Get the first meaningful section of the README/markdown as an excerpt
83pub fn extract_readme_excerpt(md: &str, max_len: usize) -> String {
84    // Take from start until we pass the first major sections or hit max_len
85    let mut excerpt = String::with_capacity(max_len.min(md.len()));
86    for line in md.lines() {
87        // Stop if we reach a deep section far into the doc
88        if excerpt.len() > max_len {
89            break;
90        }
91        excerpt.push_str(line);
92        excerpt.push('\n');
93        // Prefer stopping after an initial overview section
94        if line.trim().starts_with("## ") && excerpt.len() > (max_len / 2) {
95            break;
96        }
97    }
98    if excerpt.len() > max_len {
99        excerpt.truncate(max_len);
100        excerpt.push_str("...\n");
101    }
102    excerpt
103}
104
105/// Safe text replacement with validation
106pub fn safe_replace_text(content: &str, old_str: &str, new_str: &str) -> Result<String> {
107    if old_str.is_empty() {
108        return Err(anyhow::anyhow!("old_string cannot be empty"));
109    }
110
111    if !content.contains(old_str) {
112        return Err(anyhow::anyhow!("Text '{old_str}' not found in content"));
113    }
114
115    Ok(content.replace(old_str, new_str))
116}