use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use serde::Serialize;
use crate::freshness::record::sha256_hex;
use super::facts::{self, Fact, FAMILY_ORDER};
use super::header;
use super::snapshot::Snapshot;
pub(crate) type Counts = [usize; FAMILY_ORDER.len()];
#[derive(Serialize)]
struct CountedFamilies {
definition: usize,
edge: usize,
#[serde(rename = "publish-site")]
publish_site: usize,
#[serde(rename = "handler-registration")]
handler_registration: usize,
#[serde(rename = "di-registration")]
di_registration: usize,
#[serde(rename = "unresolved-import")]
unresolved_import: usize,
#[serde(rename = "test-file")]
test_file: usize,
#[serde(rename = "transport-address")]
transport_address: usize,
}
impl CountedFamilies {
fn of(counts: &Counts) -> CountedFamilies {
CountedFamilies {
definition: counts[0],
edge: counts[1],
publish_site: counts[2],
handler_registration: counts[3],
di_registration: counts[4],
unresolved_import: counts[5],
test_file: counts[6],
transport_address: counts[7],
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Trailer {
trailer: &'static str,
counts: CountedFamilies,
facts: usize,
payload_digest: String,
}
pub(crate) struct Stream {
header: String,
body: Vec<u8>,
trailer: String,
}
impl Stream {
pub(crate) fn bytes(&self) -> Vec<u8> {
let mut bytes =
Vec::with_capacity(self.header.len() + self.body.len() + self.trailer.len() + 2);
bytes.extend_from_slice(self.header.as_bytes());
bytes.push(b'\n');
bytes.extend_from_slice(&self.body);
bytes.extend_from_slice(self.trailer.as_bytes());
bytes.push(b'\n');
bytes
}
}
fn canonical(facts: &mut [Fact]) {
facts.sort_by(|left, right| {
left.rank
.cmp(&right.rank)
.then_with(|| left.file.as_bytes().cmp(right.file.as_bytes()))
.then_with(|| left.line.cmp(&right.line))
.then_with(|| left.json.as_bytes().cmp(right.json.as_bytes()))
});
}
fn count(facts: &[Fact]) -> Counts {
let mut counts = [0usize; FAMILY_ORDER.len()];
for fact in facts {
counts[fact.rank] += 1;
}
counts
}
pub(crate) fn render(snapshot: &Snapshot) -> Stream {
let mut facts = facts::collect(snapshot);
canonical(&mut facts);
let counts = count(&facts);
let mut body = Vec::new();
for fact in &facts {
body.extend_from_slice(fact.json.as_bytes());
body.push(b'\n');
}
let trailer = serde_json::to_string(&Trailer {
trailer: header::FORMAT,
counts: CountedFamilies::of(&counts),
facts: facts.len(),
payload_digest: format!("sha256:{}", sha256_hex(&body)),
})
.expect("a trailer always serializes");
Stream {
header: header::line(snapshot),
body,
trailer,
}
}
static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
const TMP_ATTEMPTS: u64 = 64;
pub(crate) fn write_out(path: &Path, bytes: &[u8]) -> io::Result<()> {
let dir = path.parent().filter(|p| !p.as_os_str().is_empty());
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("export.ndjson")
.to_string();
write_out_via(path, bytes, tmp_candidates(dir, name))
}
fn tmp_candidates(dir: Option<&Path>, name: String) -> impl Iterator<Item = PathBuf> {
let dir = dir.unwrap_or_else(|| Path::new(".")).to_path_buf();
let pid = std::process::id();
(0..TMP_ATTEMPTS).map(move |_| {
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
dir.join(format!(".{name}.tmp.{pid}.{counter}"))
})
}
pub(super) fn write_out_via(
path: &Path,
bytes: &[u8],
names: impl Iterator<Item = PathBuf>,
) -> io::Result<()> {
for tmp in names {
let mut file = match create_exclusive(&tmp) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
};
let write = file.write_all(bytes).and_then(|()| file.flush());
drop(file);
if let Err(error) = write {
let _ = fs::remove_file(&tmp);
return Err(error);
}
if let Err(error) = fs::rename(&tmp, path) {
let _ = fs::remove_file(&tmp);
return Err(error);
}
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"every temporary name beside the output is taken",
))
}
fn create_exclusive(path: &Path) -> io::Result<fs::File> {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
}