feat: implement protocol v2 for plugin system and add comprehensive tracing support
This commit is contained in:
1 parent
590c616d6f
commit
f07f54d4c1
12 files changed
+983
-143
No files matched your search
+114
-21
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user