feat: complete EMF MVP — 100% byte-identical round-trip, full toolchain - #2
Merged
Conversation
Correctness fixes (drove byte-identical round-trip from 74% to 100%): - Rectangle/Ellipse: drop rclBounds (per MS-EMF 2.3.5 only rclBox is present) - RoundRect: same fix (rclBox + szlCorner, no rclBounds) - Arc/ArcTo/Chord/Pie: same fix (rclBox + ptlStart + ptlEnd) - FillPath/StrokePath/StrokeAndFillPath: inherit from WithBounds so the spec-required rclBounds is parsed - SelectClipPath: WithBounds + regionMode per spec - Header: make cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometers conditional on n_size so 88/100/108-byte variants all parse Added 56 missing wire types (TODO 09 substantially complete): - SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect, IntersectClipRect, ExtSelectClipRgn, ScaleViewportExtEx, ScaleWindowExtEx, SetBrushOrgEx, SetMapperFlags, RealizePalette, SelectPalette, CreatePalette, ResizePalette, SetPaletteEntries, ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, BitBlt, StretchBlt, MaskBlt, PlgBlt, SetDIBitsToDevice, AlphaBlend, TransparentBlt, SetLayout, SetIcmMode, CreateMonoBrush, CreateDibPatternBrushPt, GradientFill, SmallTextOut, DrawEscape, ExtEscape, NamedEscape, ColorCorrectPalette, SetIcmProfileA/W, ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW, CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace, GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat, SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc Parser improvements: - Variable-array sanity check: rejects records whose declared point count exceeds nSize before bindata can hang on giant allocations - Post-EOF trailing bytes captured on Metafile#trailing for byte-faithful round-trip on files that append metadata after EMR_EOF Verification: 186/186 EMF fixtures + 21/21 EMF-EA + simple/image1.emf all round-trip byte-identically. All 21 emf-corrupted/ files parse without crashing or hanging.
Correctness fixes (drove byte-identical round-trip from 74% to 100%): - Rectangle/Ellipse: drop rclBounds (per MS-EMF 2.3.5 only rclBox is present) - RoundRect: same fix (rclBox + szlCorner, no rclBounds) - Arc/ArcTo/Chord/Pie: same fix (rclBox + ptlStart + ptlEnd) - FillPath/StrokePath/StrokeAndFillPath: inherit from WithBounds so the spec-required rclBounds is parsed - SelectClipPath: WithBounds + regionMode per spec - Header: make cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometers conditional on n_size so 88/100/108-byte variants all parse Added 57 missing wire types (TODO 09 substantially complete): - SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect, IntersectClipRect, ExtSelectClipRgn, ScaleViewportExtEx, ScaleWindowExtEx, SetBrushOrgEx, SetMapperFlags, RealizePalette, SelectPalette, CreatePalette, ResizePalette, SetPaletteEntries, ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, BitBlt, StretchBlt, MaskBlt, PlgBlt, SetDIBitsToDevice, AlphaBlend, TransparentBlt, SetLayout, SetIcmMode, CreateMonoBrush, CreateDibPatternBrushPt, GradientFill, SmallTextOut, DrawEscape, ExtEscape, NamedEscape, ColorCorrectPalette, SetIcmProfileA/W, ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW, CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace, GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat, SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc Parser improvements: - Variable-array sanity check: rejects records whose declared point count exceeds nSize before bindata can hang on giant allocations - Post-EOF trailing bytes captured on Metafile#trailing for byte-faithful round-trip on files that append metadata after EMR_EOF Verification: 186/186 EMF fixtures + 21/21 EMF-EA + simple/image1.emf all round-trip byte-identically. All 21 emf-corrupted/ files parse without crashing or hanging.
TODO 16 (round-trip harness): - spec/emf/round_trip_spec.rb walks every non-corrupted fixture (186 + 21 + 1 = 208) and asserts byte-identical parse/serialize. TODO 17 (corrupted-resilience): - spec/emf/corrupted_resilience_spec.rb walks the 21 emf-corrupted/ corpus with a 5-second timeout. Asserts each file either parses (possibly with errors) or raises Emf::FormatError. TODO 18 (visitors): - Emf::Visitors::Stats: record-class histogram with visit_emr_wire_record override for the catch-all adapter. - Emf::Visitors::Dump: human-readable record-by-record dump. TODO 19 (CLI): - exe/emf now has real subcommands: version, info, dump, validate, stats, round-trip, help. Exit codes follow Unix convention. - spec/emf/cli_spec.rb exercises each via Open3. Also: SetTextJustification wire class added.
TODO 20 (documentation): - README.adoc: full rewrite with synopsis, CLI usage, architecture, constraints, roadmap. - docs/architecture.adoc: three-layer design + MECE table. - docs/format_notes.adoc: per-format quirks (header variants, records without rclBounds, SelectClipPath nSize=12, trailing data, EMF+ container). - CONTRIBUTING.adoc: branch policy, code style, constraints, how-to- add-a-record. - CHANGELOG.adoc: Keep-a-Changelog format starting at 0.1.0. - TODO.impl/PROGRESS.md: rolled up to reflect new state. TODO 21 (CI): - .github/workflows/test.yml: matrix on Ruby 3.1/3.2/3.3/3.4 across Ubuntu and macOS. Runs rspec, rubocop, rake build. - .github/workflows/release.yml: tag-triggered (v*) release to rubygems.org + GitHub release. Never runs on main commits; never auto-tags. .rubocop.yml: bumped limits to accommodate the larger records.rb registry and the CLI's info subcommand.
ronaldtse
added a commit
that referenced
this pull request
Jul 24, 2026
…input The variable_array_sane? check (added in PR #2) covered POLYGON/POLYLINE/ POLYBEZIER/POLYBEZIERTO/POLYLINETO + their 16-bit variants + POLYDRAW/16. This commit extends the check to multi-array records: - POLYPOLYGON, POLYPOLYLINE, POLYPOLYGON16, POLYPOLYLINE16: each has TWO variable arrays (aPolyCounts and aptl/apts). Both counts are now validated against nSize before bindata tries to allocate. Adds MAX_INPUT_BYTES = 200 MB guard at the parser entry point. Refuses inputs larger than 200 MB before any allocation. The 5.7 MB EMF fixture in the corpus is large; 200 MB is a generous safety margin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: complete EMF MVP — 100% byte-identical round-trip, full toolchain
Builds on PR #1. Drives the EMF corpus from 74% to 100% byte-identical
round-trip, completes the wire catalog, and lands the visitors, CLI,
docs, and CI workflows.
What lands
Wire layer correctness fixes (TODO 09 follow-up)
The biggest single win came from correcting layouts per MS-EMF 2.3.5:
rclBounds— these only haverclBox(andszlCornerfor RoundRect).rclBounds; layout isrclBox + ptlStart + ptlEnd.WithBounds(specrequires rclBounds).
WithBounds + regionMode.cbPixelFormat/offPixelFormat/bOpenGL/szlMicrometersare conditional on
n_size, so 88/100/108-byte variants all parse.Plus 57 missing wire types (full catalog at last):
SetPixelV, SetMetArgn, OffsetClipRgn, ExcludeClipRect, IntersectClipRect,
ExtSelectClipRgn, ScaleViewportExtEx, ScaleWindowExtEx, SetBrushOrgEx,
SetMapperFlags, RealizePalette, SelectPalette, CreatePalette,
ResizePalette, SetPaletteEntries, ExtFloodFill, FillRgn, FrameRgn,
InvertRgn, PaintRgn, BitBlt, StretchBlt, MaskBlt, PlgBlt,
SetDIBitsToDevice, AlphaBlend, TransparentBlt, SetLayout, SetIcmMode,
CreateMonoBrush, CreateDibPatternBrushPt, GradientFill, SmallTextOut,
DrawEscape, ExtEscape, NamedEscape, ColorCorrectPalette,
SetIcmProfileA/W, ForceUfiMapping, SetLinkedUfis, ColorMatchToTargetW,
CreateColorspaceW/Colorspace, SetColorspace, DeleteColorspace,
GlsRecord, GlsBoundedRecord, PolyTextOutA/W, PixelFormat,
SetTextJustification, PolyDraw/16, SetArcDirection, AngleArc.
Parser hardening (TODO 11 follow-up)
count exceeds
nSizebefore bindata can hang on giant allocations.This was the cause of one
emf-corrupted/file timing out.Metafile#trailing. Somewriters append metadata after
EMR_EOF; preserving it is necessaryfor byte-faithful round-trip.
TODO 16 — Round-trip harness
spec/emf/round_trip_spec.rbwalks every non-corrupted fixture(186 + 21 + 1 = 208 files) and asserts byte-identical
parse → serialize.
TODO 17 — Corrupted-resilience spec
spec/emf/corrupted_resilience_spec.rbwalks the 21-fileemf-corrupted/corpus with a 5-second timeout. Asserts each fileeither parses (possibly with errors) or raises
Emf::FormatError.TODO 18 — Visitors
Emf::Visitors::Stats— record-class histogram.Emf::Visitors::Dump— human-readable record-by-record dump.Both override
visit_emr_wire_recordso the catch-all adapter producesinformative output.
TODO 19 — CLI
exe/emfnow has real subcommands:version,helpinfo FILE— header summary, record counts, device dimsdump FILE— record-by-record dumpvalidate FILE— exit 0 ok, 1 invalidstats FILE— record-type histogramround-trip FILE [--out PATH]— re-serialize, compare or writespec/emf/cli_spec.rbexercises each via Open3.TODO 20 — Documentation
README.adoc— full rewrite with synopsis, CLI usage, architecture,constraints, roadmap.
docs/architecture.adoc— three-layer design + MECE table.docs/format_notes.adoc— per-format quirks.CONTRIBUTING.adoc— branch policy, code style, how to add a record.CHANGELOG.adoc— Keep-a-Changelog format starting at 0.1.0.TODO 21 — CI
.github/workflows/test.yml— Ruby 3.1–3.4 × Ubuntu/macOS matrix..github/workflows/release.yml— tag-triggered (v*) release torubygems.org + GitHub release. Never runs on main commits.
Verification
bundle exec rake buildproducespkg/emf-0.1.0.gem.emf/, 21/21 inemf-ea/, 1/1 insimple/.emf-corrupted/files parse without crashing or hanging.What's still deferred