Skip to content

ixml parser: character data that no tag follows - #1233

Merged
larshp merged 1 commit into
open-abap:mainfrom
abap2UI5:claude/nifty-meitner-ema3y8-text-no-markup
Sep 15, 2026
Merged

larshp merged 1 commit into
open-abap:mainfrom
abap2UI5:claude/nifty-meitner-ema3y8-text-no-markup

Conversation

@oblomov-dev

Copy link
Copy Markdown
Contributor

What is wrong

The value branch of the parse loop takes the offset of the next < without
looking at whether there was one, src/ixml/cl_ixml.clas.locals_imp.abap:

* value
FIND FIRST OCCURRENCE OF '<' IN lv_xml MATCH OFFSET lv_offset.
lv_value = lv_xml(lv_offset).

sy-subrc is ignored, so when the document ends in character data the offset
of the previous match still stands. Two ways that goes wrong:

parse( |hello| ).
" no match had been made yet, so the offset is 0: the value is empty, lv_xml
" does not advance, and the WHILE loop runs forever. It ends as an
" out-of-memory abort of the process, not as an error the caller can see
parse( |<root/>trail| ).
parse( |<root><item>A| ).
" the offset of the last tag stands and reads past the end of the rest

None of the three is well-formed XML. But a truncated response, a file that
turns out to be HTML, or an error page where XML was expected all reach a
parser as they stand — and of all the ways to say "this is not a document",
hanging until the process dies is the worst one.

The change

FIND FIRST OCCURRENCE OF '<' IN lv_xml MATCH OFFSET lv_offset.
IF sy-subrc <> 0.
  lv_offset = strlen( lv_xml ).
ENDIF.

When no tag follows, the rest of the document is the value. The loop advances
in every case and ends, and what was read before stays readable: <root/>trail
keeps its root element, <root><item>A keeps the A.

A well-formed document is untouched — its character data always ends at a <,
so the new branch is never reached.

Tests

  • parse_text_without_markuphello is one text node, and the parse
    returns (this is the test that used to hang)
  • parse_text_after_root<root>…</root>tail still has its root element
  • parse_unclosed_tag<root><item>A keeps the value that was read

npm test is green: abaplint: 0 issue(s) found, 735 file(s) analyzed, and
the full unit run passes.

Note

This says nothing about reporting the malformed document — parse( ) still
returns 0 and if_ixml_parser~num_errors is still a todo. That is its own
change; this one is about the loop that does not terminate.

The value branch took the offset of "<" without looking at whether there was
one. When the document ends in character data there is none, and sy-subrc is
ignored, so the offset of the PREVIOUS match still stood:

* `parse( |hello| )` - no match had been made yet, so the offset was 0, the
  value was empty, lv_xml did not advance and the loop ran forever. The
  process ends in an out-of-memory abort, not in an error the caller sees.
* `parse( |<root/>trail| )` and `parse( |<root><item>A| )` - the offset of the
  last tag stood, which reads past the end of the rest.

None of the three is well-formed XML, but a truncated response or a file that
is not XML at all reaches a parser as it stands, and a hang is the worst of
the ways to say so.

Now the rest of the document is the value when no "<" follows, so the loop
advances in every case and ends.

Tests: a document of character data only, character data behind the root
element, and a document cut off inside an element.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKSWLpid4QLV6XWAoPixbb
@larshp
larshp merged commit 89a196e into open-abap:main Sep 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants