pub mod banner;
pub mod csv;
pub mod gha;
pub mod html;
pub mod json;
pub mod markdown;
pub mod ndjson;
pub mod parquet;
pub mod sarif;
#[cfg(feature = "spa")]
pub mod spa;
pub mod sqlite;
#[cfg(feature = "spa")]
pub mod step_summary;
pub(crate) mod template;
use std::path::{Path, PathBuf};
pub fn atomic_publish<F, E>(dest: &Path, write: F) -> std::result::Result<(), E>
where
F: FnOnce(&Path) -> std::result::Result<(), E>,
E: From<std::io::Error>,
{
let mut tmp_os = dest.as_os_str().to_owned();
tmp_os.push(format!(".tmp.{}", std::process::id()));
let tmp = PathBuf::from(tmp_os);
let _ = std::fs::remove_file(&tmp);
if let Err(e) = write(&tmp) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
if let Ok(f) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&tmp)
{
let _ = f.sync_all();
}
let published = std::fs::rename(&tmp, dest).or_else(|first| {
if dest.exists() {
std::fs::remove_file(dest).and_then(|()| std::fs::rename(&tmp, dest))
} else {
Err(first)
}
});
if let Err(e) = published {
let _ = std::fs::remove_file(&tmp);
return Err(E::from(std::io::Error::new(
e.kind(),
format!("atomically publish {}: {e}", dest.display()),
)));
}
Ok(())
}
pub(crate) fn serde_json_io_err(context: &str, e: &serde_json::Error) -> crate::CodeLoreError {
match e.io_error_kind() {
Some(kind) => crate::CodeLoreError::Io(std::io::Error::from(kind)),
None => crate::CodeLoreError::Output(format!("{context}: {e}")),
}
}
#[cfg(test)]
mod atomic_publish_tests {
use super::atomic_publish;
use std::io::Write as _;
fn temp_strays(dir: &std::path::Path) -> Vec<String> {
std::fs::read_dir(dir)
.expect("readdir")
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| name.contains(".tmp."))
.collect()
}
#[test]
fn success_replaces_previous_contents() {
let dir = tempfile::tempdir().expect("tempdir");
let dest = dir.path().join("out.bin");
std::fs::write(&dest, b"old").expect("seed");
atomic_publish::<_, std::io::Error>(&dest, |tmp| {
let mut f = std::fs::File::create(tmp)?;
f.write_all(b"new")
})
.expect("publish");
assert_eq!(std::fs::read(&dest).expect("read"), b"new");
assert!(temp_strays(dir.path()).is_empty(), "temp file orphaned");
}
#[test]
fn failed_write_leaves_previous_output_intact() {
let dir = tempfile::tempdir().expect("tempdir");
let dest = dir.path().join("out.bin");
std::fs::write(&dest, b"previous-good").expect("seed");
let err = atomic_publish::<_, std::io::Error>(&dest, |tmp| {
let mut f = std::fs::File::create(tmp)?;
f.write_all(b"half-written")?;
Err(std::io::Error::other("boom"))
})
.expect_err("closure error must propagate");
assert_eq!(err.to_string(), "boom");
assert_eq!(
std::fs::read(&dest).expect("read"),
b"previous-good",
"an interrupted write must not truncate the previous good output"
);
assert!(
temp_strays(dir.path()).is_empty(),
"temp file orphaned on failure"
);
}
}