From d4c5f0e77b63b0013f2ee0d2a899494068a23f32 Mon Sep 17 00:00:00 2001 From: olabusayoT <50379531+olabusayoT@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:59:41 -0400 Subject: [PATCH] Wait for a computable target length before unparsing truncating strings A string with dfdl:truncateSpecifiedLengthString whose dfdl:length is an expression reading an outputValueCalc element failed with an unretried InfosetNoDataException. The truncating string unparsers evaluate the target length inline, while the outputValueCalc was still blocked on a following sibling, so the error escaped the suspension machinery instead of being retried once the outputValueCalc could resolve. Make the truncating string unparsers SuspendableUnparsers. Their new StringTruncationSuspendableOperation tests that the target length can be evaluated and then runs the string unparse as its continuation. truncatedString_2 now unparses. truncatedString_1 and _4 depend on dfdl:valueLength and remain true deadlocks; their expected diagnostic is now the circular deadlock error. DAFFODIL-1598 --- .../runtime1/StringLengthUnparsers.scala | 51 +++++++++++++++++-- .../general/testUnparserGeneral.tdml | 10 ++-- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/StringLengthUnparsers.scala b/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/StringLengthUnparsers.scala index 49e1b31afc..0b2be365bc 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/StringLengthUnparsers.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/StringLengthUnparsers.scala @@ -26,6 +26,7 @@ import org.apache.daffodil.runtime1.processors.CharsetEv import org.apache.daffodil.runtime1.processors.ElementRuntimeData import org.apache.daffodil.runtime1.processors.Evaluatable import org.apache.daffodil.runtime1.processors.LengthEv +import org.apache.daffodil.runtime1.processors.SuspendableOperation import org.apache.daffodil.runtime1.processors.TextTruncationType import org.apache.daffodil.runtime1.processors.UnparseTargetLengthInBitsEv import org.apache.daffodil.runtime1.processors.unparsers.* @@ -70,10 +71,26 @@ class StringNoTruncateUnparser(erd: ElementRuntimeData) sealed abstract class StringSpecifiedLengthUnparserTruncateBase( stringTruncationType: TextTruncationType.Type, erd: ElementRuntimeData -) extends StringSpecifiedLengthUnparserBase(erd) { +) extends StringSpecifiedLengthUnparserBase(erd) + with SuspendableUnparser { Assert.usage(stringTruncationType ne TextTruncationType.None) + /** + * The target length the string is truncated to. It can depend on an + * outputValueCalc element that has not been computed yet. + */ + protected[runtime1] def targetLengthEv: Evaluatable[AnyRef] + + /** + * Reads the target length without retrying, so it must only be called + * once the target length can be evaluated. + */ + protected[runtime1] def unparseString(state: UState): Unit + + override protected def suspendableOperation = + new StringTruncationSuspendableOperation(erd, this) + /** * We only truncate strings, and only if textStringJustification is left or * right, and only if truncateSpecifiedLengthString is yes. @@ -122,6 +139,8 @@ class StringMaybeTruncateBitsUnparser( override val runtimeDependencies = Array(targetLengthInBitsEv, charsetEv) + override protected[runtime1] def targetLengthEv = targetLengthInBitsEv + private def getLengthInBits(str: String, state: UState): (Long, Long) = { val cs = charsetEv.evaluate(state) val mfw = cs.maybeFixedWidth @@ -171,7 +190,7 @@ class StringMaybeTruncateBitsUnparser( res } - override def unparse(state: UState): Unit = { + override protected[runtime1] def unparseString(state: UState): Unit = { // // We have to stage the bits of the value just so as to be able to count them @@ -259,7 +278,9 @@ class StringMaybeTruncateCharactersUnparser( override val runtimeDependencies = Array(lengthInCharactersEv) - override def unparse(state: UState): Unit = { + override protected[runtime1] def targetLengthEv = lengthInCharactersEv + + override protected[runtime1] def unparseString(state: UState): Unit = { val dos = state.getDataOutputStream val valueString = contentString(state) val targetLengthInCharacters = @@ -285,3 +306,27 @@ class StringMaybeTruncateCharactersUnparser( Assert.invariant(nCharsWritten == valueToWrite.length) } } + +/** + * Suspends until the target length can be evaluated, then unparses the + * string, because the truncating string unparsers read the target length + * without retrying. + */ +class StringTruncationSuspendableOperation( + override val rd: ElementRuntimeData, + unparser: StringSpecifiedLengthUnparserTruncateBase +) extends SuspendableOperation { + + override def toString = "string truncation for " + rd.diagnosticDebugName + + // Evaluating throws a RetryableException while the length is unavailable, + // which blocks this operation. + override protected def test(ustate: UState): Boolean = { + unparser.targetLengthEv.evaluate(ustate) + true + } + + override protected def continuation(ustate: UState): Unit = { + unparser.unparseString(ustate) + } +} diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section00/general/testUnparserGeneral.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section00/general/testUnparserGeneral.tdml index 549cef2799..4940801eb0 100644 --- a/daffodil-test/src/test/resources/org/apache/daffodil/section00/general/testUnparserGeneral.tdml +++ b/daffodil-test/src/test/resources/org/apache/daffodil/section00/general/testUnparserGeneral.tdml @@ -1023,8 +1023,7 @@ - not have a value - circular dependency + circularly deadlocked @@ -1055,10 +1054,7 @@ - - not have a value - circular dependency - + 5ABCDE @@ -1113,7 +1109,7 @@ - Value length unknown + circularly deadlocked