Skip to content

A key normalizing to a single character is containment-matched against unrelated properties, reattributing and losing values #128

Description

@matt-edmondson

Filed per the recommendation on #113, which noted this and deliberately left it alone:

The single-character looseness noted and deliberately left alone (meta_x_field → x → next) is the same class of bug as the empty-string one, one step less severe. Worth its own issue rather than leaving it in a comment — it is main's behaviour today and will outlive this thread.

What's wrong

PropertyNameNormalizer.Normalize strips one decorative prefix and one decorative suffix, so a key like meta_x_field normalizes to the single character x. Both matching call sites then test containment in both directions:

  • NameStandardizer.FindStandardPropertyMatch — !normalizedKey.Contains(normalizedStandard) && !normalizedStandard.Contains(normalizedKey)
  • PropertyMerger.FindBasicCanonicalName — the same pair, against the other keys present
  • PropertyMerger.CalculateWordMatchScore — word1.Contains(word2) || word2.Contains(word1), scoring +1

A one-character fragment is contained in any candidate that happens to use that letter, so it is admitted on an incidental letter rather than a shared word. Both classes already guard the degenerate Length == 0 case, with a comment explaining exactly this failure mode; a length of 1 falls through that guard.

Measured on main (6ed61c5), .NET SDK 10.0.401, Linux

NameStandardizer.StandardizePropertyNames silently renames the key:

key normalizes to result
meta_x_field x next
page_a_value a area
meta_e_field e editor
custom_s_data s slug
page_x_text x next
page_t_data t toc
meta_z_info z preserved — no standard property contains "z"

PropertyMerger.MergeSimilarProperties with FrontmatterMergeStrategy.Maximum goes further and drops a property entirely:

IN  [meta_x_field=A, next=B, text=C]
OUT [meta_x_field=A, next=B]            // text=C is gone

The merger also cross-references mutually, because containment is bidirectional: meta_x_field → next (as "next" contains "x") while next → meta_x_field at the same time.

Why it matters

This is the failure mode #113 and #124 were explicitly trying to close — a value silently attributed to an unrelated property, in a library whose job is round-tripping frontmatter — reached by a fragment of length 1 instead of length 0. The merger case is worse than reattribution: the value is not moved, it is lost.

Suggested fix

Extend the existing degenerate-fragment rule from "empty" to "shorter than two characters", for the containment paths only. Exact matching should keep working, so a genuine one-character key still merges with another key that normalizes to the same thing.

The floor is safe at 2: the shortest names anywhere in StandardOrder.PropertyNames or PropertyMappings are by, tag and url, and no standard property or mapping name normalizes to fewer than two characters — so nothing legitimate is matched by containment on a single character today.

Acceptance criteria

  • A key whose normalized form is a single character is preserved by StandardizePropertyNames rather than renamed to an unrelated standard property.
  • MergeSimilarProperties neither merges such a key into an unrelated one nor drops any other property because of it.
  • Exact matching on a one-character normalized form still works.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions