feat: implement protocol v2 for plugin system and add comprehensive tracing support
Release / check-release (push) Successful in 16s
Release / build (push) Skipped
Release / package (deb) (push) Skipped
Release / package (rpm) (push) Skipped
Release / publish (push) Skipped
CI / test (push) Successful in 1m58s

This commit is contained in:
Elias Wendland committed 2026-07-17 17:46:14 +02:00
1 parent 590c616d6f
commit f07f54d4c1
12 files changed
+983 -143

No files matched your search

+114 -21
View File
@@ -24,55 +24,78 @@ pub struct DetectedFormat {
}
fn normalized(format: &str) -> String {
match format.to_ascii_lowercase().as_str() {
let normalized = match format.to_ascii_lowercase().as_str() {
"jpg" => "jpeg".to_owned(),
"htm" => "html".to_owned(),
"txt" | "ascii" => "text".to_owned(),
other => other.to_owned(),
}
};
tracing::trace!(
input = format,
output = normalized,
"normalized format name"
);
normalized
}
pub fn requested_format(path: Option<&Path>, explicit: Option<&str>) -> Option<String> {
explicit.map(normalized).or_else(|| {
tracing::trace!(path = ?path, explicit, "resolving requested format");
let requested = explicit.map(normalized).or_else(|| {
path.and_then(Path::extension)
.and_then(|value| value.to_str())
.map(normalized)
})
});
tracing::trace!(?requested, "resolved requested format");
requested
}
pub fn identify_path(path: &Path, explicit: Option<&str>) -> Result<DetectedFormat, String> {
tracing::debug!(path = %path.display(), explicit, is_directory = path.is_dir(), "starting path identification");
if path.is_dir() {
tracing::trace!(path = %path.display(), "identifying directory artifact");
if explicit.is_some_and(|format| normalized(format) != "frames") {
return Err("directory inputs currently support only the 'frames' format".to_owned());
}
let manifest = path.join(".convertis-frames.json");
tracing::trace!(manifest = %manifest.display(), exists = manifest.exists(), "checking frame manifest");
if manifest.exists() {
let value: serde_json::Value = serde_json::from_slice(
&fs::read(&manifest)
.map_err(|error| format!("could not read {}: {error}", manifest.display()))?,
)
.map_err(|error| format!("invalid frame manifest: {error}"))?;
if value
tracing::trace!(manifest = %manifest.display(), "reading frame manifest");
let bytes = fs::read(&manifest)
.map_err(|error| format!("could not read {}: {error}", manifest.display()))?;
tracing::trace!(manifest = %manifest.display(), byte_count = bytes.len(), "parsing frame manifest");
let value: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|error| format!("invalid frame manifest: {error}"))?;
let schema_version = value
.get("schema_version")
.and_then(serde_json::Value::as_u64)
!= Some(1)
{
.and_then(serde_json::Value::as_u64);
tracing::trace!(?schema_version, "read frame manifest schema version");
if schema_version != Some(1) {
return Err("unsupported frame manifest schema".to_owned());
}
} else {
tracing::trace!(path = %path.display(), "scanning directory for recognizable frames");
let mut detected = None;
for entry in fs::read_dir(path)
.map_err(|error| error.to_string())?
.flatten()
{
for entry in fs::read_dir(path).map_err(|error| error.to_string())? {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::trace!(%error, "could not inspect frame-directory entry");
continue;
}
};
tracing::trace!(entry = %entry.path().display(), "inspecting frame-directory entry");
if !entry.path().is_file() {
tracing::trace!(entry = %entry.path().display(), "skipping non-file frame-directory entry");
continue;
}
let bytes = fs::read(entry.path()).map_err(|error| error.to_string())?;
tracing::trace!(entry = %entry.path().display(), byte_count = bytes.len(), "read potential frame file");
let Some(format) = identify_bytes(&bytes) else {
tracing::trace!(entry = %entry.path().display(), "frame candidate was not recognizable");
continue;
};
if format.media_kind != MediaKind::Image {
tracing::trace!(entry = %entry.path().display(), media_kind = ?format.media_kind, "frame candidate was not a still image");
continue;
}
if detected
@@ -83,6 +106,7 @@ pub fn identify_path(path: &Path, explicit: Option<&str>) -> Result<DetectedForm
"frame directories without metadata must use one image format".to_owned(),
);
}
tracing::trace!(entry = %entry.path().display(), format = %format.format, "accepted frame candidate");
detected = Some(format.format);
}
if detected.is_none() {
@@ -91,6 +115,7 @@ pub fn identify_path(path: &Path, explicit: Option<&str>) -> Result<DetectedForm
);
}
}
tracing::debug!(path = %path.display(), "identified frames directory");
return Ok(DetectedFormat {
format: "frames".to_owned(),
mime: "application/vnd.convertis.frames+json".to_owned(),
@@ -98,51 +123,109 @@ pub fn identify_path(path: &Path, explicit: Option<&str>) -> Result<DetectedForm
artifact_kind: ArtifactKind::Directory,
});
}
tracing::trace!(path = %path.display(), "reading file for content identification");
let bytes =
fs::read(path).map_err(|error| format!("could not read {}: {error}", path.display()))?;
tracing::trace!(path = %path.display(), byte_count = bytes.len(), "read file for identification");
if let Some(format) = explicit {
return Ok(from_format(&normalized(format), &bytes));
let format = normalized(format);
tracing::debug!(path = %path.display(), format, "using explicit input format");
return Ok(from_format(&format, &bytes));
}
identify_bytes(&bytes)
.ok_or_else(|| format!("could not identify {} from its contents", path.display()))
let identified = identify_bytes(&bytes)
.ok_or_else(|| format!("could not identify {} from its contents", path.display()))?;
tracing::debug!(path = %path.display(), format = %identified.format, "identified file contents");
Ok(identified)
}
pub fn identify_bytes(bytes: &[u8]) -> Option<DetectedFormat> {
tracing::trace!(
byte_count = bytes.len(),
"starting content signature identification"
);
if bytes.starts_with(&[0, 0, 1, 0]) {
tracing::trace!(rule = "ico-header", "matched content signature");
return Some(from_format("ico", bytes));
}
tracing::trace!(
rule = "ico-header",
matched = false,
"checked content signature"
);
if bytes.starts_with(b"<!DOCTYPE html") || bytes.starts_with(b"<html") {
tracing::trace!(rule = "html-prefix", "matched content signature");
return Some(from_format("html", bytes));
}
tracing::trace!(
rule = "html-prefix",
matched = false,
"checked content signature"
);
if bytes.windows(4).any(|window| window == b"M4A ") {
tracing::trace!(rule = "m4a-brand", "matched content signature");
return Some(from_format("m4a", bytes));
}
tracing::trace!(
rule = "m4a-brand",
matched = false,
"checked content signature"
);
if bytes.starts_with(b"OggS") {
tracing::trace!(rule = "ogg-header", "matched content signature");
if bytes.windows(8).any(|window| window == b"OpusHead") {
tracing::trace!(rule = "opus-header", "matched Ogg subtype signature");
return Some(from_format("opus", bytes));
}
if bytes.windows(6).any(|window| window == b"theora") {
tracing::trace!(rule = "theora-header", "matched Ogg subtype signature");
return Some(from_format("ogv", bytes));
}
tracing::trace!("no Ogg subtype signature matched; using generic Ogg format");
return Some(from_format("ogg", bytes));
}
tracing::trace!(
rule = "ogg-header",
matched = false,
"checked content signature"
);
if let Some(kind) = infer::get(bytes) {
return Some(from_format(&normalized(kind.extension()), bytes));
let format = normalized(kind.extension());
tracing::trace!(
infer_extension = kind.extension(),
mime = kind.mime_type(),
format,
"matched infer signature database"
);
return Some(from_format(&format, bytes));
}
tracing::trace!("infer signature database did not match");
let text = std::str::from_utf8(bytes).ok()?;
tracing::trace!(
utf8 = true,
character_count = text.chars().count(),
"content is valid UTF-8"
);
if !text.contains('\0') {
tracing::trace!(rule = "utf8-without-nul", "classified content as text");
return Some(from_format("text", bytes));
}
tracing::trace!("content did not match any supported identification rule");
None
}
fn from_format(format: &str, bytes: &[u8]) -> DetectedFormat {
tracing::trace!(
format,
byte_count = bytes.len(),
"deriving media details from format"
);
let animation = match format {
"webp" => bytes.len() > 20 && &bytes[12..16] == b"VP8X" && bytes[20] & 0x02 != 0,
"png" | "apng" => bytes.windows(4).any(|window| window == b"acTL"),
"gif" => gif_is_animated(bytes),
_ => false,
};
tracing::trace!(format, animation, "determined animation state");
let media_kind = if animation {
MediaKind::Animation
} else if matches!(
@@ -194,6 +277,7 @@ fn from_format(format: &str, bytes: &[u8]) -> DetectedFormat {
} else {
format.to_owned()
};
tracing::trace!(format, mime, ?media_kind, "derived media details");
DetectedFormat {
format,
mime: mime.to_owned(),
@@ -203,18 +287,27 @@ fn from_format(format: &str, bytes: &[u8]) -> DetectedFormat {
}
fn gif_is_animated(bytes: &[u8]) -> bool {
tracing::trace!(byte_count = bytes.len(), "checking GIF animation state");
let mut options = gif::DecodeOptions::new();
options.set_color_output(gif::ColorOutput::Indexed);
let Ok(mut decoder) = options.read_info(std::io::Cursor::new(bytes)) else {
tracing::trace!("GIF decoder rejected content while checking animation");
return false;
};
let mut frames = 0;
while let Ok(Some(_)) = decoder.read_next_frame() {
frames += 1;
tracing::trace!(frames, "decoded GIF frame while checking animation");
if frames > 1 {
tracing::trace!(frames, animated = true, "GIF has multiple frames");
return true;
}
}
tracing::trace!(
frames,
animated = false,
"GIF has fewer than two decodable frames"
);
false
}