1#![expect(
2 clippy::string_slice,
3 reason = "Cargo manifest offsets come from ASCII markers and are therefore UTF-8 boundaries."
4)]
5
6use anyhow::{Context, Result};
9use regex::Regex;
10use sha2::{Digest, Sha256};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[inline]
15pub fn current_timestamp() -> u64 {
16 current_timestamp_result().unwrap_or(0)
17}
18
19#[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
28pub 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
60pub fn extract_toml_str(content: &str, key: &str) -> Option<String> {
62 let pkg_section = if let Some(start) = content.find("[package]") {
64 let rest = &content[start + "[package]".len()..];
65 if let Some(_next) = rest.find('\n') {
67 &content[start..]
68 } else {
69 &content[start..]
70 }
71 } else {
72 content
73 };
74
75 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
82pub fn extract_readme_excerpt(md: &str, max_len: usize) -> String {
84 let mut excerpt = String::with_capacity(max_len.min(md.len()));
86 for line in md.lines() {
87 if excerpt.len() > max_len {
89 break;
90 }
91 excerpt.push_str(line);
92 excerpt.push('\n');
93 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#[must_use]
113pub fn env_flag_enabled(var_name: &str) -> bool {
114 std::env::var(var_name).ok().is_some_and(|value| {
115 matches!(value.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on" | "debug")
116 })
117}
118
119#[must_use]
126pub fn parse_bool_env_value(value: &str) -> Option<bool> {
127 match value.trim().to_ascii_lowercase().as_str() {
128 "1" | "true" | "yes" | "on" => Some(true),
129 "0" | "false" | "no" | "off" => Some(false),
130 _ => None,
131 }
132}
133
134#[must_use]
144pub fn parse_env_bool(name: &str, default: bool) -> bool {
145 std::env::var(name)
146 .ok()
147 .and_then(|value| parse_bool_env_value(&value))
148 .unwrap_or(default)
149}
150
151pub fn safe_replace_text(content: &str, old_str: &str, new_str: &str) -> Result<String> {
153 if old_str.is_empty() {
154 return Err(anyhow::anyhow!("old_string cannot be empty"));
155 }
156
157 if !content.contains(old_str) {
158 return Err(anyhow::anyhow!("Text '{old_str}' not found in content"));
159 }
160
161 Ok(content.replace(old_str, new_str))
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn readme_excerpt_does_not_split_utf8() {
170 let markdown = "你".repeat(700);
171
172 assert_eq!(extract_readme_excerpt(&markdown, 1201), format!("{}...\n", "你".repeat(400)));
173 }
174
175 #[test]
176 fn parse_bool_env_value_accepts_canonical_sets() {
177 for truthy in ["1", "true", "YES", " On ", "on"] {
178 assert_eq!(parse_bool_env_value(truthy), Some(true), "{truthy}");
179 }
180 for falsy in ["0", "false", "NO", " Off ", "off"] {
181 assert_eq!(parse_bool_env_value(falsy), Some(false), "{falsy}");
182 }
183 for unrecognized in ["", "debug", "2", "maybe"] {
184 assert_eq!(parse_bool_env_value(unrecognized), None, "{unrecognized}");
185 }
186 }
187
188 #[test]
189 fn parse_env_bool_falls_back_to_default_when_unset_or_unrecognized() {
190 assert!(!parse_env_bool("VTCODE_ENV_BOOL_TEST_DEFINITELY_UNSET", false));
191 assert!(parse_env_bool("VTCODE_ENV_BOOL_TEST_DEFINITELY_UNSET", true));
192
193 let guard = crate::env_lock::lock();
194 guard.set_var("VTCODE_ENV_BOOL_TEST_VALUE", "yes");
195 assert!(parse_env_bool("VTCODE_ENV_BOOL_TEST_VALUE", false));
196 guard.set_var("VTCODE_ENV_BOOL_TEST_VALUE", "bogus");
197 assert!(!parse_env_bool("VTCODE_ENV_BOOL_TEST_VALUE", false));
198 }
199}