Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Artifact conformance

on:
pull_request:
release:
types: [published]
schedule:
- cron: "17 4 * * 2"
workflow_dispatch:

permissions:
contents: read

jobs:
fast:
name: Fast hermetic corpus
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94"

- name: Run focused conformance harness
env:
RENDERFLOW_CONFORMANCE_TIER: "fast"
RENDERFLOW_CONFORMANCE_REPORT: "${{ runner.temp }}/renderflow-conformance-fast.json"
run: cargo test --package renderflow --test golden_conformance --locked

- name: Upload conformance report
if: always()
uses: actions/upload-artifact@v6
with:
name: "renderflow-conformance-fast-${{ github.run_id }}"
path: "${{ runner.temp }}/renderflow-conformance-fast.json"

maximal:
name: Maximal tool-backed corpus
if: github.event_name == 'schedule' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94"

- name: Install optional conformance providers
run: |
sudo apt-get update
sudo apt-get install --yes ffmpeg pandoc

- name: Run maximal conformance harness
env:
RENDERFLOW_CONFORMANCE_TIER: "maximal"
RENDERFLOW_CONFORMANCE_REPORT: "${{ runner.temp }}/renderflow-conformance-maximal.json"
run: cargo test --package renderflow --test golden_conformance --locked

- name: Upload conformance report
if: always()
uses: actions/upload-artifact@v6
with:
name: "renderflow-conformance-maximal-${{ github.run_id }}"
path: "${{ runner.temp }}/renderflow-conformance-maximal.json"
retention-days: 90
39 changes: 38 additions & 1 deletion crates/renderflow-core/src/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ pub struct DetectionConflict {
/// The `buf` slice should contain at least the first 16 bytes of the file for
/// reliable results, but longer slices are handled correctly.
pub fn detect_from_bytes(buf: &[u8]) -> Option<Format> {
if buf.starts_with(b"RIFF") && buf.len() >= 12 {
match &buf[8..12] {
b"WAVE" => return Some(Format::Wav),
b"AVI " => return Some(Format::Avi),
b"WEBP" => return Some(Format::Webp),
_ => {}
}
}
if buf.len() >= 12 && buf.get(4..8) == Some(b"ftyp") {
return Some(match &buf[8..12] {
b"avif" | b"avis" => Format::Avif,
_ => Format::Mp4,
});
}

// Iterate in a deterministic order so that ties between formats sharing
// a prefix are resolved consistently.
let registry = FormatCapabilityRegistry::global();
Expand All @@ -104,7 +119,10 @@ pub fn detect_from_bytes(buf: &[u8]) -> Option<Format> {

// Prefer the match with the longest (most specific) signature to avoid
// false positives when a shorter signature is a prefix of another.
candidates.sort_by(|a, b| b.1.cmp(&a.1));
candidates.sort_by(|a, b| {
b.1.cmp(&a.1)
.then_with(|| a.0.to_string().cmp(&b.0.to_string()))
});
candidates.into_iter().next().map(|(f, _)| f)
}

Expand Down Expand Up @@ -198,6 +216,25 @@ mod tests {
assert_eq!(result, Some(Format::Flac));
}

#[test]
fn detect_riff_formats_from_form_type() {
assert_eq!(detect_from_bytes(b"RIFF\0\0\0\0WAVE"), Some(Format::Wav));
assert_eq!(detect_from_bytes(b"RIFF\0\0\0\0AVI "), Some(Format::Avi));
assert_eq!(detect_from_bytes(b"RIFF\0\0\0\0WEBP"), Some(Format::Webp));
}

#[test]
fn detect_iso_base_media_from_major_brand() {
assert_eq!(
detect_from_bytes(b"\0\0\0\x18ftypisom\0\0\0\0"),
Some(Format::Mp4)
);
assert_eq!(
detect_from_bytes(b"\0\0\0\x18ftypavif\0\0\0\0"),
Some(Format::Avif)
);
}

#[test]
fn detect_mp3_from_id3_tag() {
let buf = b"ID3\x03\x00\x00\x00\x00\x00\x00";
Expand Down
4 changes: 3 additions & 1 deletion crates/renderflow-core/src/intake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,9 @@ impl IntakeEngine {
.find(|descriptor| descriptor.id == "zip")
.copied()
} else {
magic_matches.first().copied()
crate::detect::detect_from_bytes(bytes)
.and_then(|format| registry.get(format))
.or_else(|| magic_matches.first().copied())
};
if let Some(descriptor) = magic_match {
signals.push(IntakeSignal {
Expand Down
30 changes: 26 additions & 4 deletions crates/renderflow-core/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,10 +821,7 @@ fn conformance_row(
.map(|tool| tool.stable_id().to_string())
.collect(),
validator_ids,
fixture_ids: match descriptor.id {
"png" => vec!["fixture.corrupt.png.truncated".to_string()],
_ => Vec::new(),
},
fixture_ids: golden_fixture_ids(descriptor.id),
supported_platforms: vec![
"linux".to_string(),
"macos".to_string(),
Expand All @@ -836,6 +833,22 @@ fn conformance_row(
}
}

fn golden_fixture_ids(format: &str) -> Vec<String> {
let ids: &[&str] = match format {
"markdown" => &["fixture.document.markdown"],
"pdf" => &["fixture.document.pdf"],
"png" => &["fixture.image.png", "fixture.mismatch.jpeg-png"],
"svg" => &["fixture.image.svg"],
"wav" => &["fixture.audio.wav"],
"mp4" => &["fixture.video.mp4"],
"zip" => &["fixture.archive.zip", "fixture.corrupt.zip"],
"json" => &["fixture.data.json"],
"srt" => &["fixture.subtitle.srt"],
_ => &[],
};
ids.iter().map(|id| (*id).to_string()).collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -862,5 +875,14 @@ mod tests {
let second = CapabilityConformanceMatrix::builtins();
assert_eq!(first, second);
assert!(!first.formats.is_empty());
assert_eq!(
first
.formats
.iter()
.find(|row| row.format == "wav")
.unwrap()
.fixture_ids,
vec!["fixture.audio.wav"]
);
}
}
Loading
Loading