use std::{
fs,
io::Write,
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use imagegen_bridge_core::{ArtifactCollisionPolicy, BridgeError, ErrorCode, OutputFormat};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use uuid::Uuid;
use crate::{ImageLimits, ImageMetadata, inspect_image};
const OWNERSHIP_DIRECTORY: &str = ".imagegen-bridge-ownership";
const MAX_MARKER_BYTES: u64 = 4 * 1024;
const MAX_SIDECAR_BYTES: u64 = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredArtifact {
pub id: String,
pub name: String,
pub path: PathBuf,
pub metadata: ImageMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredSidecar {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredArtifactContent {
pub id: String,
pub name: String,
pub bytes: Vec<u8>,
pub metadata: ImageMetadata,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ArtifactPublication<'a> {
pub directory: Option<&'a str>,
pub filename: Option<&'a str>,
pub collision: ArtifactCollisionPolicy,
}
#[derive(Debug, Clone)]
pub struct ArtifactStore {
root: PathBuf,
ownership_root: PathBuf,
limits: ImageLimits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetentionPolicy {
pub max_age: Duration,
pub max_artifacts: Option<usize>,
pub max_scan_entries: usize,
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
max_age: Duration::from_secs(7 * 24 * 60 * 60),
max_artifacts: None,
max_scan_entries: 100_000,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CleanupReport {
pub scanned: usize,
pub deleted: usize,
pub skipped: usize,
pub scan_limit_reached: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ArtifactRepairReport {
pub scanned: usize,
pub healthy: usize,
pub orphaned_records: usize,
pub missing_sidecars: usize,
pub repaired: usize,
pub skipped: usize,
pub scan_limit_reached: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactRepairMode {
Audit,
Apply,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct OwnershipRecord {
version: u8,
id: String,
name: String,
created_at: u64,
sha256: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
sidecar_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sidecar_sha256: Option<String>,
}
struct OwnedCandidate {
marker: PathBuf,
artifact: PathBuf,
sidecar: Option<PathBuf>,
record: OwnershipRecord,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SidecarState {
None,
Missing,
Valid,
Invalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OwnedPathState {
Missing,
Regular(u64),
Invalid,
}
impl ArtifactStore {
pub fn new(root: impl Into<PathBuf>, limits: ImageLimits) -> Result<Self, BridgeError> {
let root = root.into();
fs::create_dir_all(&root)
.map_err(|error| artifact_error(format!("could not create artifact root: {error}")))?;
let root = fs::canonicalize(root)
.map_err(|error| artifact_error(format!("could not open artifact root: {error}")))?;
if !root.is_dir() {
return Err(artifact_error("artifact root is not a directory"));
}
let ownership_path = root.join(OWNERSHIP_DIRECTORY);
fs::create_dir_all(&ownership_path).map_err(|error| {
artifact_error(format!("could not create artifact ownership root: {error}"))
})?;
if fs::symlink_metadata(&ownership_path)
.map_err(|error| {
artifact_error(format!(
"could not inspect artifact ownership root: {error}"
))
})?
.file_type()
.is_symlink()
{
return Err(artifact_error(
"artifact ownership root must not be a symbolic link",
));
}
let ownership_root = fs::canonicalize(ownership_path).map_err(|error| {
artifact_error(format!("could not open artifact ownership root: {error}"))
})?;
if !ownership_root.is_dir()
|| ownership_root.parent() != Some(root.as_path())
|| !ownership_root.starts_with(&root)
|| ownership_root.file_name().and_then(|value| value.to_str())
!= Some(OWNERSHIP_DIRECTORY)
{
return Err(artifact_error(
"artifact ownership root must be a real child directory",
));
}
Ok(Self {
root,
ownership_root,
limits,
})
}
pub fn publish(
&self,
bytes: &[u8],
filename_prefix: Option<&str>,
expected_format: Option<OutputFormat>,
) -> Result<StoredArtifact, BridgeError> {
self.publish_with_options(
bytes,
filename_prefix,
expected_format,
ArtifactPublication::default(),
)
}
pub fn publish_with_options(
&self,
bytes: &[u8],
filename_prefix: Option<&str>,
expected_format: Option<OutputFormat>,
publication: ArtifactPublication<'_>,
) -> Result<StoredArtifact, BridgeError> {
let metadata = inspect_image(bytes, self.limits).map_err(|error| BridgeError {
code: ErrorCode::Artifact,
..error
})?;
if expected_format.is_some_and(|expected| expected != metadata.format) {
return Err(artifact_error(
"generated image format does not match the effective request",
));
}
let id = Uuid::now_v7().to_string();
let (directory, portable_directory) = self.publication_directory(publication.directory)?;
if let Some(filename) = publication.filename
&& !safe_filename(filename, metadata.format)
{
return Err(artifact_error(
"output filename must be a safe component with a matching image extension",
));
}
let requested_name = publication.filename.map_or_else(
|| {
let prefix = sanitize_prefix(filename_prefix.unwrap_or("image"));
format!("{prefix}-{id}.{}", extension(metadata.format))
},
|filename| filename_with_extension(filename, metadata.format),
);
let (destination, filename) = publish_noclobber(
&directory,
&requested_name,
bytes,
publication.collision,
publication.filename.is_some(),
)?;
if let Err(error) = sync_directory(&directory) {
let _ = fs::remove_file(&destination);
return Err(error);
}
let name = portable_directory.map_or_else(
|| filename.clone(),
|directory| format!("{directory}/{filename}"),
);
let record = OwnershipRecord {
version: 1,
id: id.clone(),
name: name.clone(),
created_at: unix_timestamp(SystemTime::now())?,
sha256: metadata.sha256.clone(),
sidecar_name: None,
sidecar_sha256: None,
};
if let Err(error) = self.publish_ownership(&record) {
let _ = fs::remove_file(&destination);
let _ = sync_directory(&self.root);
return Err(error);
}
Ok(StoredArtifact {
id,
name,
path: destination,
metadata,
})
}
pub fn attach_metadata(
&self,
artifact_id: &str,
artifact_name: &str,
encoded: &[u8],
) -> Result<StoredSidecar, BridgeError> {
if Uuid::parse_str(artifact_id).is_err() || !safe_relative(artifact_name) {
return Err(artifact_error("artifact identity is invalid"));
}
if encoded.is_empty()
|| u64::try_from(encoded.len()).unwrap_or(u64::MAX) > MAX_SIDECAR_BYTES
|| !matches!(
serde_json::from_slice::<serde_json::Value>(encoded),
Ok(serde_json::Value::Object(_))
)
{
return Err(artifact_error(
"artifact metadata must be a bounded JSON object",
));
}
let marker = self.ownership_root.join(format!("{artifact_id}.json"));
let mut candidate = self
.read_candidate(&marker)
.map_err(|()| artifact_error("artifact ownership record is invalid"))?;
if candidate.record.name != artifact_name
|| candidate.record.sidecar_name.is_some()
|| candidate.record.sidecar_sha256.is_some()
|| self.verify_candidate(&candidate).is_err()
{
return Err(artifact_error(
"artifact metadata cannot be attached to this artifact",
));
}
let directory = candidate
.artifact
.parent()
.ok_or_else(|| artifact_error("artifact directory is invalid"))?;
let filename = format!("metadata-{artifact_id}.json");
let destination = directory.join(&filename);
let mut temporary = NamedTempFile::new_in(directory)
.map_err(|_| artifact_error("could not create metadata sidecar"))?;
temporary
.write_all(encoded)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|_| artifact_error("could not write metadata sidecar"))?;
temporary
.persist_noclobber(&destination)
.map_err(|_| artifact_error("could not publish metadata sidecar without overwrite"))?;
sync_directory(directory)?;
let portable_name = Path::new(artifact_name)
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.and_then(Path::to_str)
.map_or_else(|| filename.clone(), |parent| format!("{parent}/{filename}"));
candidate.record.sidecar_name = Some(portable_name.clone());
candidate.record.sidecar_sha256 =
Some(base16ct::lower::encode_string(&Sha256::digest(encoded)));
if let Err(error) = self.replace_ownership(&candidate.record) {
let _ = fs::remove_file(&destination);
let _ = sync_directory(directory);
return Err(error);
}
Ok(StoredSidecar {
name: portable_name,
})
}
pub fn read(&self, artifact_id: &str) -> Result<StoredArtifactContent, BridgeError> {
if Uuid::parse_str(artifact_id).is_err() {
return Err(artifact_error("artifact identity is invalid"));
}
let marker = self.ownership_root.join(format!("{artifact_id}.json"));
let candidate = self
.read_candidate(&marker)
.map_err(|()| artifact_error("artifact was not found or is invalid"))?;
self.verify_candidate(&candidate)
.map_err(|()| artifact_error("artifact was not found or is invalid"))?;
let bytes = fs::read(&candidate.artifact)
.map_err(|_| artifact_error("artifact could not be read"))?;
let metadata = inspect_image(&bytes, self.limits)
.map_err(|_| artifact_error("artifact failed delivery verification"))?;
Ok(StoredArtifactContent {
id: candidate.record.id,
name: candidate.record.name,
bytes,
metadata,
})
}
fn publication_directory(
&self,
relative: Option<&str>,
) -> Result<(PathBuf, Option<String>), BridgeError> {
let Some(relative) = relative else {
return Ok((self.root.clone(), None));
};
if !safe_relative(relative) {
return Err(artifact_error(
"output directory is not a safe relative path",
));
}
let mut directory = self.root.clone();
for component in relative.split('/') {
directory.push(component);
match fs::symlink_metadata(&directory) {
Ok(metadata) if metadata.file_type().is_dir() => {}
Ok(_) => {
return Err(artifact_error(
"output directory component must not be a file or symlink",
));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(&directory).map_err(|_| {
artifact_error("could not create output directory component")
})?;
}
Err(_) => return Err(artifact_error("could not inspect output directory")),
}
}
let canonical = fs::canonicalize(&directory)
.map_err(|_| artifact_error("could not open output directory"))?;
if !canonical.starts_with(&self.root) || canonical == self.ownership_root {
return Err(artifact_error("output directory escapes the artifact root"));
}
Ok((canonical, Some(relative.to_owned())))
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub fn cleanup(
&self,
policy: RetentionPolicy,
now: SystemTime,
) -> Result<CleanupReport, BridgeError> {
if policy.max_scan_entries == 0 {
return Err(artifact_error(
"retention scan limit must be greater than zero",
));
}
self.verify_ownership_root()?;
let now = unix_timestamp(now)?;
let cutoff = now.saturating_sub(policy.max_age.as_secs());
let mut report = CleanupReport::default();
let mut candidates = Vec::new();
for entry in fs::read_dir(&self.ownership_root)
.map_err(|error| artifact_error(format!("could not scan ownership records: {error}")))?
{
if report.scanned >= policy.max_scan_entries {
report.scan_limit_reached = true;
break;
}
report.scanned += 1;
let Ok(entry) = entry else {
report.skipped += 1;
continue;
};
match self.read_candidate(&entry.path()) {
Ok(candidate) if self.verify_candidate(&candidate).is_ok() => {
candidates.push(candidate);
}
Err(()) | Ok(_) => report.skipped += 1,
}
}
candidates.sort_by(|left, right| {
right
.record
.created_at
.cmp(&left.record.created_at)
.then_with(|| right.record.id.cmp(&left.record.id))
});
for (index, candidate) in candidates.into_iter().enumerate() {
let exceeds_count = policy.max_artifacts.is_some_and(|maximum| index >= maximum);
let expired = candidate.record.created_at <= cutoff;
if !expired && !exceeds_count {
continue;
}
if self.remove_verified(&candidate).is_ok() {
report.deleted += 1;
} else {
report.skipped += 1;
}
}
Ok(report)
}
pub fn repair_orphans(
&self,
max_scan_entries: usize,
mode: ArtifactRepairMode,
) -> Result<ArtifactRepairReport, BridgeError> {
if max_scan_entries == 0 {
return Err(artifact_error(
"artifact repair scan limit must be greater than zero",
));
}
self.verify_ownership_root()?;
let mut report = ArtifactRepairReport::default();
for entry in fs::read_dir(&self.ownership_root)
.map_err(|error| artifact_error(format!("could not scan ownership records: {error}")))?
{
if report.scanned >= max_scan_entries {
report.scan_limit_reached = true;
break;
}
report.scanned += 1;
let Ok(entry) = entry else {
report.skipped += 1;
continue;
};
let Ok(candidate) = self.read_candidate(&entry.path()) else {
report.skipped += 1;
continue;
};
if self.verify_artifact(&candidate).is_ok() {
match self.sidecar_state(&candidate) {
SidecarState::None | SidecarState::Valid => report.healthy += 1,
SidecarState::Missing => {
report.missing_sidecars += 1;
if mode == ArtifactRepairMode::Apply {
if self.clear_missing_sidecar(&candidate).is_ok() {
report.repaired += 1;
} else {
report.skipped += 1;
}
}
}
SidecarState::Invalid => report.skipped += 1,
}
} else if self.owned_path_state(&candidate.artifact) == OwnedPathState::Missing {
report.orphaned_records += 1;
match self.sidecar_state(&candidate) {
SidecarState::None | SidecarState::Missing | SidecarState::Valid => {
if mode == ArtifactRepairMode::Apply {
if self.remove_orphaned_record(&candidate).is_ok() {
report.repaired += 1;
} else {
report.skipped += 1;
}
}
}
SidecarState::Invalid => report.skipped += 1,
}
} else {
report.skipped += 1;
}
}
Ok(report)
}
fn publish_ownership(&self, record: &OwnershipRecord) -> Result<(), BridgeError> {
self.verify_ownership_root()?;
let encoded = serde_json::to_vec(record)
.map_err(|_| artifact_error("could not encode artifact ownership record"))?;
if u64::try_from(encoded.len()).unwrap_or(u64::MAX) > MAX_MARKER_BYTES {
return Err(artifact_error("artifact ownership record is too large"));
}
let destination = self.ownership_root.join(format!("{}.json", record.id));
let mut temporary = NamedTempFile::new_in(&self.ownership_root).map_err(|error| {
artifact_error(format!("could not create ownership record: {error}"))
})?;
temporary
.write_all(&encoded)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|error| {
artifact_error(format!("could not write ownership record: {error}"))
})?;
temporary.persist_noclobber(destination).map_err(|error| {
artifact_error(format!("could not publish ownership record: {error}"))
})?;
sync_directory(&self.ownership_root)
}
fn replace_ownership(&self, record: &OwnershipRecord) -> Result<(), BridgeError> {
self.verify_ownership_root()?;
let encoded = serde_json::to_vec(record)
.map_err(|_| artifact_error("could not encode artifact ownership record"))?;
if u64::try_from(encoded.len()).unwrap_or(u64::MAX) > MAX_MARKER_BYTES {
return Err(artifact_error("artifact ownership record is too large"));
}
let destination = self.ownership_root.join(format!("{}.json", record.id));
let mut temporary = NamedTempFile::new_in(&self.ownership_root)
.map_err(|_| artifact_error("could not create ownership record"))?;
temporary
.write_all(&encoded)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|_| artifact_error("could not write ownership record"))?;
temporary
.persist(&destination)
.map_err(|_| artifact_error("could not replace ownership record"))?;
sync_directory(&self.ownership_root)
}
fn read_candidate(&self, marker: &Path) -> Result<OwnedCandidate, ()> {
if marker.parent() != Some(self.ownership_root.as_path())
|| self.verify_ownership_root().is_err()
{
return Err(());
}
let marker_metadata = fs::symlink_metadata(marker).map_err(|_| ())?;
if !marker_metadata.file_type().is_file() || marker_metadata.len() > MAX_MARKER_BYTES {
return Err(());
}
let marker_name = marker
.file_name()
.and_then(|value| value.to_str())
.ok_or(())?;
let encoded = fs::read(marker).map_err(|_| ())?;
let record: OwnershipRecord = serde_json::from_slice(&encoded).map_err(|_| ())?;
let sidecar_valid = match (&record.sidecar_name, &record.sidecar_sha256) {
(None, None) => true,
(Some(name), Some(sha256)) => {
safe_relative(name)
&& sha256.len() == 64
&& sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
}
_ => false,
};
if record.version != 1
|| marker_name != format!("{}.json", record.id)
|| Uuid::parse_str(&record.id).is_err()
|| !safe_relative(&record.name)
|| record.sha256.len() != 64
|| !record.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
|| !sidecar_valid
{
return Err(());
}
Ok(OwnedCandidate {
marker: marker.to_owned(),
artifact: self.root.join(&record.name),
sidecar: record
.sidecar_name
.as_deref()
.map(|name| self.root.join(name)),
record,
})
}
fn remove_verified(&self, candidate: &OwnedCandidate) -> Result<(), ()> {
self.verify_candidate(candidate)?;
if let Some(sidecar) = &candidate.sidecar {
fs::remove_file(sidecar).map_err(|_| ())?;
}
fs::remove_file(&candidate.artifact).map_err(|_| ())?;
fs::remove_file(&candidate.marker).map_err(|_| ())?;
sync_directory(&self.root).map_err(|_| ())?;
sync_directory(&self.ownership_root).map_err(|_| ())
}
fn clear_missing_sidecar(&self, expected: &OwnedCandidate) -> Result<(), ()> {
let mut candidate = self.read_candidate(&expected.marker)?;
if candidate.record != expected.record
|| self.verify_artifact(&candidate).is_err()
|| self.sidecar_state(&candidate) != SidecarState::Missing
{
return Err(());
}
candidate.record.sidecar_name = None;
candidate.record.sidecar_sha256 = None;
self.replace_ownership(&candidate.record).map_err(|_| ())
}
fn remove_orphaned_record(&self, expected: &OwnedCandidate) -> Result<(), ()> {
let candidate = self.read_candidate(&expected.marker)?;
if candidate.record != expected.record
|| self.owned_path_state(&candidate.artifact) != OwnedPathState::Missing
{
return Err(());
}
match self.sidecar_state(&candidate) {
SidecarState::Valid => {
let sidecar = candidate.sidecar.as_ref().ok_or(())?;
fs::remove_file(sidecar).map_err(|_| ())?;
sync_directory(sidecar.parent().ok_or(())?).map_err(|_| ())?;
}
SidecarState::None | SidecarState::Missing => {}
SidecarState::Invalid => return Err(()),
}
fs::remove_file(&candidate.marker).map_err(|_| ())?;
sync_directory(&self.ownership_root).map_err(|_| ())
}
fn verify_candidate(&self, candidate: &OwnedCandidate) -> Result<(), ()> {
self.verify_artifact(candidate)?;
if !matches!(
self.sidecar_state(candidate),
SidecarState::None | SidecarState::Valid
) {
return Err(());
}
Ok(())
}
fn verify_artifact(&self, candidate: &OwnedCandidate) -> Result<(), ()> {
let OwnedPathState::Regular(length) = self.owned_path_state(&candidate.artifact) else {
return Err(());
};
if length > self.limits.max_encoded_bytes {
return Err(());
}
let bytes = fs::read(&candidate.artifact).map_err(|_| ())?;
let digest = base16ct::lower::encode_string(&Sha256::digest(&bytes));
if digest != candidate.record.sha256 || inspect_image(&bytes, self.limits).is_err() {
return Err(());
}
Ok(())
}
fn sidecar_state(&self, candidate: &OwnedCandidate) -> SidecarState {
let (Some(sidecar), Some(expected)) =
(&candidate.sidecar, &candidate.record.sidecar_sha256)
else {
return SidecarState::None;
};
match self.owned_path_state(sidecar) {
OwnedPathState::Missing => SidecarState::Missing,
OwnedPathState::Invalid => SidecarState::Invalid,
OwnedPathState::Regular(length) => {
if length > MAX_SIDECAR_BYTES {
return SidecarState::Invalid;
}
let Ok(encoded) = fs::read(sidecar) else {
return SidecarState::Invalid;
};
if base16ct::lower::encode_string(&Sha256::digest(&encoded)) != *expected
|| !matches!(
serde_json::from_slice::<serde_json::Value>(&encoded),
Ok(serde_json::Value::Object(_))
)
{
SidecarState::Invalid
} else {
SidecarState::Valid
}
}
}
}
fn owned_path_state(&self, path: &Path) -> OwnedPathState {
owned_path_state_beneath(&self.root, path)
}
fn verify_ownership_root(&self) -> Result<(), BridgeError> {
let metadata = fs::symlink_metadata(&self.ownership_root)
.map_err(|_| artifact_error("artifact ownership root is unavailable"))?;
if !metadata.file_type().is_dir()
|| self.ownership_root.parent() != Some(self.root.as_path())
{
return Err(artifact_error("artifact ownership root is invalid"));
}
Ok(())
}
}
fn owned_path_state_beneath(root: &Path, path: &Path) -> OwnedPathState {
let Ok(relative) = path.strip_prefix(root) else {
return OwnedPathState::Invalid;
};
let mut components = relative.components().peekable();
if components.peek().is_none() {
return OwnedPathState::Invalid;
}
let mut current = root.to_owned();
while let Some(component) = components.next() {
let std::path::Component::Normal(component) = component else {
return OwnedPathState::Invalid;
};
current.push(component);
let metadata = match fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return OwnedPathState::Missing;
}
Err(_) => return OwnedPathState::Invalid,
};
if metadata.file_type().is_symlink() {
return OwnedPathState::Invalid;
}
if components.peek().is_some() {
if !metadata.file_type().is_dir() {
return OwnedPathState::Invalid;
}
} else if metadata.file_type().is_file() {
return OwnedPathState::Regular(metadata.len());
} else {
return OwnedPathState::Invalid;
}
}
OwnedPathState::Invalid
}
fn safe_relative(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 512
&& value.split('/').all(|component| {
!component.is_empty()
&& component != "."
&& component != ".."
&& component.len() <= 160
&& !component.starts_with('.')
&& component
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
})
}
fn safe_filename(value: &str, format: OutputFormat) -> bool {
if value.is_empty()
|| value.len() > 160
|| value.starts_with('.')
|| value.contains(['/', '\\'])
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
{
return false;
}
let Some((_, extension)) = value.rsplit_once('.') else {
return true;
};
match format {
OutputFormat::Png => extension.eq_ignore_ascii_case("png"),
OutputFormat::Jpeg => {
extension.eq_ignore_ascii_case("jpg") || extension.eq_ignore_ascii_case("jpeg")
}
OutputFormat::Webp => extension.eq_ignore_ascii_case("webp"),
}
}
fn filename_with_extension(filename: &str, format: OutputFormat) -> String {
if Path::new(filename).extension().is_some() {
filename.to_owned()
} else {
format!("{filename}.{}", extension(format))
}
}
fn publish_noclobber(
directory: &Path,
requested_name: &str,
bytes: &[u8],
collision: ArtifactCollisionPolicy,
explicit_filename: bool,
) -> Result<(PathBuf, String), BridgeError> {
let attempts = if explicit_filename && collision == ArtifactCollisionPolicy::Suffix {
10_000
} else {
1
};
for attempt in 0..attempts {
let name = if attempt == 0 {
requested_name.to_owned()
} else {
suffixed_filename(requested_name, attempt + 1)
};
let destination = directory.join(&name);
let mut temporary = NamedTempFile::new_in(directory)
.map_err(|_| artifact_error("could not create temporary artifact"))?;
temporary
.write_all(bytes)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|_| artifact_error("could not write artifact"))?;
match temporary.persist_noclobber(&destination) {
Ok(_) => return Ok((destination, name)),
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => {
return Err(artifact_error(
"could not publish artifact without overwrite",
));
}
}
}
Err(artifact_error(
"artifact collision suffix limit was reached",
))
}
fn suffixed_filename(filename: &str, suffix: usize) -> String {
let path = Path::new(filename);
let stem = path
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or("image");
path.extension()
.and_then(|value| value.to_str())
.map_or_else(
|| format!("{stem}-{suffix}"),
|extension| format!("{stem}-{suffix}.{extension}"),
)
}
fn unix_timestamp(time: SystemTime) -> Result<u64, BridgeError> {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.map_err(|_| artifact_error("system time is before the Unix epoch"))
}
fn sanitize_prefix(value: &str) -> String {
let sanitized: String = value
.chars()
.filter_map(|character| {
if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
Some(character.to_ascii_lowercase())
} else if character.is_whitespace() {
Some('-')
} else {
None
}
})
.take(64)
.collect();
let sanitized = sanitized.trim_matches('-');
if sanitized.is_empty() {
"image".to_owned()
} else {
sanitized.to_owned()
}
}
const fn extension(format: OutputFormat) -> &'static str {
match format {
OutputFormat::Png => "png",
OutputFormat::Jpeg => "jpg",
OutputFormat::Webp => "webp",
}
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> Result<(), BridgeError> {
fs::File::open(path)
.and_then(|directory| directory.sync_all())
.map_err(|error| artifact_error(format!("could not sync artifact directory: {error}")))
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> Result<(), BridgeError> {
Ok(())
}
fn artifact_error(message: impl Into<String>) -> BridgeError {
BridgeError::new(ErrorCode::Artifact, message)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::fs;
use super::*;
use crate::inspect::test_png;
#[test]
fn publishes_verified_artifacts_without_name_reuse() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let bytes = test_png(2, 2);
let first = store
.publish(&bytes, Some("My portrait"), Some(OutputFormat::Png))
.unwrap();
let second = store
.publish(&bytes, Some("My portrait"), Some(OutputFormat::Png))
.unwrap();
assert_ne!(first.name, second.name);
assert!(first.name.starts_with("my-portrait-"));
assert_eq!(fs::read(first.path).unwrap(), bytes);
}
#[test]
fn rejects_mismatched_effective_format_before_write() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let error = store
.publish(&test_png(1, 1), None, Some(OutputFormat::Jpeg))
.unwrap_err();
assert_eq!(error.code, ErrorCode::Artifact);
assert_eq!(
fs::read_dir(root.path())
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.file_name() != OWNERSHIP_DIRECTORY)
.count(),
0
);
}
#[test]
fn cleanup_deletes_only_verified_bridge_owned_artifacts() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), Some("owned"), Some(OutputFormat::Png))
.unwrap();
let unowned = root.path().join("unowned.png");
fs::write(&unowned, test_png(2, 2)).unwrap();
let report = store
.cleanup(
RetentionPolicy {
max_age: Duration::ZERO,
..RetentionPolicy::default()
},
SystemTime::now() + Duration::from_secs(1),
)
.unwrap();
assert_eq!(report.deleted, 1);
assert!(!owned.path.exists());
assert!(unowned.exists());
}
#[test]
fn reads_only_matching_owned_artifacts() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let published = store
.publish(&test_png(2, 3), Some("owned"), Some(OutputFormat::Png))
.unwrap();
let content = store.read(&published.id).unwrap();
assert_eq!(content.id, published.id);
assert_eq!(content.name, published.name);
assert_eq!((content.metadata.width, content.metadata.height), (2, 3));
assert!(store.read("019f0000-0000-7000-8000-000000000000").is_err());
}
#[test]
fn cleanup_does_not_delete_an_owned_path_after_content_replacement() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), None, Some(OutputFormat::Png))
.unwrap();
fs::write(&owned.path, test_png(3, 3)).unwrap();
let report = store
.cleanup(
RetentionPolicy {
max_age: Duration::ZERO,
..RetentionPolicy::default()
},
SystemTime::now() + Duration::from_secs(1),
)
.unwrap();
assert_eq!(report.deleted, 0);
assert_eq!(report.skipped, 1);
assert!(owned.path.exists());
}
#[test]
fn cleanup_scan_is_bounded() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
for index in 0..3 {
fs::write(
store.ownership_root.join(format!("invalid-{index}.json")),
b"{}",
)
.unwrap();
}
let report = store
.cleanup(
RetentionPolicy {
max_scan_entries: 2,
..RetentionPolicy::default()
},
SystemTime::now(),
)
.unwrap();
assert_eq!(report.scanned, 2);
assert_eq!(report.skipped, 2);
assert!(report.scan_limit_reached);
}
#[test]
fn orphan_repair_is_auditable_and_removes_only_verified_owned_state() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), Some("orphan"), Some(OutputFormat::Png))
.unwrap();
let sidecar = store
.attach_metadata(&owned.id, &owned.name, br#"{"prompt":"test"}"#)
.unwrap();
let marker = store.ownership_root.join(format!("{}.json", owned.id));
let sidecar = root.path().join(sidecar.name);
let unowned = root.path().join("unowned.png");
fs::write(&unowned, test_png(1, 1)).unwrap();
fs::remove_file(&owned.path).unwrap();
let audit = store.repair_orphans(10, ArtifactRepairMode::Audit).unwrap();
assert_eq!(audit.scanned, 1);
assert_eq!(audit.orphaned_records, 1);
assert_eq!(audit.repaired, 0);
assert!(marker.exists());
assert!(sidecar.exists());
let repaired = store.repair_orphans(10, ArtifactRepairMode::Apply).unwrap();
assert_eq!(repaired.orphaned_records, 1);
assert_eq!(repaired.repaired, 1);
assert!(!marker.exists());
assert!(!sidecar.exists());
assert!(unowned.exists());
}
#[test]
fn orphan_repair_clears_only_an_absent_sidecar_reference() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), Some("sidecar"), Some(OutputFormat::Png))
.unwrap();
let sidecar = store
.attach_metadata(&owned.id, &owned.name, br#"{"prompt":"test"}"#)
.unwrap();
fs::remove_file(root.path().join(sidecar.name)).unwrap();
assert!(store.read(&owned.id).is_err());
let audit = store.repair_orphans(10, ArtifactRepairMode::Audit).unwrap();
assert_eq!(audit.missing_sidecars, 1);
assert_eq!(audit.repaired, 0);
let repaired = store.repair_orphans(10, ArtifactRepairMode::Apply).unwrap();
assert_eq!(repaired.missing_sidecars, 1);
assert_eq!(repaired.repaired, 1);
assert_eq!(store.read(&owned.id).unwrap().name, owned.name);
assert_eq!(
store
.repair_orphans(10, ArtifactRepairMode::Audit)
.unwrap()
.healthy,
1
);
}
#[test]
fn orphan_repair_never_modifies_changed_or_invalid_content() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), Some("changed"), Some(OutputFormat::Png))
.unwrap();
let sidecar = store
.attach_metadata(&owned.id, &owned.name, br#"{"prompt":"test"}"#)
.unwrap();
let sidecar = root.path().join(sidecar.name);
fs::write(&sidecar, br#"{"prompt":"changed"}"#).unwrap();
fs::remove_file(&owned.path).unwrap();
let report = store.repair_orphans(10, ArtifactRepairMode::Apply).unwrap();
assert_eq!(report.repaired, 0);
assert_eq!(report.orphaned_records, 1);
assert_eq!(report.skipped, 1);
assert!(sidecar.exists());
assert!(
store
.ownership_root
.join(format!("{}.json", owned.id))
.exists()
);
assert!(store.repair_orphans(0, ArtifactRepairMode::Audit).is_err());
}
#[cfg(unix)]
#[test]
fn reads_cleanup_and_repair_reject_replaced_parent_symlinks() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let bytes = test_png(2, 2);
let owned = store
.publish_with_options(
&bytes,
None,
Some(OutputFormat::Png),
ArtifactPublication {
directory: Some("gallery"),
..ArtifactPublication::default()
},
)
.unwrap();
fs::remove_file(&owned.path).unwrap();
fs::remove_dir(root.path().join("gallery")).unwrap();
fs::write(outside.path().join(owned.path.file_name().unwrap()), &bytes).unwrap();
symlink(outside.path(), root.path().join("gallery")).unwrap();
assert!(store.read(&owned.id).is_err());
let cleanup = store
.cleanup(
RetentionPolicy {
max_age: Duration::ZERO,
..RetentionPolicy::default()
},
SystemTime::now() + Duration::from_secs(1),
)
.unwrap();
assert_eq!(cleanup.deleted, 0);
assert_eq!(cleanup.skipped, 1);
let repair = store.repair_orphans(10, ArtifactRepairMode::Apply).unwrap();
assert_eq!(repair.repaired, 0);
assert_eq!(repair.skipped, 1);
assert!(
outside
.path()
.join(owned.path.file_name().unwrap())
.exists()
);
}
#[cfg(unix)]
#[test]
fn ownership_operations_reject_a_replaced_ownership_root() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), Some("owned"), Some(OutputFormat::Png))
.unwrap();
let marker_name = format!("{}.json", owned.id);
fs::copy(
store.ownership_root.join(&marker_name),
outside.path().join(&marker_name),
)
.unwrap();
fs::rename(
&store.ownership_root,
root.path().join("original-ownership"),
)
.unwrap();
symlink(outside.path(), &store.ownership_root).unwrap();
assert!(store.read(&owned.id).is_err());
assert!(
store
.cleanup(RetentionPolicy::default(), SystemTime::now())
.is_err()
);
assert!(store.repair_orphans(10, ArtifactRepairMode::Apply).is_err());
assert!(outside.path().join(marker_name).is_file());
assert!(owned.path.is_file());
}
#[test]
fn publishes_nested_exact_names_and_cleans_them_up() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let bytes = test_png(2, 2);
let owned = store
.publish_with_options(
&bytes,
None,
Some(OutputFormat::Png),
ArtifactPublication {
directory: Some("portraits/people"),
filename: Some("alice"),
collision: ArtifactCollisionPolicy::Error,
},
)
.unwrap();
assert_eq!(owned.name, "portraits/people/alice.png");
assert_eq!(fs::read(&owned.path).unwrap(), bytes);
let sidecar = store
.attach_metadata(&owned.id, &owned.name, br#"{"prompt":"test"}"#)
.unwrap();
assert!(root.path().join(&sidecar.name).is_file());
let report = store
.cleanup(
RetentionPolicy {
max_age: Duration::ZERO,
..RetentionPolicy::default()
},
SystemTime::now() + Duration::from_secs(1),
)
.unwrap();
assert_eq!(report.deleted, 1);
assert!(!owned.path.exists());
assert!(!root.path().join(sidecar.name).exists());
}
#[test]
fn explicit_collision_can_error_or_select_a_suffix() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let bytes = test_png(2, 2);
let exact = ArtifactPublication {
filename: Some("portrait.png"),
..ArtifactPublication::default()
};
store
.publish_with_options(&bytes, None, Some(OutputFormat::Png), exact)
.unwrap();
assert!(
store
.publish_with_options(&bytes, None, Some(OutputFormat::Png), exact)
.is_err()
);
let suffixed = store
.publish_with_options(
&bytes,
None,
Some(OutputFormat::Png),
ArtifactPublication {
collision: ArtifactCollisionPolicy::Suffix,
..exact
},
)
.unwrap();
assert_eq!(suffixed.name, "portrait-2.png");
}
#[test]
fn direct_store_calls_reject_filename_traversal_and_symlink_directories() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let bytes = test_png(2, 2);
assert!(
store
.publish_with_options(
&bytes,
None,
Some(OutputFormat::Png),
ArtifactPublication {
filename: Some("../escape.png"),
..ArtifactPublication::default()
},
)
.is_err()
);
#[cfg(unix)]
{
std::os::unix::fs::symlink(outside.path(), root.path().join("linked")).unwrap();
assert!(
store
.publish_with_options(
&bytes,
None,
Some(OutputFormat::Png),
ArtifactPublication {
directory: Some("linked"),
..ArtifactPublication::default()
},
)
.is_err()
);
}
}
#[test]
fn metadata_attachment_rejects_non_json_and_duplicate_sidecars() {
let root = tempfile::tempdir().unwrap();
let store = ArtifactStore::new(root.path(), ImageLimits::default()).unwrap();
let owned = store
.publish(&test_png(2, 2), None, Some(OutputFormat::Png))
.unwrap();
assert!(
store
.attach_metadata(&owned.id, &owned.name, b"not-json")
.is_err()
);
store
.attach_metadata(&owned.id, &owned.name, br#"{"version":1}"#)
.unwrap();
assert!(
store
.attach_metadata(&owned.id, &owned.name, br#"{"version":2}"#)
.is_err()
);
}
}