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 SHA256 hash of the given content
24pub fn calculate_sha256(content: &[u8]) -> String {
25    let mut hasher = Sha256::new();
26    hasher.update(content);
27    let digest = hasher.finalize();
28    let mut output = String::with_capacity(digest.len() * 2);
29
30    for byte in digest {
31        output.push(nibble_to_hex(byte >> 4));
32        output.push(nibble_to_hex(byte & 0x0f));
33    }
34
35    output
36}
37
38#[allow(clippy::unreachable)]
39fn nibble_to_hex(nibble: u8) -> char {
40    match nibble {
41        0..=9 => char::from(b'0' + nibble),
42        10..=15 => char::from(b'a' + (nibble - 10)),
43        _ => unreachable!("nibble must be in 0..=15"),
44    }
45}
46
47/// Extract a string value from a simple TOML key assignment within the `[package]` section
48pub fn extract_toml_str(content: &str, key: &str) -> Option<String> {
49    // Only consider the [package] section to avoid matching other tables
50    let pkg_section = if let Some(start) = content.find("[package]") {
51        let rest = &content[start + "[package]".len()..];
52        // Stop at next section header or end
53        if let Some(_next) = rest.find('\n') {
54            &content[start..]
55        } else {
56            &content[start..]
57        }
58    } else {
59        content
60    };
61
62    // Example target: name = "vtcode"
63    let pattern = format!(r#"(?m)^\s*{}\s*=\s*"([^"]+)"\s*$"#, regex::escape(key));
64    let re = Regex::new(&pattern).ok()?;
65    re.captures(pkg_section)
66        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_owned()))
67}
68
69/// Get the first meaningful section of the README/markdown as an excerpt
70pub fn extract_readme_excerpt(md: &str, max_len: usize) -> String {
71    // Take from start until we pass the first major sections or hit max_len
72    let mut excerpt = String::new();
73    for line in md.lines() {
74        // Stop if we reach a deep section far into the doc
75        if excerpt.len() > max_len {
76            break;
77        }
78        excerpt.push_str(line);
79        excerpt.push('\n');
80        // Prefer stopping after an initial overview section
81        if line.trim().starts_with("## ") && excerpt.len() > (max_len / 2) {
82            break;
83        }
84    }
85    if excerpt.len() > max_len {
86        excerpt.truncate(max_len);
87        excerpt.push_str("...\n");
88    }
89    excerpt
90}
91
92/// Safe text replacement with validation
93pub fn safe_replace_text(content: &str, old_str: &str, new_str: &str) -> Result<String> {
94    if old_str.is_empty() {
95        return Err(anyhow::anyhow!("old_string cannot be empty"));
96    }
97
98    if !content.contains(old_str) {
99        return Err(anyhow::anyhow!("Text '{}' not found in content", old_str));
100    }
101
102    Ok(content.replace(old_str, new_str))
103}