Skip to main content

vtcode_commons/
utils.rs

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