Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
324 lines
13 KiB
Rust
324 lines
13 KiB
Rust
//! The thumbnail view-regions contract.
|
|
//!
|
|
//! A thumbnail is still ONE mandatory picture. `views` is metadata about that
|
|
//! picture — which parts of it are a spectrogram, a waveform strip, a packed
|
|
//! sprite sheet — so a consumer reads the layout instead of measuring pixels
|
|
//! and guessing. These tests pin the two properties that make that safe to
|
|
//! rely on: the kind numbers are frozen and append-only, and the five fields a
|
|
//! thumbnail has always carried stay exactly where they were on the wire.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use makepad_asset_data::*;
|
|
|
|
/// Frozen wire numbers. A kind may be APPENDED with the next free tag; an
|
|
/// existing one may never be renumbered, because every stored manifest in
|
|
/// every catalog already means the old number.
|
|
#[test]
|
|
fn view_kind_tags_are_frozen_and_append_only() {
|
|
// The tags are not public API, so they are pinned through the only thing
|
|
// that is: the bytes. One view per kind, encoded on its own, and the tag
|
|
// is the first byte of the view block.
|
|
let tag_of = |kind: ThumbnailViewKind| -> u8 {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![ThumbnailView::whole(kind, thumb.width, thumb.height)];
|
|
let bytes = manifest_with(thumb).to_canonical_bytes().unwrap();
|
|
let at = views_offset(&bytes);
|
|
// count:u32 then the first view's kind tag.
|
|
bytes[at + 4]
|
|
};
|
|
assert_eq!(tag_of(ThumbnailViewKind::Image), 0);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Fft), 1);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Wave), 2);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Anim), 3);
|
|
}
|
|
|
|
/// An unknown kind refuses rather than being taken for a known one: that is
|
|
/// what makes appending safe. A future `video` view read by today's decoder
|
|
/// is an error, never an `image`.
|
|
#[test]
|
|
fn an_unknown_kind_refuses() {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![ThumbnailView::whole(ThumbnailViewKind::Anim, 512, 512)];
|
|
let mut bytes = manifest_with(thumb).to_canonical_bytes().unwrap();
|
|
let at = views_offset(&bytes);
|
|
bytes[at + 4] = 99;
|
|
assert!(matches!(
|
|
AssetManifest::from_canonical_bytes(&bytes),
|
|
Err(AssetDataError::BadTag { what: "ThumbnailViewKind", found: 99 })
|
|
));
|
|
// And so does an unknown LAYOUT tag, for the same reason.
|
|
let mut bytes = manifest_with({
|
|
let mut t = thumbnail();
|
|
t.views = vec![ThumbnailView::whole(ThumbnailViewKind::Image, 512, 512)];
|
|
t
|
|
})
|
|
.to_canonical_bytes()
|
|
.unwrap();
|
|
let at = views_offset(&bytes);
|
|
bytes[at + 5] = 7;
|
|
assert!(matches!(
|
|
AssetManifest::from_canonical_bytes(&bytes),
|
|
Err(AssetDataError::BadTag { what: "ThumbnailLayout", found: 7 })
|
|
));
|
|
}
|
|
|
|
/// The compatibility promise, proved rather than asserted: a reader that
|
|
/// knows only the five fields a thumbnail carried before views existed walks
|
|
/// them off a NEW manifest and gets exactly the values the producer wrote.
|
|
/// The views ride behind them, where such a reader simply never looks.
|
|
#[test]
|
|
fn an_old_shaped_reader_still_parses_a_new_thumbnail() {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![
|
|
ThumbnailView::rect(ThumbnailViewKind::Fft, 0, 0, 512, 448),
|
|
ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 448, 512, 64),
|
|
];
|
|
thumb.canonicalize();
|
|
let bytes = manifest_with(thumb.clone()).to_canonical_bytes().unwrap();
|
|
|
|
// Exactly the decode an old reader performed: blob, media, width, height,
|
|
// byte_len, in that order, at those widths, big-endian.
|
|
let at = find(&bytes, thumb.blob.as_bytes()).expect("thumbnail blob on the wire");
|
|
let mut p = at + 32;
|
|
let u8_ = |p: &mut usize| {
|
|
let v = bytes[*p];
|
|
*p += 1;
|
|
v
|
|
};
|
|
let u32_ = |p: &mut usize| {
|
|
let v = u32::from_be_bytes(bytes[*p..*p + 4].try_into().unwrap());
|
|
*p += 4;
|
|
v
|
|
};
|
|
let u64_ = |p: &mut usize| {
|
|
let v = u64::from_be_bytes(bytes[*p..*p + 8].try_into().unwrap());
|
|
*p += 8;
|
|
v
|
|
};
|
|
assert_eq!(u8_(&mut p), 0, "media tag: Png");
|
|
assert_eq!(u32_(&mut p), thumb.width);
|
|
assert_eq!(u32_(&mut p), thumb.height);
|
|
assert_eq!(u64_(&mut p), thumb.byte_len);
|
|
// Everything the old reader knew is consumed, and the picture it would
|
|
// fetch and draw is byte-identical to before. What it stopped at is the
|
|
// views block: a count it never knew to read.
|
|
assert_eq!(u32_(&mut p), 2, "and the new metadata sits behind it");
|
|
|
|
// The same five fields, from a thumbnail with NO views, land at the same
|
|
// offsets with the same values — views are additive, never a reshuffle.
|
|
let plain = ThumbnailMeta::plain(thumb.blob, thumb.media, thumb.width, thumb.height,
|
|
thumb.byte_len);
|
|
let plain_bytes = manifest_with(plain).to_canonical_bytes().unwrap();
|
|
let plain_at = find(&plain_bytes, thumb.blob.as_bytes()).unwrap();
|
|
assert_eq!(
|
|
&plain_bytes[plain_at..plain_at + 32 + 17],
|
|
&bytes[at..at + 32 + 17],
|
|
"the fixed head of a thumbnail is the same bytes either way"
|
|
);
|
|
}
|
|
|
|
/// Absence is not a claim: a thumbnail with no views round-trips, and says
|
|
/// nothing at all about being a sheet — which is exactly what every revision
|
|
/// baked before this field existed means.
|
|
#[test]
|
|
fn no_views_means_no_claim() {
|
|
let manifest = weapon_manifest();
|
|
let thumb = manifest.thumbnail.clone().unwrap();
|
|
assert!(thumb.views.is_empty());
|
|
assert_eq!(thumb.animation(), None);
|
|
assert_eq!(thumb.view(ThumbnailViewKind::Anim), None);
|
|
let back = AssetManifest::from_canonical_bytes(&manifest.to_canonical_bytes().unwrap()).unwrap();
|
|
assert_eq!(back, manifest);
|
|
}
|
|
|
|
/// Views round-trip through the canonical encoding, and a producer that found
|
|
/// them in any order ships identical bytes after `canonicalize` — or refuses,
|
|
/// rather than being silently reordered.
|
|
#[test]
|
|
fn views_round_trip_and_have_one_canonical_order() {
|
|
let composite = audio_composite();
|
|
let manifest = manifest_with(composite.clone());
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
let back = AssetManifest::from_canonical_bytes(&bytes).unwrap();
|
|
assert_eq!(back.thumbnail.as_ref().unwrap(), &composite);
|
|
assert_eq!(back.to_canonical_bytes().unwrap(), bytes);
|
|
|
|
let mut shuffled = manifest_with({
|
|
let mut t = audio_composite();
|
|
t.views.reverse();
|
|
t
|
|
});
|
|
assert!(matches!(
|
|
shuffled.to_canonical_bytes(),
|
|
Err(AssetDataError::NotSorted { what: "thumbnail views" })
|
|
));
|
|
shuffled.canonicalize();
|
|
assert_eq!(shuffled.to_canonical_bytes().unwrap(), bytes);
|
|
|
|
// The same region declared twice is a producer bug, not a set.
|
|
let mut duped = manifest_with({
|
|
let mut t = audio_composite();
|
|
t.views.push(t.views[0]);
|
|
t.canonicalize();
|
|
t
|
|
});
|
|
duped.canonicalize();
|
|
assert!(matches!(
|
|
duped.to_canonical_bytes(),
|
|
Err(AssetDataError::Duplicate { what: "thumbnail views" })
|
|
));
|
|
}
|
|
|
|
/// A view must fit the picture it claims to describe. A region that runs off
|
|
/// the edge is a producer that stamped a layout it did not write, and a
|
|
/// consumer that trusted it would read someone else's pixels.
|
|
#[test]
|
|
fn a_view_must_fit_inside_its_picture() {
|
|
let refuse = |views: Vec<ThumbnailView>| {
|
|
let mut t = thumbnail();
|
|
t.views = views;
|
|
t.canonicalize();
|
|
manifest_with(t).to_canonical_bytes().unwrap_err()
|
|
};
|
|
// 512x512 picture.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Image, 0, 0, 513, 512)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect bounds" }
|
|
));
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 0, 0, 10)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect" }
|
|
));
|
|
// A near-overflow origin must refuse, not wrap into range.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Image, u32::MAX, 0, 8, 8)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect bounds" }
|
|
));
|
|
// Cells: 4 columns of 128 is exactly 512 wide; a fifth column is not.
|
|
let cells = |cols, count| ThumbnailCells { cols, cell_w: 128, cell_h: 128, first: 0, count };
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(5, 5), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells bounds" }
|
|
));
|
|
// 4x4 cells of 128 fill a 512x512 picture exactly, and are accepted.
|
|
let mut ok = thumbnail();
|
|
ok.views = vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 16), 8.0)];
|
|
manifest_with(ok).to_canonical_bytes().expect("a layout that fits is accepted");
|
|
// A seventeenth cell needs a fifth row.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 17), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells bounds" }
|
|
));
|
|
// An empty range describes nothing.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 0), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells" }
|
|
));
|
|
}
|
|
|
|
/// A cycling rate is bounded and positive: zero fps is a still declared as an
|
|
/// animation, and 10 000 fps is a producer bug that would spin a consumer's
|
|
/// frame clock.
|
|
#[test]
|
|
fn fps_is_bounded_and_positive() {
|
|
let with_fps = |fps: f32| {
|
|
let mut t = thumbnail();
|
|
t.views = vec![ThumbnailView {
|
|
kind: ThumbnailViewKind::Anim,
|
|
layout: ThumbnailLayout::Cells(ThumbnailCells {
|
|
cols: 4,
|
|
cell_w: 128,
|
|
cell_h: 128,
|
|
first: 0,
|
|
count: 4,
|
|
}),
|
|
fps: Some(fps),
|
|
}];
|
|
manifest_with(t).to_canonical_bytes()
|
|
};
|
|
assert!(with_fps(8.0).is_ok());
|
|
assert!(with_fps(240.0).is_ok());
|
|
assert!(matches!(
|
|
with_fps(0.0),
|
|
Err(AssetDataError::Malformed { what: "thumbnail view fps" })
|
|
));
|
|
assert!(matches!(
|
|
with_fps(-1.0),
|
|
Err(AssetDataError::Malformed { what: "thumbnail view fps" })
|
|
));
|
|
assert!(matches!(
|
|
with_fps(1000.0),
|
|
Err(AssetDataError::OverBudget { what: "thumbnail view fps", .. })
|
|
));
|
|
}
|
|
|
|
/// More regions than the budget refuses before allocation, on both sides.
|
|
#[test]
|
|
fn the_view_count_is_bounded() {
|
|
let mut t = thumbnail();
|
|
t.views = (0..9)
|
|
.map(|i| ThumbnailView::rect(ThumbnailViewKind::Image, i * 8, 0, 8, 8))
|
|
.collect();
|
|
assert!(matches!(
|
|
manifest_with(t).to_canonical_bytes(),
|
|
Err(AssetDataError::OverBudget { what: "thumbnail views", .. })
|
|
));
|
|
}
|
|
|
|
/// The two questions consumers actually ask, answered from the manifest:
|
|
/// "is this a sheet, and what is its layout" and "where is the wave strip".
|
|
#[test]
|
|
fn the_accessors_answer_the_questions_consumers_used_to_guess() {
|
|
let composite = audio_composite();
|
|
assert_eq!(composite.animation(), None, "a spectrogram is not a sprite sheet");
|
|
assert!(matches!(
|
|
composite.view(ThumbnailViewKind::Wave).map(|v| v.layout),
|
|
Some(ThumbnailLayout::Rect(ThumbnailRect { x: 0, y: 448, w: 512, h: 64 }))
|
|
));
|
|
|
|
let mut sheet = thumbnail();
|
|
let cells = ThumbnailCells { cols: 4, cell_w: 128, cell_h: 128, first: 0, count: 11 };
|
|
sheet.views = vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells, 12.0)];
|
|
let (found, fps) = sheet.animation().expect("a declared sheet says so");
|
|
assert_eq!(found, cells);
|
|
assert_eq!(fps, 12.0);
|
|
// Eleven real frames in a 4x4 grid: the five padding cells the packer
|
|
// wrote to clear the 256px floor are NOT frames, and nobody has to scan
|
|
// pixels to find that out.
|
|
assert_eq!(found.count, 11);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// The audio composite the importer bakes: FFT over most of the picture, a
|
|
/// thin wave strip along the bottom edge, both declared.
|
|
fn audio_composite() -> ThumbnailMeta {
|
|
let mut t = thumbnail();
|
|
t.views = vec![
|
|
ThumbnailView::rect(ThumbnailViewKind::Fft, 0, 0, 512, 448),
|
|
ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 448, 512, 64),
|
|
];
|
|
t.canonicalize();
|
|
t
|
|
}
|
|
|
|
fn manifest_with(thumbnail: ThumbnailMeta) -> AssetManifest {
|
|
let mut m = weapon_manifest();
|
|
m.thumbnail = Some(thumbnail);
|
|
m
|
|
}
|
|
|
|
/// Byte offset of the views count inside an encoded manifest: the thumbnail
|
|
/// blob, then the 17 fixed bytes an old reader consumes.
|
|
fn views_offset(bytes: &[u8]) -> usize {
|
|
let thumb = weapon_manifest().thumbnail.clone().unwrap();
|
|
find(bytes, thumb.blob.as_bytes()).expect("thumbnail blob on the wire") + 32 + 17
|
|
}
|
|
|
|
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
|
haystack.windows(needle.len()).position(|w| w == needle)
|
|
}
|