#![forbid(unsafe_code)]
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
fmt::{self, Display, Formatter},
fs,
io::{self, BufWriter, Write},
path::{Path, PathBuf},
};
use serde::Serialize;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ArtifactPath {
requested: PathBuf,
}
impl ArtifactPath {
pub fn try_new(requested: PathBuf) -> Result<Self, ArtifactOutputError> {
if requested.as_os_str().is_empty() {
return Err(ArtifactOutputError::EmptyPath);
}
Ok(Self { requested })
}
pub fn as_path(&self) -> &Path {
&self.requested
}
pub fn create_parent(&self) -> Result<(), ArtifactOutputError> {
if let Some(parent) = self
.requested
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent).map_err(|source| ArtifactOutputError::CreateParent {
path: parent.to_owned(),
source,
})?;
}
Ok(())
}
fn json_destination_permissions(&self) -> Result<Option<fs::Permissions>, ArtifactOutputError> {
match fs::symlink_metadata(self.as_path()) {
Ok(metadata) if metadata.is_file() => Ok(Some(metadata.permissions())),
Ok(_) => Err(ArtifactOutputError::UnsupportedDestination {
path: self.requested.clone(),
}),
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(ArtifactOutputError::Inspect {
path: self.requested.clone(),
source,
}),
}
}
fn write_json(&self, value: &impl Serialize) -> Result<(), ArtifactOutputError> {
let permissions = self.json_destination_permissions()?;
self.create_parent()?;
let parent = self
.requested
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let mut builder = tempfile::Builder::new();
builder.prefix(".delaunay-json-");
#[cfg(unix)]
{
builder.permissions(permissions.unwrap_or_else(|| fs::Permissions::from_mode(0o666)));
}
#[cfg(not(unix))]
let _ = permissions;
let mut temporary =
builder
.tempfile_in(parent)
.map_err(|source| ArtifactOutputError::Open {
path: self.requested.clone(),
source,
})?;
{
let mut writer = BufWriter::new(temporary.as_file_mut());
serde_json::to_writer_pretty(&mut writer, value).map_err(|source| {
ArtifactOutputError::Serialize {
destination: ArtifactDestination::Path(self.requested.clone()),
source,
}
})?;
writer.flush().map_err(|source| self.flush_error(source))?;
}
if let Some(permissions) = self.json_destination_permissions()? {
temporary
.as_file()
.set_permissions(permissions)
.map_err(|source| ArtifactOutputError::Publish {
path: self.requested.clone(),
source,
})?;
}
temporary
.persist(&self.requested)
.map_err(|error| ArtifactOutputError::Publish {
path: self.requested.clone(),
source: error.error,
})?;
Ok(())
}
pub fn flush_error(&self, source: io::Error) -> ArtifactOutputError {
ArtifactOutputError::Flush {
path: self.requested.clone(),
source,
}
}
}
pub fn write_json_output(
value: &impl Serialize,
path: Option<&ArtifactPath>,
) -> Result<(), ArtifactOutputError> {
if let Some(path) = path {
path.write_json(value)?;
} else {
let stdout = io::stdout();
let mut handle = stdout.lock();
serde_json::to_writer_pretty(&mut handle, value).map_err(|source| {
ArtifactOutputError::Serialize {
destination: ArtifactDestination::Stdout,
source,
}
})?;
writeln!(handle).map_err(|source| ArtifactOutputError::WriteStdout { source })?;
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ArtifactDestination {
Stdout,
Path(PathBuf),
}
impl Display for ArtifactDestination {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Stdout => formatter.write_str("stdout"),
Self::Path(path) => write!(formatter, "{}", path.display()),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArtifactOutputError {
#[error("artifact path must not be empty")]
EmptyPath,
#[error("failed to inspect JSON artifact {path:?}: {source}")]
Inspect {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(
"JSON artifact {path:?} must be a regular file or a new path; symlinks are not allowed"
)]
UnsupportedDestination {
path: PathBuf,
},
#[error("failed to create artifact directory {path:?}: {source}")]
CreateParent {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to open artifact {path:?} for writing: {source}")]
Open {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to serialize JSON to {destination}: {source}")]
Serialize {
destination: ArtifactDestination,
#[source]
source: serde_json::Error,
},
#[error("failed to flush artifact {path:?}: {source}")]
Flush {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to publish JSON artifact {path:?}: {source}")]
Publish {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to write JSON to stdout: {source}")]
WriteStdout {
#[source]
source: io::Error,
},
}