Reject ambiguous mappings in PiecewiseLinearMap::new - #2083
Conversation
new() sorted the (from, to) pairs into parallel Vecs without checking for duplicate 'from' values. When two mappings shared a 'from' but disagreed on 'to', both were stored and map() returned whichever sorted first, silently discarding the other mapping. new() now returns a Result. Exact duplicate mappings are collapsed, since repeating a mapping is redundant rather than ambiguous, and mappings that share a 'from' but disagree on 'to' are rejected. Duplicate 'to' values stay legal, so reverse() builds its map without the check: reversing a many-to-one map is exactly how duplicate 'from' values legitimately arise, and map() resolves those to the first match on purpose. CoordConverter::new was already fallible, so both call sites just propagate. Fixes googlefonts#937
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
anthrotype
left a comment
There was a problem hiding this comment.
The core approach looks right: exact duplicates are harmlessly collapsed, conflicting input coordinates become a well-scoped error, and reverse() preserves the intentional many-to-one semantics from #1528. Apart from the default_idx cleanup, I have no code concerns.
It indexed the caller's mappings, which user_to_design no longer matches once sorted and deduped, and nothing read it back. Keep it as a constructor parameter to pick design_default. unmapped_coords_get_deduped asserted nothing else, so it now checks the dedup count and that the default still normalizes to 0.
anthrotype
left a comment
There was a problem hiding this comment.
Thanks. mind also signing the CLA?
All done. Let me know if you need anything else. |
Fixes #937.
The bug
PiecewiseLinearMap::newsorted the(from, to)pairs and unzipped them into parallelVecs with no duplicate detection and no error path. Two mappings sharing afrombut disagreeing ontowere both stored, andmap()then resolved an exact hit viapartition_pointand returned whichever sorted first, silently dropping the other mapping.On
main(ee249ef):The fix
newnow returnsResult, with aDuplicateMapInputvariant on the existingfontdrasil::error::Error.CoordConverter::newwas already fallible, so both call sites just propagate with?and nothing outsidefontdrasilchanges. (PiecewiseLinearMapis crate-private, so this isn't a public API break;Errordoes gain a variant.)Two judgement calls worth your attention:
Duplicate
fromis rejected, duplicatetois not. Per @anthrotype in #1528, "duplicate keys are bad, but duplicate values are fine".reverse()therefore builds its map without the check, since reversing a many-to-one map is exactly how duplicatefromvalues legitimately arise, andmap()resolves those to the first match on purpose (the ufo2ft#978 behaviour).Exact duplicate mappings are collapsed rather than rejected. Repeating a mapping identically is redundant, not ambiguous, and nothing gets discarded. This is not hypothetical:
fea-rs'ssimple_axisbuilds[(min, min), (default, default), (max, max)], so any axis withmin == defaultrepeats a mapping, and rejecting those broke thefea-rssuite.CoordConverter::unmappedalready handles the same case withmappings.dedup(), so this follows the idiom that was already in the file. Happy to make it strict instead if you'd rather callers pre-dedup.Things to weigh
dict, so a conflicting duplicate<map>collapses last-wins and never errors; fontc previously took first-after-sort. A source in the wild with a genuinely conflicting duplicate will now fail a build that used to succeed (quietly disagreeing with fontmake). That's what let PiecewiseLinearMap::new return error if input mappings contain duplicate keys #937 asks for, but a crater run is the real gate here, not the unit tests. Reachable from.designspaceand fontra; the glyphs path keysaxis_mappingsby user value, so duplicates can't survive parsing.avargets slightly more spec-correct.to_segment_mapis the only production consumer ofCoordConverter::iter(). For a source with an exactly-repeated<map>, it previously emitted aSegmentMapswith a duplicatefromCoordinate, which the spec forbids.fvaris unaffected.CoordConverter::default_idxindexes the caller's mappings, notuser_to_design, which has always been sorted and is now also deduped. Nothing in production reads the field, so I only documented it rather than recomputing it. Say the word if you'd like it fixed properly, here or separately.Testing
cargo fmt,cargo clippy --all-features --all-targets -D warnings, andcargo doc -D warningsare clean, and the full suite passes: fontdrasil 63, fontir 180, glyphs-reader 278, glyphs2fontir 89, ufo2fontir 80, fontra2fontir 20, fontbe 132, fontc 202. I couldn't run thefea-rsttx tests orfontc_craterlocally (nottxon PATH, nocmakefortidy-sys); both fail identically on a clean checkout here, andfontc_craterhas nofontdrasildependency.I mutation-tested the new tests rather than trusting them: dropping the
dedup(), deduping byfromalone (which would reintroduce the original silent drop), and comparingtoinstead offromare each caught by at least two tests.I use AI tooling to help with my open source work. This patch was written and tested with that assistance, and I've reviewed and verified all of it myself.