use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub(super) fn list_toml_files(dir: &Path) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
let read =
std::fs::read_dir(dir).with_context(|| format!("reading directory {}", dir.display()))?;
for entry in read {
let entry = entry.with_context(|| format!("reading entry under {}", dir.display()))?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("toml") {
out.push(path);
}
}
out.sort();
Ok(out)
}
pub(super) fn atomic_write(path: &Path, content: &str) -> Result<()> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let tmp = tempfile::NamedTempFile::new_in(parent)
.with_context(|| format!("creating tempfile in {}", parent.display()))?;
{
use std::io::Write;
let mut handle = tmp.as_file();
handle.write_all(content.as_bytes())?;
handle.flush()?;
handle.sync_all()?;
}
tmp.persist(path).map_err(|e| e.error)?;
Ok(())
}
pub fn fix_single_brace_in_verify_blocks(toml_text: &str) -> (String, usize) {
let mut out = String::with_capacity(toml_text.len());
let mut in_verify = false;
let mut total = 0usize;
for line in toml_text.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix('[') {
let header = rest.trim_end_matches(['\r', ' ', '\t']);
in_verify = header.starts_with("detector.verify")
|| header.starts_with("[detector.verify")
|| header == "detector.verify]"
|| header == "[detector.verify]]";
if !in_verify {
let stripped = header.trim_matches(['[', ']'].as_ref());
in_verify = stripped.starts_with("detector.verify");
}
}
if in_verify {
let (rewritten, count) = rewrite_braces_in_string_literals(line);
total += count;
out.push_str(&rewritten);
} else {
out.push_str(line);
}
out.push('\n');
}
if !toml_text.ends_with('\n') && out.ends_with('\n') {
out.pop();
}
(out, total)
}
#[doc(hidden)]
pub fn rewrite_braces_in_string_literals(line: &str) -> (String, usize) {
let mut out = String::with_capacity(line.len());
let mut count = 0usize;
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'"' || b == b'\'' {
let quote = b;
out.push(quote as char);
let mut j = i + 1;
let mut literal = String::new();
while j < bytes.len() {
let c = bytes[j];
if quote == b'"' && c == b'\\' && j + 1 < bytes.len() {
literal.push(c as char);
literal.push(bytes[j + 1] as char);
j += 2;
continue;
}
if c == quote {
break;
}
literal.push(c as char);
j += 1;
}
let (rewritten_literal, c) = rewrite_braces(&literal);
count += c;
out.push_str(&rewritten_literal);
if j < bytes.len() {
out.push(quote as char);
i = j + 1;
} else {
i = j;
}
} else {
out.push(b as char);
i += 1;
}
}
(out, count)
}
#[doc(hidden)]
pub fn rewrite_braces(s: &str) -> (String, usize) {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'{' {
if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
out.push('{');
out.push('{');
i += 2;
continue;
}
let start = i + 1;
if start < bytes.len() && (bytes[start].is_ascii_alphabetic() || bytes[start] == b'_') {
let mut end = start + 1;
while end < bytes.len()
&& (bytes[end].is_ascii_alphanumeric()
|| bytes[end] == b'_'
|| bytes[end] == b'.')
{
end += 1;
}
if end < bytes.len() && bytes[end] == b'}' {
out.push_str("{{");
out.push_str(&s[start..end]);
out.push_str("}}");
count += 1;
i = end + 1;
continue;
}
}
out.push('{');
i += 1;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
(out, count)
}