LT-22702: Escape custom list range ids at the point they are written - #1064
Open
imnasnainaec wants to merge 3 commits into
Open
LT-22702: Escape custom list range ids at the point they are written#1064imnasnainaec wants to merge 3 commits into
imnasnainaec wants to merge 3 commits into
Conversation
A custom possibility list's name reaches two attributes: the range id in the .lift header and the range id in the .lift-ranges file. It was escaped once as element content when the exporter collected it, which is the wrong rule for an attribute and the wrong place to apply it. The header escaped it a second time, so a list named "Birds & Beasts" arrived as "Birds &amp; Beasts" and disagreed with its own ranges file. The ranges file did not escape it at all, so a name holding a quotation mark closed the id attribute early and left the document unparseable. Hold the collected names unescaped and escape at each write site, as the rest of the exporter does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NUnit Tests 1 files - 1 1 suites - 1 11m 45s ⏱️ - 2m 46s Results for commit c4b3bf2. ± Comparison against base commit 7f93348. This pull request removes 6 and adds 7 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1064 +/- ##
==========================================
+ Coverage 37.91% 38.04% +0.13%
==========================================
Files 1499 1499
Lines 350117 350109 -8
Branches 40233 40231 -2
==========================================
+ Hits 132747 133215 +468
+ Misses 188043 187610 -433
+ Partials 29327 29284 -43
🚀 New features to boost your workflow:
|
The doc comments on the two range-name maps and on the two new export tests described how the exporter used to escape range names, which the repository comment standard bans. State the current contract instead: range names are held unescaped, and each is escaped by the rules of the context it is written into. Also reset m_customListsGuids in the per-test setup. NUnit shares one fixture instance across the tests in a class, so the list outlived the cache whose lists it indexes and grew by two entries per test; the assertions that index it were correct only for whichever test ran first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fixture instance outlives each test's cache, so the id lists that VerifyExport indexes positionally kept entries from earlier caches; they matched only because a fresh cache reassigns custom-field flids in the same order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
imnasnainaec
marked this pull request as ready for review
August 14, 2026 14:14
5 tasks
papeh
approved these changes
Aug 14, 2026
| m_mapPartsOfSpeech.Clear(); | ||
| m_mapAcademicDomains.Clear(); | ||
| m_mapPublications.Clear(); | ||
| // NUnit shares one fixture instance across the tests, so these outlive the cache they index. |
Contributor
There was a problem hiding this comment.
should this comment go at the beginning of this method?
| /// <summary> | ||
| /// Maps a possibility list to the name of its LIFT range. Range names are held unescaped, so | ||
| /// that each is escaped by the rules of the context it is written into. | ||
| /// </summary> |
Contributor
There was a problem hiding this comment.
I'm not sure we need this comment. The field name is self-documenting, and the usage is documented by example.
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.
Fixes LT-22702.
Companion to #1063. The two diffs do not overlap.
Quick Summary
range/@idinthe
.liftheader, and therange/@idof the same range in the.lift-rangesfile.(
MakeSafeAndNormalizedXml), rather than at each write site with the attribute rules --which is both the wrong rule and the wrong place.
the rest of the exporter already does for every other range and range-element id.
Two defects fall out of escaping on collection:
list named
Birds & Beastsis emitted asBirds &amp; Beasts, while the ranges filekept a single escape.
element rules never escape
", so a list namedSo-called "words"closes the id attributeearly and leaves the
.lift-rangesunreadable by any conforming parser.The change
Collect raw. Four sites stop pre-escaping, so the two name maps hold what the user typed:
GetCustomListsAndReferencedListsGetCustomListsNotYetAddedAddPossListRefdByFieldMapCmPossibilityListGuidsToLiftRangeNamesThe
m_ListsGuidToRangeNamemap already held standard range names raw viaMapGuidToRange, sothis also stops it holding a mix of escaped and unescaped values.
Escape at the write site.
WritePossibilityListAsRangenow puts itsrangeIdthroughMakeSafeAndNormalizedAttribute, hoisted into a local so the normalizer runs once and bothbranches are visibly the same.
Leave the two already-correct write sites alone. The header
<range id href>and thecustom-field spec string need no change once their input is raw; they stop double-escaping on
their own.
Record the invariant. It is stated on the two map declarations, since "held unescaped" is
the kind of thing a later change would otherwise re-break.
Scope
Only custom and referenced possibility lists are affected.
WritePossibilityListAsRangehassix callers; the other five pass
RangeNamesconstants, where the helper is a no-op.Not addressed here.
WriteRangeRefsForListsReferencedByFieldswrites a header<range id href>for every collected list, whileWritePossibilityListAsRangereturns beforewriting anything for a list with no possibilities. So an empty custom list still yields a header
reference to a range that has no body in the ranges file. That is pre-existing and left alone;
the only effect here is that the dangling id now carries the corrected spelling.
Sweep: every other range id write already escapes and normalizes
I swept every
<range id=and<range-element id=write inLiftExporter.csto be sure thisis the whole of it.
WritePossibilityRangeElement-- covers semantic domains, status, locations, people,anthropology, translation tags, production restrictions and custom lists. Escapes and
normalizes both its range ids and its range-element ids.
-feature-value,-slot,-infl-class,-stem-name. Same.RangeNamesconstant.Effects worth knowing
This is not a normalization change. The collected names were already NFC-normalized on the
way in, and remain so --
MakeSafeAndNormalizedAttributenormalizes as well as escapes. No idchanges spelling as a result of this branch; only escaping changes, and only for names holding
&,<,>or". (#1063 is the branch that changes spelling.)FLEx's own importer is unaffected. A custom list's range falls through
ProcessRangeElement's switch todefault:, which handles only the-slot,-infl-class,-feature-valueand-stem-namesuffixes and otherwise does nothing but aDebug.WriteLine.FLEx does not import custom-list ranges at all, so there is no re-import behavior to check.
This matters only to third-party consumers of the files, and to what lands in a shared repo.
The
qaa-x-speccustom-field definition also changes.GetCustomFieldDefinitionappendsthe map value raw into
; range={0}, andGenerateCustomFieldSpecsescapes the whole specstring once at its write site. With the map now holding raw names, a custom list name containing
one of the four characters reaches that form single-escaped where it used to be double-escaped.
This is the same defect and the same correction as the header range ref, and it brings custom
lists into line with the standard list names, which
MapGuidToRangehas always stored raw. Itis called out separately because it touches the header field definitions, not just ranges.
LIFT Send/Receive sees a whole-record delete plus add.
rangeis keyed on@idby both theChorus element strategy and by
XmlMergeService's record splitting, and unlikerange-elementit has no guid to fall back on.
after upgrading rewrites that id once. Chorus models that as a delete plus an add of the whole
<range>record, taking every<range-element>inside it along -- a large single diff.change, which an un-upgraded client would keep undoing.
"has been committing an unparseable ranges file untilnow.
Testing
Two regression tests in
LiftExportTests, each covering one of the two defects. Both wereconfirmed to fail on
mainbefore the fix.LiftExportRanges_CustomListRangeIdWithQuoteIsEscapedForAnAttribute-- a custom list namedSo-called "words", asserting the ranges document parses and the id reads back as the namestored. Without the fix this fails with
XmlException: 'words' is an unexpected token. Expecting white space.LiftExportRanges_CustomListRangeIdWithAmpersandIsEscapedExactlyOnce-- a custom list namedBirds & Beasts, asserting the ranges id reads back as the name stored and that the.liftheader names the range the same way. Without the fix the header and the ranges file disagree.
Both drive the real exporter over a real custom list and custom field, through a shared
ExportWithCustomListhelper that runs the.liftpass and the ranges pass on one exporter --the ranges pass writes only the lists the
.liftpass collected..\test.ps1 -SkipNative -TestProject LexTextControlsTests -TestFilter "FullyQualifiedName~LiftExportTests":7 passed, 0 failed, including the pre-existing
LiftExport(which asserts the header rangeids for both a referenced and an unreferenced custom list) and
LiftExport_MultiParagraphWithAmpersandExports. The full suite and the native tests have notbeen run locally; leaving this as a draft for CI.
Test fixture resets
The fixture also now resets its five shared custom-field collections in
[SetUp], alongside themaps it already reset there. NUnit shares one fixture instance across a class, so each of them
outlived the cache it indexes and grew by several entries per test, while
VerifyExportandVerifyExportRangesindex them positionally.Why each of the five needed resetting
m_customListsGuidshad to be reset for the new tests to pass: guids are minted per object, so[0]and[1]named lists in a cache that had already been disposed.The four flid lists (
m_customFieldEntryIds,m_customFieldSenseIds,m_customFieldAllomorphsIds,m_customFieldExampleSentencesIds) were still passing, but onlybecause
AddCustomFieldallocates fromclid * 1000 + 500upward per metadata cache, so a freshcache plus an unchanged
[SetUp]order reassigns the same flids and the stale entries happenedto hold the values the current cache would assign. Resetting them too retires that unwritten
invariant, which a reordered or added custom field would otherwise break into a confusing
failure.
CI-ready checklist
.github/commit-guidelines.md.git log --checkandgit diff --checkare clean).This change is