logicaffeine_kernel/interface/lean/
emit_publish.rs1use 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
15pub const DOCUMENT_EXTENSION: &str = "lg";
17pub const FAILURE_EXTENSION: &str = "failure.txt";
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Published {
23 Document(PathBuf),
25 Failure(PathBuf),
27}
28
29pub 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
34pub 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
96fn 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}