Skip to main content

logicaffeine_kernel/interface/lean/
emit_publish.rs

1//! Publishing an emission's outcome: all or nothing.
2//!
3//! A document is published only after it has been verified again from its bytes, and only by
4//! renaming a fully written, synced staging file over its final name — a reader sees the old
5//! file or the new one, never a prefix. A FAILED run publishes no document and leaves no earlier
6//! success under its name: the stale `<stem>.lg` is removed and the failure is recorded beside it,
7//! separately, in `<stem>.failure.txt`.
8
9use super::{verify, EmittedDocument};
10use crate::interface::lean::{Result, Verdict};
11use std::io::{self, Write};
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14
15/// The published document's extension.
16pub const DOCUMENT_EXTENSION: &str = "lg";
17/// The extension of a failed run's diagnostic record.
18pub const FAILURE_EXTENSION: &str = "failure.txt";
19
20/// What a publication left in place.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Published {
23    /// The verified document, at this path.
24    Document(PathBuf),
25    /// No document: the run failed, and its diagnostic is at this path.
26    Failure(PathBuf),
27}
28
29/// Publish `outcome` under `dir/<stem>.lg`, or its failure under `dir/<stem>.failure.txt`.
30pub fn publish(dir: &Path, stem: &str, outcome: &Result<EmittedDocument>) -> io::Result<Published> {
31    publish_through(dir, stem, outcome, &mut |sink, bytes| sink.write_all(bytes))
32}
33
34/// [`publish`], writing every file's bytes through `write` — the seam a test uses to make a write
35/// fail half way.
36pub fn publish_through(
37    dir: &Path,
38    stem: &str,
39    outcome: &Result<EmittedDocument>,
40    write: &mut dyn FnMut(&mut dyn Write, &[u8]) -> io::Result<()>,
41) -> io::Result<Published> {
42    let plain = !stem.is_empty()
43        && stem != "."
44        && stem != ".."
45        && !stem.contains(['/', '\\', '\0'])
46        && !stem.starts_with('.');
47    if !plain {
48        return Err(io::Error::new(
49            io::ErrorKind::InvalidInput,
50            format!("{stem:?} is not a plain file stem"),
51        ));
52    }
53    let document = dir.join(format!("{stem}.{DOCUMENT_EXTENSION}"));
54    let failure = dir.join(format!("{stem}.{FAILURE_EXTENSION}"));
55
56    let verified: std::result::Result<&EmittedDocument, String> = match outcome {
57        Ok(doc) => verify(doc)
58            .map(|_| doc)
59            .map_err(|fault| format!("unsupported_feature: {fault}")),
60        Err(verdict) => Err(describe(verdict)),
61    };
62    match verified {
63        Ok(doc) => match write_atomically(dir, &document, doc.source.as_bytes(), write) {
64            Ok(()) => {
65                remove_if_present(&failure)?;
66                Ok(Published::Document(document))
67            }
68            Err(error) => {
69                remove_if_present(&document)?;
70                Err(error)
71            }
72        },
73        Err(diagnostic) => {
74            remove_if_present(&document)?;
75            write_atomically(dir, &failure, diagnostic.as_bytes(), write)?;
76            Ok(Published::Failure(failure))
77        }
78    }
79}
80
81fn describe(verdict: &Verdict) -> String {
82    let (outcome, reason) = match verdict {
83        Verdict::Reject(r) => ("reject", r),
84        Verdict::Decline(r) => ("decline", r),
85    };
86    format!("{outcome} {}: {}\n", reason.code(), reason.message())
87}
88
89fn remove_if_present(path: &Path) -> io::Result<()> {
90    match std::fs::remove_file(path) {
91        Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
92        _ => Ok(()),
93    }
94}
95
96/// Write `bytes` to a staging file beside `path`, sync it, and rename it over `path`. On any
97/// failure the staging file is removed and `path` is untouched by this call.
98fn write_atomically(
99    dir: &Path,
100    path: &Path,
101    bytes: &[u8],
102    write: &mut dyn FnMut(&mut dyn Write, &[u8]) -> io::Result<()>,
103) -> io::Result<()> {
104    static SERIAL: AtomicU64 = AtomicU64::new(0);
105    let name = path
106        .file_name()
107        .and_then(|n| n.to_str())
108        .unwrap_or("document");
109    let staging = dir.join(format!(
110        ".{name}.staging-{}-{}",
111        std::process::id(),
112        SERIAL.fetch_add(1, Ordering::Relaxed)
113    ));
114    let result = (|| {
115        let mut file = std::fs::OpenOptions::new()
116            .write(true)
117            .create_new(true)
118            .open(&staging)?;
119        write(&mut file, bytes)?;
120        file.flush()?;
121        file.sync_all()?;
122        drop(file);
123        std::fs::rename(&staging, path)?;
124        if let Ok(handle) = std::fs::File::open(dir) {
125            let _ = handle.sync_all();
126        }
127        Ok(())
128    })();
129    if result.is_err() {
130        let _ = std::fs::remove_file(&staging);
131    }
132    result
133}