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