use std::time::Instant;
use super::diagnostics::{self, Phases};
use super::screen::{self, Outgoing};
use super::{scope, visibility, BoundaryVerdict, Evidence, Surface, TermScope};
use crate::error::{Error, Result};
use crate::host::Hosting;
pub fn guard_publication(
hosting: &dyn Hosting,
destination: &str,
outgoing: &Outgoing<'_>,
scope: &TermScope,
) -> Result<()> {
if !visibility::screens_writes(hosting, destination)? {
return Ok(());
}
let screened = screen::screen(outgoing, scope);
diagnostics::record("publication", &screened.verdict, &screened.phases);
settle(screened.verdict, screened.evidence)
}
pub fn guard_publication_unasked(
destination: &str,
outgoing: &Outgoing<'_>,
scope: &TermScope,
) -> Result<()> {
if !visibility::screens_writes_unasked(destination)? {
return Ok(());
}
let screened = screen::screen(outgoing, scope);
diagnostics::record("publication", &screened.verdict, &screened.phases);
settle(screened.verdict, screened.evidence)
}
pub fn guard_fields(
hosting: &dyn Hosting,
destination: &str,
fields: &[(Surface, &str)],
scope: &TermScope,
) -> Result<()> {
if !visibility::screens_writes(hosting, destination)? {
return Ok(());
}
let started = Instant::now();
let mut evidence = Vec::new();
let mut phases = Phases::default();
let verdict = match scope::derive(scope) {
Err(failed) => failed.verdict(&mut evidence),
Ok(derived) => {
phases.derivation = started.elapsed();
phases.terms = derived.rules.len();
phases.identities = derived.sources;
match derived.matcher() {
Err(failed) => failed.verdict(&mut evidence),
Ok(matcher) => {
let mut verdict = BoundaryVerdict::Pass;
for (surface, text) in fields {
for rule in matcher.find(text) {
if verdict == BoundaryVerdict::Pass {
verdict = BoundaryVerdict::Refuse { surface: *surface };
}
evidence.push(derived.evidence(
*surface,
screen::surface_name(*surface),
rule,
));
}
}
verdict
}
}
}
};
diagnostics::record("fields", &verdict, &phases.ended(started));
settle(verdict, evidence)
}
pub fn is_boundary_reason(reason: &str) -> bool {
reason.contains(super::REFUSED_SENTENCE) || reason.contains(super::UNAVAILABLE_SENTENCE)
}
pub fn settle(verdict: BoundaryVerdict, evidence: Vec<Evidence>) -> Result<()> {
let Some(reason) = verdict.reason() else {
return Ok(());
};
let kept = match keep(&evidence) {
Ok(path) => format!(
"; what it found is recorded privately at {}, on this host only",
path.display()
),
Err(error) => format!("; what it found could not be recorded privately: {error}"),
};
let reason = format!("{reason}. Nothing was written to the remote{kept}");
Err(match verdict {
BoundaryVerdict::Refuse { .. } => Error::GateFailed { reason },
_ => Error::Invalid { reason },
})
}
pub fn keep(evidence: &[Evidence]) -> Result<std::path::PathBuf> {
let directory = crate::home::root()?.join("boundary").join("evidence");
crate::home::ensure_dir(&directory)?;
private_directory(&directory)?;
let path = directory.join(format!("{}.json", crate::ids::unique()));
let document = serde_json::to_string_pretty(evidence)
.map_err(|error| crate::error::invalid(error.to_string()))?;
write_private(&path, &document)?;
Ok(path)
}
#[cfg(unix)]
fn private_directory(directory: &std::path::Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700))
.map_err(crate::error::at("restrict", directory))
}
#[cfg(not(unix))]
fn private_directory(_directory: &std::path::Path) -> Result<()> {
Ok(())
}
fn write_private(path: &std::path::Path, contents: &str) -> Result<()> {
use std::io::Write;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(path)
.map_err(crate::error::at("write", path))?;
file.write_all(contents.as_bytes())
.map_err(crate::error::at("write", path))
}