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    crate::formatting::truncate_byte_budget(&excerpt, max_len, "...\n")
99}
100
101/// Safe text replacement with validation
102pub fn safe_replace_text(content: &str, old_str: &str, new_str: &str) -> Result<String> {
103    if old_str.is_empty() {
104        return Err(anyhow::anyhow!("old_string cannot be empty"));
105    }
106
107    if !content.contains(old_str) {
108        return Err(anyhow::anyhow!("Text '{old_str}' not found in content"));
109    }
110
111    Ok(content.replace(old_str, new_str))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn readme_excerpt_does_not_split_utf8() {
120        let markdown = "你".repeat(700);
121
122        assert_eq!(extract_readme_excerpt(&markdown, 1201), format!("{}...\n", "你".repeat(400)));
123    }
124}