Schema major 1 Phase E: resolved-layout length prefixes u64 -> u32

Unifies the resolved-layout's 13 length/count prefixes to u32 (schema major 1),
matching the core codec's put_len; the manifest-embedded barrier blobs stay u64
(regime (b), canonical). The resolved layout is an encode-only, non-persisted
determinism fingerprint, so there is no migrate-on-read and no bundle
LayoutCache machinery (that would be ahead of a producer) — a cross-major layout
cache is regenerated, never decoded.

- resolved.rs: push_u64 length helper -> push_len (u32 LE, debug_assert
  n <= u32::MAX). Data fields (rgba, layer, page.number, smufl_version) untouched.
- Byte-shape lock: count_prefixes_are_u32_width_locked asserts an empty layout's
  four counts occupy 4x4 bytes after the 32-byte ScoreVersion (catalog length
  recomputed independently), so a revert to u64 fails (verified: 128 vs 112).

Zero golden churn (every existing resolved test is self-comparison). Full gate
green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
This commit is contained in:
Levi Neuwirth 2026-07-06 13:20:40 -04:00
parent 4598f30ddd
commit 64de92605f
1 changed files with 50 additions and 15 deletions

View File

@ -114,18 +114,18 @@ impl ResolvedLayoutIR {
impl CanonicalEncode for ResolvedLayoutIR { impl CanonicalEncode for ResolvedLayoutIR {
fn encode_canonical(&self, out: &mut Vec<u8>) { fn encode_canonical(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.source.0); out.extend_from_slice(&self.source.0);
push_u64(out, self.pages.len() as u64); push_len(out, self.pages.len());
for page in &self.pages { for page in &self.pages {
encode_page(out, page); encode_page(out, page);
} }
push_u64(out, self.glyphs.len() as u64); push_len(out, self.glyphs.len());
for glyph in &self.glyphs { for glyph in &self.glyphs {
encode_provenance(out, &glyph.provenance); encode_provenance(out, &glyph.provenance);
// The glyph reference itself (so swapping two glyphs' symbols, even // The glyph reference itself (so swapping two glyphs' symbols, even
// with the consulted-name set unchanged, changes the canonical bytes // with the consulted-name set unchanged, changes the canonical bytes
// — the encoding is injective in glyph identity). // — the encoding is injective in glyph identity).
let name = glyph.glyph.as_str().as_bytes(); let name = glyph.glyph.as_str().as_bytes();
push_u64(out, name.len() as u64); push_len(out, name.len());
out.extend_from_slice(name); out.extend_from_slice(name);
let (qx, qy) = quantize(glyph.position); let (qx, qy) = quantize(glyph.position);
qx.encode_canonical(out); qx.encode_canonical(out);
@ -145,7 +145,7 @@ impl CanonicalEncode for ResolvedLayoutIR {
out.extend_from_slice(&glyph.style.rgba.to_le_bytes()); out.extend_from_slice(&glyph.style.rgba.to_le_bytes());
out.extend_from_slice(&glyph.layer.to_le_bytes()); out.extend_from_slice(&glyph.layer.to_le_bytes());
} }
push_u64(out, self.strokes.len() as u64); push_len(out, self.strokes.len());
for stroke in &self.strokes { for stroke in &self.strokes {
encode_provenance(out, &stroke.provenance); encode_provenance(out, &stroke.provenance);
let (fx, fy) = quantize(stroke.from); let (fx, fy) = quantize(stroke.from);
@ -158,7 +158,7 @@ impl CanonicalEncode for ResolvedLayoutIR {
out.extend_from_slice(&stroke.style.rgba.to_le_bytes()); out.extend_from_slice(&stroke.style.rgba.to_le_bytes());
out.extend_from_slice(&stroke.layer.to_le_bytes()); out.extend_from_slice(&stroke.layer.to_le_bytes());
} }
push_u64(out, self.engraving_decisions.len() as u64); push_len(out, self.engraving_decisions.len());
for decision in &self.engraving_decisions { for decision in &self.engraving_decisions {
encode_decision(out, decision); encode_decision(out, decision);
} }
@ -179,24 +179,24 @@ fn encode_page(out: &mut Vec<u8>, page: &ResolvedPage) {
] { ] {
encode_staff_space(out, margin); encode_staff_space(out, margin);
} }
push_u64(out, page.systems.len() as u64); push_len(out, page.systems.len());
for system in &page.systems { for system in &page.systems {
encode_provenance(out, &system.provenance); encode_provenance(out, &system.provenance);
encode_rect(out, system.bounding_box); encode_rect(out, system.bounding_box);
push_u64(out, system.staves.len() as u64); push_len(out, system.staves.len());
for staff in &system.staves { for staff in &system.staves {
encode_provenance(out, &staff.provenance); encode_provenance(out, &staff.provenance);
out.extend_from_slice(&staff.staff.canonical_bytes()); out.extend_from_slice(&staff.staff.canonical_bytes());
encode_rect(out, staff.bounding_box); encode_rect(out, staff.bounding_box);
} }
push_u64(out, system.measures.len() as u64); push_len(out, system.measures.len());
for measure in &system.measures { for measure in &system.measures {
encode_provenance(out, &measure.provenance); encode_provenance(out, &measure.provenance);
out.extend_from_slice(&measure.measure.canonical_bytes()); out.extend_from_slice(&measure.measure.canonical_bytes());
encode_rect(out, measure.bounding_box); encode_rect(out, measure.bounding_box);
} }
} }
push_u64(out, page.free_objects.len() as u64); push_len(out, page.free_objects.len());
for object in &page.free_objects { for object in &page.free_objects {
push_u128(out, object.0); push_u128(out, object.0);
} }
@ -229,8 +229,15 @@ fn encode_f32(out: &mut Vec<u8>, value: f32) {
.encode_canonical(out); .encode_canonical(out);
} }
fn push_u64(out: &mut Vec<u8>, v: u64) { /// Appends a `u32` little-endian length/count prefix (schema major 1: the
out.extend_from_slice(&v.to_le_bytes()); /// resolved-layout unifies its length prefixes to `u32`, matching the core
/// codec's `put_len`; no resolved-layout count nears 4 GB). The resolved layout
/// is a non-canonical, encode-only determinism fingerprint (Appendix D
/// §"Quantized Layout Coordinates"), so this width change has no persisted-format
/// migration — a cross-major layout cache is regenerated, never decoded.
fn push_len(out: &mut Vec<u8>, n: usize) {
debug_assert!(n <= u32::MAX as usize, "resolved-layout length exceeds u32");
out.extend_from_slice(&(n as u32).to_le_bytes());
} }
fn push_u128(out: &mut Vec<u8>, v: u128) { fn push_u128(out: &mut Vec<u8>, v: u128) {
@ -249,7 +256,7 @@ fn quantize(p: Point) -> (QuantizedCoord, QuantizedCoord) {
/// Length-prefixes an id's canonical bytes (self-delimiting). /// Length-prefixes an id's canonical bytes (self-delimiting).
fn encode_source(out: &mut Vec<u8>, source: &TypedObjectId) { fn encode_source(out: &mut Vec<u8>, source: &TypedObjectId) {
let bytes = source.to_canonical_bytes(); let bytes = source.to_canonical_bytes();
push_u64(out, bytes.len() as u64); push_len(out, bytes.len());
out.extend_from_slice(&bytes); out.extend_from_slice(&bytes);
} }
@ -271,9 +278,9 @@ fn encode_provenance(out: &mut Vec<u8>, p: &Provenance) {
.collect(); .collect();
deps.sort(); deps.sort();
deps.dedup(); deps.dedup();
push_u64(out, deps.len() as u64); push_len(out, deps.len());
for bytes in deps { for bytes in deps {
push_u64(out, bytes.len() as u64); push_len(out, bytes.len());
out.extend_from_slice(&bytes); out.extend_from_slice(&bytes);
} }
} }
@ -326,7 +333,7 @@ fn encode_catalog(out: &mut Vec<u8>, c: &GlyphCatalogIdentity) {
out.extend_from_slice(&c.smufl_version.major.to_le_bytes()); out.extend_from_slice(&c.smufl_version.major.to_le_bytes());
out.extend_from_slice(&c.smufl_version.minor.to_le_bytes()); out.extend_from_slice(&c.smufl_version.minor.to_le_bytes());
let font = c.font_id.0.as_bytes(); let font = c.font_id.0.as_bytes();
push_u64(out, font.len() as u64); push_len(out, font.len());
out.extend_from_slice(font); out.extend_from_slice(font);
match c.font_version { match c.font_version {
None => out.push(0), None => out.push(0),
@ -392,6 +399,34 @@ mod tests {
assert_ne!(a, moved.canonical_bytes()); assert_ne!(a, moved.canonical_bytes());
} }
#[test]
fn count_prefixes_are_u32_width_locked() {
// Schema major 1 unifies the resolved-layout length/count prefixes to
// u32 (Binary Format companion §"Schema Major 1"). This locks the byte
// shape so a revert to the old u64 prefixes fails: an empty layout
// encodes its four counts — pages, glyphs, strokes, engraving_decisions
// — as u32 zeros (16 bytes) right after the 32-byte ScoreVersion source,
// then the catalog. Under u64 that region would be 32 bytes, shifting the
// catalog and lengthening the output by 16.
let bytes = ir(vec![], vec![]).canonical_bytes();
let source_len = ScoreVersion::default().0.len();
assert_eq!(source_len, 32, "ScoreVersion source is 32 bytes");
// The first count prefix (pages) is a 4-byte u32 zero — not 8 bytes.
assert_eq!(&bytes[source_len..source_len + 4], &0u32.to_le_bytes());
// The four count prefixes occupy exactly 4 × 4 bytes; then the catalog,
// whose length we recompute independently (no magic number).
let catalog_len = {
let mut c = Vec::new();
encode_catalog(&mut c, &GlyphCatalogIdentity::default());
c.len()
};
assert_eq!(
bytes.len(),
source_len + 4 * 4 + catalog_len,
"four u32 count prefixes (16 bytes), not u64 (32 bytes)"
);
}
#[test] #[test]
fn canonical_bytes_capture_engraving_decisions_and_catalog() { fn canonical_bytes_capture_engraving_decisions_and_catalog() {
let base = ir(vec![glyph(1, 1.0)], vec![]); let base = ir(vec![glyph(1, 1.0)], vec![]);