stevedlawrence commented on code in PR #1652:
URL: https://github.com/apache/daffodil/pull/1652#discussion_r3372987445
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -254,45 +254,55 @@ trait ElementBaseGrammarMixin
final lazy val prefixedLengthBody = prefixedLengthElementDecl.parsedValue
def checkEndOfParentRestrictionsOnCurrentElement(optParentELU:
Option[LengthUnits]): Unit = {
+ Assert.invariant(optParentELU.isDefined)
Review Comment:
If we are asserting that optParentELU is defined, can we make this not an
Option and force callers to do any necessary checks?
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -648,20 +658,20 @@ trait ElementBaseGrammarMixin
}
case LengthKind.Pattern =>
schemaDefinitionError("Binary data elements cannot have
lengthKind='pattern'.")
+ case LengthKind.EndOfParent if optionBinaryCalendarRep.isDefined =>
Review Comment:
I think it's possible to have `dfdl:binaryCalendarRep` defined but not be
applicable and so it would normally be ignored, e.g.
```xml
<element name="foo"
type="xs:int"
dfdl:lengthKind="endOfParent"
dfdl:binaryNumberRep="packed"
dfdl:binaryCalendarRep="binaryMilliseconds" ... />
```
The code will currently error because `binaryCalendarRep` is defined even
though it won't be used because the type is not a date/time/dateTime. I think
this wants to have logic similar to the delimited case where it checks the
primType and does different checks for calendar types vs non calendar types.
Same issue with optionBinaryNumberRep below
In fact, I think the EOP checks are exactly the same as the delimited
checks, so can we do something like this with a tweak to SDE to include the
right type in the diagnostic:
```scala
case LengthKind.Delimited | LengthKind.EndOfParent => {
...
SDE(
"lengthKind='%s' only supported for packed binary formats.",
repElement.lengthKind.toString
)
...
}
```
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -281,24 +281,25 @@ trait ElementBaseGrammarMixin
|| (currentElement.representation == Representation.Text)
|| (currentElement.primType eq PrimType.HexBinary)
|| (currentElement.representation == Representation.Binary
- && ((currentElement.binaryNumberRep match {
- case BinaryNumberRep.Packed => true
- case BinaryNumberRep.Bcd => true
- case BinaryNumberRep.Ibm4690Packed => true
+ && ((
+ currentElement.binaryNumberRep,
+ currentElement.optionBinaryCalendarRep
+ ) match {
+ case (
+ BinaryNumberRep.Packed | BinaryNumberRep.Bcd |
BinaryNumberRep.Ibm4690Packed,
+ _
+ ) =>
+ true
+ case (
+ _,
+ Some(
+ BinaryCalendarRep.Packed | BinaryCalendarRep.Bcd |
+ BinaryCalendarRep.Ibm4690Packed
+ )
+ ) =>
+ true
Review Comment:
scala-fmt makes this impossible to make sense of. Suggest we revert back to
the old way
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -1341,18 +1451,29 @@ trait ElementBaseGrammarMixin
// non-explicit lengthKind
val body = bodyArg
+ val eopSimpleTypeElementThatNeedsBitLimit =
+ (isSimpleType && lengthKind == LengthKind.EndOfParent)
+ && !this.isInstanceOf[Root]
Review Comment:
Why do do we need to consider simple types and root elements? It feels like
the only thing that determines if we need the `SpecifiedLengthOfOnParentParser`
is if the `body` parser expects something else to set the parents length or if
it figures it out itself. The only things that do that are hexBinary and packed
binary parsers, so everything else needs it.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -1341,18 +1451,29 @@ trait ElementBaseGrammarMixin
// non-explicit lengthKind
val body = bodyArg
+ val eopSimpleTypeElementThatNeedsBitLimit =
+ (isSimpleType && lengthKind == LengthKind.EndOfParent)
+ && !this.isInstanceOf[Root]
+ && impliedRepresentation != Representation.Text
+ && !isNillable
+ && primType != PrimType.HexBinary
// there are essentially two categories of processors that read/write data
input/output
- // stream: those that calculate lengths themselves and those that expect
another
- // processor to calculate the length and set the bit limit which this
processor will use as
- // the length. The following determines if this element requires another
processor to
- // calculate and set the bit limit, and if so adds the appropriate grammar
to do that
- val bodyRequiresSpecifiedLengthBitLimit = lengthKind !=
LengthKind.Delimited && (
- isSimpleType && impliedRepresentation == Representation.Text ||
- isSimpleType && isNillable ||
- isComplexType && lengthKind != LengthKind.Implicit ||
- lengthKind == LengthKind.Prefixed ||
- isSimpleType && primType == PrimType.HexBinary && lengthKind ==
LengthKind.Pattern
- )
+ // stream: those that calculate lengths themselves (ex: binary numeric
parsers) and those
+ // that expect another processor to calculate the length and set the bit
limit which
+ // this processor will use as the length (such as text parsers). The
following determines
+ // if this element requires another processor to calculate and set the bit
limit, and if so
+ // adds the appropriate grammar to do that
+ val bodyRequiresSpecifiedLengthBitLimit = lengthKind !=
LengthKind.Delimited
+ // Note for non-root EndOfParent simple types, we don't wish to duplicate
the length
+ // calculation efforts unless we know it needs the bit limit set by a
parent
+ && !eopSimpleTypeElementThatNeedsBitLimit
+ && (
+ isSimpleType && impliedRepresentation == Representation.Text ||
+ isSimpleType && isNillable ||
+ isComplexType && lengthKind != LengthKind.Implicit ||
+ lengthKind == LengthKind.Prefixed ||
+ isSimpleType && primType == PrimType.HexBinary && lengthKind ==
LengthKind.Pattern
+ )
Review Comment:
I'm finding it hard to make sense of all of these conditions and how they
combine with the new eop boolean. Would it simplify anything to have separate
match cases based on lengthKind, e.g.
```scala
val bodyRequiresSpecifiedLengthBitLimit = lengthKind match {
case LengthKind.Delimited => false
case LengthKind.EndOfParent => {
// is string or nillable or complex
}
case _ =>
// existing logic
}
```
This way we don't need to consider how EOP interacts with the existing
logic, which was all kindof designed without EOP in mind.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/TermGrammarMixin.scala:
##########
@@ -84,4 +95,149 @@ trait TermGrammarMixin extends AlignedMixin with
BitOrderMixin with TermRuntime1
MandatoryTextAlignment(this, knownEncodingAlignmentInBits, true)
}
+ def optEffectiveLengthUnits(optLastNonEOPELU: Option[LengthUnits]):
Option[LengthUnits] = {
+ this match {
+ case e: ElementBase =>
+ e.lengthKind match {
+ case LengthKind.EndOfParent => optLastNonEOPELU
+ case LengthKind.Explicit | LengthKind.Prefixed => Some(e.lengthUnits)
+ case LengthKind.Pattern => Some(LengthUnits.Characters)
+ case LengthKind.Implicit | LengthKind.Delimited =>
+ None // invalid parent; SDE fires separately
+ }
+ case c: ChoiceTermBase if c.choiceLengthKind ==
ChoiceLengthKind.Explicit =>
+ Some(LengthUnits.Bytes)
+ // Sequences are transparent — the ELU is inherited from the nearest
enclosing box.
+ case _: SequenceTermBase => optLastNonEOPELU
+ case _: ChoiceTermBase => None // implicit-length choice; SDE fires
separately
+ }
+ }
+
+ final lazy val childrenEndOfParent: Seq[ElementBase] =
LV(Symbol("childrenEndOfParent")) {
+ val gms = termChildren
+ val chls = gms.flatMap {
+ case eb: ElementBase if eb.lengthKind == LengthKind.EndOfParent =>
Seq(eb)
+ case eb: ElementBase => Nil
+ case c: ChoiceTermBase => Nil
+ case mg: ModelGroup => mg.childrenEndOfParent
+ }
+ chls
+ }.value
+
+ def checkEndOfParentRestrictions(lastNonEOPELU: Option[LengthUnits]):
Boolean = {
+ val term = this
+ lazy val eopChildren = this.childrenEndOfParent
+ lazy val optParentELU = term.optEffectiveLengthUnits(lastNonEOPELU)
+ // checks
+ term match {
+ case rootElem: Root if rootElem.lengthKind == LengthKind.EndOfParent => {
+
rootElem.checkEndOfParentRestrictionsOnCurrentElement(Some(LengthUnits.Characters))
+ eopChildren.foreach { child =>
+ child.checkEndOfParentRestrictionsOnCurrentElement(lastNonEOPELU)
+ }
+ }
+ case e: ElementBase if eopChildren.nonEmpty => {
+ eopChildren.foreach { child =>
+ child.schemaDefinitionWhen(
+ e.lengthKind == LengthKind.Implicit || e.lengthKind ==
LengthKind.Delimited,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but its
parent is an element with dfdl:lengthKind 'implicit' or 'delimited'."
+ )
+ child.checkEndOfParentRestrictionsOnCurrentElement(optParentELU)
+ }
+ }
+ case s: SequenceTermBase if eopChildren.nonEmpty => {
+ eopChildren.foreach { child =>
+ child.schemaDefinitionWhen(
+ s.separatorPosition == SeparatorPosition.Postfix,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a sequence with dfdl:separatorPosition defined as 'postfix'."
+ )
+ child.schemaDefinitionWhen(
+ s.sequenceKind != SequenceKind.Ordered,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a sequence with dfdl:sequenceKind defined as 'unordered'."
+ )
+ child.schemaDefinitionWhen(
+ s.hasTerminator,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a sequence with a dfdl:terminator."
+ )
+ child.schemaDefinitionWhen(
+ s.realElementChildren.exists(e => e.floating == YesNo.Yes),
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a sequence with elements defining dfdl:floating='yes'."
+ )
+ child.schemaDefinitionWhen(
+ s.trailingSkip != 0,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a sequence with a non-zero dfdl:trailingSkip."
+ )
+ }
+
+ }
+ case c: ChoiceTermBase if eopChildren.nonEmpty => {
+ // TODO: The DFDL spec (12.3.6) explicitly mentions that an
EndOfParent element
+ // can be terminated by a choice with choiceLengthKind='explicit',
with no reference
+ // to implicit length choices. Later on in 12.3.6, it specified what
the parent Effective
+ // Length Units would be for an explicit length choice, again with no
reference to
+ // implicit length choices. The only way to get an EndOfParent element
within an
+ // implicit length choice would be to have it actually be terminated
by the choice's
+ // parent, similar to how we treat elements within an element that is
also EndOfParent.
+ // The only mention of implicit length choices in the DFDL Spec is to
mention SDEs when
+ // an EndOfParent element is enclosed by a choice with
choiceLengthKind='implicit'. We
+ // think that may be a typo, so for now we disallow
ChoiceLengthKind.Implicit
+ // enclosing an EndOfParent element.
+ // See Daffodil-3080
+ eopChildren.foreach { child =>
+ child.schemaDefinitionWhen(
+ c.choiceLengthKind == ChoiceLengthKind.Implicit,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but its
parent is a choice with dfdl:choiceLengthKind 'implicit'."
+ )
+ child.schemaDefinitionWhen(
+ c.hasTerminator,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a choice with a dfdl:terminator."
+ )
+ child.schemaDefinitionWhen(
+ c.trailingSkip != 0,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but is
in a choice with a non-zero dfdl:trailingSkip."
+ )
+ child.checkEndOfParentRestrictionsOnCurrentElement(optParentELU)
+ }
+ }
+ case _ => // do nothing
+ }
+ // end checks
+ val sawEOP = term.termChildren.foldLeft(false) { case (sawEOP, child) =>
+ if (sawEOP) {
+ // Choice branches are alternatives, not sequential data — the
after-EOP SDE must
+ // not fire across branches. Only sequences and elements have
sequential ordering.
+ term match {
+ case _: ChoiceTermBase => // has alternatives which can all be EOP;
skip
+ case _ =>
+ child.schemaDefinitionWhen(
+ child.isInstanceOf[ModelGroup],
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but a
model group is defined between this element and the end of the enclosing
component"
Review Comment:
Note that the `child` context of the SDE in this case is the model group.
But the diagnostic makes it sound like it's the EOP element.
We could change the diagnostic to be something like "This model group is
defined after an an EOP in the containing model group". Could do something
similar with the below diagnostic.
Alternatively, instead of returning a sawEOP boolean, we could return a
`Maybe[Element]`. Where `Some(Element)` is the most recently seen EOP element,
and Nope means we haven't seen an EOP element. So insteead of `if (sawEOP)` we
do something like `if (optMostRecentEOPElement.isDefined)` (probably wants a
better name).
I dont' feel strongly either way.
##########
daffodil-core/src/main/scala/org/apache/daffodil/io/InputSource.scala:
##########
@@ -350,9 +356,19 @@ class BucketingInputSource(
bytesFilledInLastBucket = 0
lastBucketIndex += 1
if ((lastBucketIndex - oldestBucketIndex) >=
maxNumberOfNonNullBuckets) {
- // This frees the oldest bucket, allowing it to be garbage
collected.
- buckets(oldestBucketIndex) = null
- oldestBucketIndex += 1
+ if (buckets(oldestBucketIndex).refCount == 0) {
Review Comment:
I'm not sure this is right. maxNumberOfNonNullBuckets is a hard max, it
doesn't matter if things still reference old buckets and could backtrack to
them. We still need to allow those buckets to be garbage collected. If
something does backtrack into one of the garbage collected bucket then we throw
a BacktrackingException.
With this logic, we could get a PoU at the first byte that never gets
resolved and we would never garbage collect old buckets.
Note that this means we can only go so far in looking for EOD. Maybe we have
an off-by-one error somwehere, or maybe the math to figure out the target EOD
bucket needs to be fixed so we don't look for buckets too far in the future
that cause our current buckets to get garbage collected.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/TermGrammarMixin.scala:
##########
@@ -84,4 +95,149 @@ trait TermGrammarMixin extends AlignedMixin with
BitOrderMixin with TermRuntime1
MandatoryTextAlignment(this, knownEncodingAlignmentInBits, true)
}
+ def optEffectiveLengthUnits(optLastNonEOPELU: Option[LengthUnits]):
Option[LengthUnits] = {
+ this match {
+ case e: ElementBase =>
+ e.lengthKind match {
+ case LengthKind.EndOfParent => optLastNonEOPELU
+ case LengthKind.Explicit | LengthKind.Prefixed => Some(e.lengthUnits)
+ case LengthKind.Pattern => Some(LengthUnits.Characters)
+ case LengthKind.Implicit | LengthKind.Delimited =>
+ None // invalid parent; SDE fires separately
+ }
+ case c: ChoiceTermBase if c.choiceLengthKind ==
ChoiceLengthKind.Explicit =>
+ Some(LengthUnits.Bytes)
+ // Sequences are transparent — the ELU is inherited from the nearest
enclosing box.
+ case _: SequenceTermBase => optLastNonEOPELU
+ case _: ChoiceTermBase => None // implicit-length choice; SDE fires
separately
+ }
+ }
+
+ final lazy val childrenEndOfParent: Seq[ElementBase] =
LV(Symbol("childrenEndOfParent")) {
+ val gms = termChildren
+ val chls = gms.flatMap {
+ case eb: ElementBase if eb.lengthKind == LengthKind.EndOfParent =>
Seq(eb)
+ case eb: ElementBase => Nil
+ case c: ChoiceTermBase => Nil
+ case mg: ModelGroup => mg.childrenEndOfParent
+ }
+ chls
+ }.value
+
+ def checkEndOfParentRestrictions(lastNonEOPELU: Option[LengthUnits]):
Boolean = {
+ val term = this
+ lazy val eopChildren = this.childrenEndOfParent
+ lazy val optParentELU = term.optEffectiveLengthUnits(lastNonEOPELU)
+ // checks
+ term match {
+ case rootElem: Root if rootElem.lengthKind == LengthKind.EndOfParent => {
+
rootElem.checkEndOfParentRestrictionsOnCurrentElement(Some(LengthUnits.Characters))
Review Comment:
This can be lastNoneEPELU. We should use whatever we initialize
lastNoneEOPELU with, which is Characters for the root element.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/ParserTraits.scala:
##########
@@ -170,18 +171,82 @@ trait PrefixedLengthParserMixin {
* Some parsers do not calculate their own length, but instead expect another
parser
* to set the bit limit, and then they use that bit limit as the length.
* An example of this is prefix length parsers. This trait can be used by those
- * parsers to do determine the length based on the bitLimit and position.
+ * parsers to determine the length based on the bitLimit and position.
+ *
+ * For dfdl:lengthKind='endOfParent' parsers that need to scan to end-of-stream
+ * when no bit limit is set, mix in [[EndOfParentBitLengthMixin]] instead.
*/
trait BitLengthFromBitLimitMixin {
- def getBitLength(s: ParseOrUnparseState): Int = {
- val pState = s.asInstanceOf[PState]
- val len = getLengthInBits(pState)
- len.toInt
+ def getBitLength(s: ParseOrUnparseState): Int =
getBitLengthAsInt(s.asInstanceOf[PState])
+
+ /**
+ * getLengthInBits converted to Int with a parse error on overflow.
+ *
+ * The default maxCacheSizeInBytes (256 MiB) yields a maximum bit length of
+ * exactly Int.MaxValue + 1, so a bare .toInt without this guard can overflow
+ * by one bit on a full-cache EOP element.
+ */
+ def getBitLengthAsInt(pstate: PState): Int = {
+ val len = getLengthInBits(pstate)
+ if (pstate.processorStatus ne Success) return 0
+ if (len > Int.MaxValue) {
+ pstate.setFailed(
+ new ParseError(
+ One(pstate.schemaFileLocation),
+ One(pstate.currentLocation),
+ "Bit length %d exceeds maximum (%d) for this parser.",
+ len,
+ Int.MaxValue
+ )
+ )
+ 0
+ } else {
+ len.toInt
+ }
}
def getLengthInBits(pstate: PState): Long = {
- val len = pstate.bitLimit0b.get - pstate.bitPos0b
- len
+ if (pstate.bitLimit0b.isDefined) {
+ pstate.bitLimit0b.get - pstate.bitPos0b
+ } else {
+ Assert.invariantFailed("BitLimit not set for parser.")
+ }
+ }
+}
+
+/**
+ * Extends [[BitLengthFromBitLimitMixin]] with dfdl:lengthKind='endOfParent'
support.
+ * When no bit limit is set (i.e., the parser is at the root or outermost EOP
element),
+ * falls back to [[org.apache.daffodil.io.InputSource#optEndOfDataPosition]]
to locate
+ * the end of the data stream.
+ *
+ * Only mix this in for parsers that are instantiated specifically for EOP
elements.
+ * Parsers for prefixed or explicitly-bounded elements should use the plain
+ * [[BitLengthFromBitLimitMixin]]; mixing in this trait by mistake would
silently
+ * consume the entire remaining stream instead of failing at the missing bit
limit.
+ */
+trait EndOfParentBitLengthMixin extends BitLengthFromBitLimitMixin {
+
+ override def getLengthInBits(pstate: PState): Long = {
+ if (pstate.bitLimit0b.isDefined) {
+ pstate.bitLimit0b.get - pstate.bitPos0b
+ } else {
+ val dis = pstate.dataInputStream
+ dis.optEndOfDataPosition match {
+ case Some(endOfDataPosition) =>
+ (endOfDataPosition * 8) - dis.bitPos0b
+ case None =>
+ pstate.setFailed(
+ new ParseError(
+ One(pstate.schemaFileLocation),
+ One(pstate.currentLocation),
+ "Cannot determine end-of-data position for
dfdl:lengthKind='endOfParent': " +
+ "data stream exceeds the maximum cache size. Consider
increasing maxCacheSizeInBytes."
Review Comment:
I'm hesitant about this. For one `maxCacheSizeInBytes` is an implementation
defined limit of the BucketingInputSource, so this is kindof assuming the
failure is due to that. Additionally, there is now way for users to change this
via the public API. Users could use private APIs, but we dont' recommend that.
So it's a suggestion that pretty much no one can actually use.
We probably do want to fix the issue to at least allow API users to set the
cache size, but until then this diagnostic isn't really actionable for users.
Maybe we just say the data position and use knownBytesAvailable() to include
how many bytes were were able to scan from that position before giving up.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -132,6 +149,85 @@ class SpecifiedLengthPatternParser(
}
}
+class SpecifiedLengthEndOfParentParser(
+ eParser: Parser,
+ erd: ElementRuntimeData
+) extends SpecifiedLengthParserBase(eParser, erd),
+ EndOfParentBitLengthMixin {
+
+ override protected def getBitLength(s: PState): MaybeULong = {
+ MaybeULong(getLengthInBits(s))
+ }
+
+ override def parse(pState: PState): Unit = {
+ val dis = pState.dataInputStream
+
+ if (erd.isComplexType) {
+ if (dis.bitLimit0b.isDefined) {
+ val (nBits: Long, dis: InputSourceDataInputStream, startingBitPos0b:
Long) =
+ checkLengthAndParseWithinBitLimits(
+ pState,
+ shouldCheckDefinedForLength = false
+ ) match {
+ case Some(result) => result
+ case None => return
+ }
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+ val finalEndPos0b = startingBitPos0b + nBits
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex)
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+
+ Assert.invariant(dis eq pState.dataInputStream)
+ val bitsToSkip = finalEndPos0b - dis.bitPos0b
+ // if this is < 0, then the parsing of children went past the limit,
which it isn't supposed to.
+ skipBits(pState, bitsToSkip, dis)
+ } else {
+ val startingBitPos0b = dis.bitPos0b
+
+ eParser.parse1(pState)
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex) {
Review Comment:
If we have gotten here this must be comlpex so we can skip this check.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/Term.scala:
##########
@@ -562,4 +560,11 @@ trait Term
}
}
+ final lazy val realElementChildren: Seq[ElementBase] = {
Review Comment:
Should the name of this reference EOP somehow? The fact that this does not
descend into choices is specifically because this is used for EOP and is
different from how realChildren works.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -629,6 +630,9 @@ trait ElementBaseGrammarMixin
)
}
+ private lazy val unsupportedLengthKindEOPMessage =
+ "lengthKind='endOfParent' only supported for packed binary formats."
+
Review Comment:
While I agree it's nice to avoid duplication, our convention is kindof to
duplicate error messages all over the place. And this is only used in three
places, so it's not a ton of duplciatino this is avoiding.
What we really need is some sort of error message lookup table system, where
all error messages get an identifier. But until that point, I would suggest we
just duplicate this message in the few places it's used.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/ElementBaseGrammarMixin.scala:
##########
@@ -252,6 +253,60 @@ trait ElementBaseGrammarMixin
}
final lazy val prefixedLengthBody = prefixedLengthElementDecl.parsedValue
+ def checkEndOfParentRestrictionsOnCurrentElement(optParentELU:
Option[LengthUnits]): Unit = {
+ Assert.invariant(optParentELU.isDefined)
+ val parentELU = optParentELU.get
+ val currentElement: ElementBase = this
+ Assert.invariant(currentElement.lengthKind == LengthKind.EndOfParent)
+ schemaDefinitionWhen(
+ currentElement.hasTerminator,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but specifies
a dfdl:terminator."
+ )
+ schemaDefinitionWhen(
+ currentElement.trailingSkip != 0,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but specifies
a non-zero dfdl:trailingSkip."
+ )
+ schemaDefinitionWhen(
+ currentElement.maxOccurs > 1,
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but specifies
a maxOccurs greater than 1."
+ )
+ schemaDefinitionWhen(
+ currentElement.impliedRepresentation == Representation.Text
+ && (!currentElement.isKnownEncoding ||
!currentElement.knownEncodingIsFixedWidth ||
currentElement.knownEncodingWidthInBits != 8)
+ && (parentELU != LengthUnits.Characters),
+ "element is specified as dfdl:lengthKind=\"endOfParent\", but the
element has text representation, and does not have a single-byte character set
encoding, and the effective length units of the parent is not 'characters'."
+ )
+ if (currentElement.isSimpleType) {
+ val meetsSimpleTypeRestrictions = (currentElement.primType eq
PrimType.String)
+ || (currentElement.representation == Representation.Text)
+ || (currentElement.primType eq PrimType.HexBinary)
+ || (currentElement.representation == Representation.Binary
+ && ((
+ currentElement.binaryNumberRep,
Review Comment:
You need to look at the type to know if you you should look at
binaryNubmerRep or binaryCalendarRep. For example, if the type is not
date/dateTime/time, then you dont' care about the value of binaryCalendarRep
since it will be ignored.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/TermGrammarMixin.scala:
##########
@@ -84,4 +95,149 @@ trait TermGrammarMixin extends AlignedMixin with
BitOrderMixin with TermRuntime1
MandatoryTextAlignment(this, knownEncodingAlignmentInBits, true)
}
+ def optEffectiveLengthUnits(optLastNonEOPELU: Option[LengthUnits]):
Option[LengthUnits] = {
+ this match {
+ case e: ElementBase =>
+ e.lengthKind match {
+ case LengthKind.EndOfParent => optLastNonEOPELU
+ case LengthKind.Explicit | LengthKind.Prefixed => Some(e.lengthUnits)
+ case LengthKind.Pattern => Some(LengthUnits.Characters)
+ case LengthKind.Implicit | LengthKind.Delimited =>
+ None // invalid parent; SDE fires separately
+ }
+ case c: ChoiceTermBase if c.choiceLengthKind ==
ChoiceLengthKind.Explicit =>
+ Some(LengthUnits.Bytes)
+ // Sequences are transparent — the ELU is inherited from the nearest
enclosing box.
+ case _: SequenceTermBase => optLastNonEOPELU
+ case _: ChoiceTermBase => None // implicit-length choice; SDE fires
separately
+ }
+ }
+
+ final lazy val childrenEndOfParent: Seq[ElementBase] =
LV(Symbol("childrenEndOfParent")) {
+ val gms = termChildren
+ val chls = gms.flatMap {
+ case eb: ElementBase if eb.lengthKind == LengthKind.EndOfParent =>
Seq(eb)
+ case eb: ElementBase => Nil
+ case c: ChoiceTermBase => Nil
+ case mg: ModelGroup => mg.childrenEndOfParent
+ }
+ chls
+ }.value
+
+ def checkEndOfParentRestrictions(lastNonEOPELU: Option[LengthUnits]):
Boolean = {
+ val term = this
+ lazy val eopChildren = this.childrenEndOfParent
+ lazy val optParentELU = term.optEffectiveLengthUnits(lastNonEOPELU)
+ // checks
+ term match {
+ case rootElem: Root if rootElem.lengthKind == LengthKind.EndOfParent => {
Review Comment:
I'm still not convinced we need this separate case. With suggested changes
below, the logic holds for both root and non-root elements. The only addition
if a condition to call checkEndOfParentRestrictionsOnCurrentElement on the
rootELem.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/TermGrammarMixin.scala:
##########
@@ -84,4 +95,149 @@ trait TermGrammarMixin extends AlignedMixin with
BitOrderMixin with TermRuntime1
MandatoryTextAlignment(this, knownEncodingAlignmentInBits, true)
}
+ def optEffectiveLengthUnits(optLastNonEOPELU: Option[LengthUnits]):
Option[LengthUnits] = {
+ this match {
+ case e: ElementBase =>
+ e.lengthKind match {
+ case LengthKind.EndOfParent => optLastNonEOPELU
+ case LengthKind.Explicit | LengthKind.Prefixed => Some(e.lengthUnits)
+ case LengthKind.Pattern => Some(LengthUnits.Characters)
+ case LengthKind.Implicit | LengthKind.Delimited =>
+ None // invalid parent; SDE fires separately
+ }
+ case c: ChoiceTermBase if c.choiceLengthKind ==
ChoiceLengthKind.Explicit =>
+ Some(LengthUnits.Bytes)
+ // Sequences are transparent — the ELU is inherited from the nearest
enclosing box.
+ case _: SequenceTermBase => optLastNonEOPELU
+ case _: ChoiceTermBase => None // implicit-length choice; SDE fires
separately
+ }
+ }
+
+ final lazy val childrenEndOfParent: Seq[ElementBase] =
LV(Symbol("childrenEndOfParent")) {
+ val gms = termChildren
+ val chls = gms.flatMap {
+ case eb: ElementBase if eb.lengthKind == LengthKind.EndOfParent =>
Seq(eb)
+ case eb: ElementBase => Nil
+ case c: ChoiceTermBase => Nil
+ case mg: ModelGroup => mg.childrenEndOfParent
+ }
+ chls
+ }.value
+
+ def checkEndOfParentRestrictions(lastNonEOPELU: Option[LengthUnits]):
Boolean = {
+ val term = this
+ lazy val eopChildren = this.childrenEndOfParent
+ lazy val optParentELU = term.optEffectiveLengthUnits(lastNonEOPELU)
+ // checks
+ term match {
+ case rootElem: Root if rootElem.lengthKind == LengthKind.EndOfParent => {
+
rootElem.checkEndOfParentRestrictionsOnCurrentElement(Some(LengthUnits.Characters))
+ eopChildren.foreach { child =>
+ child.checkEndOfParentRestrictionsOnCurrentElement(lastNonEOPELU)
Review Comment:
This can pass in optParentELU. For a root element with EOP optParentELU ends
up being lastNonEOPELU, so it'sfunctionally the same, but it makes it so all
calls to `checkEndOfParentRestrictionsOnCurrentElement` have the same
optParentELU parameter.
##########
daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/TermGrammarMixin.scala:
##########
@@ -84,4 +95,149 @@ trait TermGrammarMixin extends AlignedMixin with
BitOrderMixin with TermRuntime1
MandatoryTextAlignment(this, knownEncodingAlignmentInBits, true)
}
+ def optEffectiveLengthUnits(optLastNonEOPELU: Option[LengthUnits]):
Option[LengthUnits] = {
+ this match {
+ case e: ElementBase =>
+ e.lengthKind match {
+ case LengthKind.EndOfParent => optLastNonEOPELU
+ case LengthKind.Explicit | LengthKind.Prefixed => Some(e.lengthUnits)
+ case LengthKind.Pattern => Some(LengthUnits.Characters)
+ case LengthKind.Implicit | LengthKind.Delimited =>
+ None // invalid parent; SDE fires separately
+ }
+ case c: ChoiceTermBase if c.choiceLengthKind ==
ChoiceLengthKind.Explicit =>
+ Some(LengthUnits.Bytes)
+ // Sequences are transparent — the ELU is inherited from the nearest
enclosing box.
+ case _: SequenceTermBase => optLastNonEOPELU
+ case _: ChoiceTermBase => None // implicit-length choice; SDE fires
separately
+ }
+ }
+
+ final lazy val childrenEndOfParent: Seq[ElementBase] =
LV(Symbol("childrenEndOfParent")) {
+ val gms = termChildren
+ val chls = gms.flatMap {
+ case eb: ElementBase if eb.lengthKind == LengthKind.EndOfParent =>
Seq(eb)
+ case eb: ElementBase => Nil
+ case c: ChoiceTermBase => Nil
+ case mg: ModelGroup => mg.childrenEndOfParent
+ }
+ chls
+ }.value
+
+ def checkEndOfParentRestrictions(lastNonEOPELU: Option[LengthUnits]):
Boolean = {
+ val term = this
+ lazy val eopChildren = this.childrenEndOfParent
+ lazy val optParentELU = term.optEffectiveLengthUnits(lastNonEOPELU)
+ // checks
+ term match {
+ case rootElem: Root if rootElem.lengthKind == LengthKind.EndOfParent => {
+
rootElem.checkEndOfParentRestrictionsOnCurrentElement(Some(LengthUnits.Characters))
+ eopChildren.foreach { child =>
+ child.checkEndOfParentRestrictionsOnCurrentElement(lastNonEOPELU)
+ }
+ }
+ case e: ElementBase if eopChildren.nonEmpty => {
Review Comment:
We an remove the `eopChildren.nonEmpty` checks in all of these cases. If
it's empty the eopChildren.foreach loops will just be no-ops.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/ParserTraits.scala:
##########
@@ -170,18 +171,82 @@ trait PrefixedLengthParserMixin {
* Some parsers do not calculate their own length, but instead expect another
parser
* to set the bit limit, and then they use that bit limit as the length.
* An example of this is prefix length parsers. This trait can be used by those
- * parsers to do determine the length based on the bitLimit and position.
+ * parsers to determine the length based on the bitLimit and position.
+ *
+ * For dfdl:lengthKind='endOfParent' parsers that need to scan to end-of-stream
+ * when no bit limit is set, mix in [[EndOfParentBitLengthMixin]] instead.
*/
trait BitLengthFromBitLimitMixin {
- def getBitLength(s: ParseOrUnparseState): Int = {
- val pState = s.asInstanceOf[PState]
- val len = getLengthInBits(pState)
- len.toInt
+ def getBitLength(s: ParseOrUnparseState): Int =
getBitLengthAsInt(s.asInstanceOf[PState])
+
+ /**
+ * getLengthInBits converted to Int with a parse error on overflow.
+ *
+ * The default maxCacheSizeInBytes (256 MiB) yields a maximum bit length of
Review Comment:
I don't think maxCacheSizeInBytes is relevant here. That is an
implementation detail of the bucketing input source and can be configured. Even
with the ByteArrayInputSource, which desn't have a concept of a max cache size,
we could theoretically overflow if we had a length in bits over 4GB.
That said, we might have a bug if we are using Int's, we should probably be
using Longs to avoid this overflow. Weuse long for getLengthInBits, so I'm not
sure whey we would ever want to convert that to a Int. Might be worth opening a
ticket.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/ParserTraits.scala:
##########
@@ -170,18 +171,82 @@ trait PrefixedLengthParserMixin {
* Some parsers do not calculate their own length, but instead expect another
parser
* to set the bit limit, and then they use that bit limit as the length.
* An example of this is prefix length parsers. This trait can be used by those
- * parsers to do determine the length based on the bitLimit and position.
+ * parsers to determine the length based on the bitLimit and position.
+ *
+ * For dfdl:lengthKind='endOfParent' parsers that need to scan to end-of-stream
+ * when no bit limit is set, mix in [[EndOfParentBitLengthMixin]] instead.
*/
trait BitLengthFromBitLimitMixin {
- def getBitLength(s: ParseOrUnparseState): Int = {
- val pState = s.asInstanceOf[PState]
- val len = getLengthInBits(pState)
- len.toInt
+ def getBitLength(s: ParseOrUnparseState): Int =
getBitLengthAsInt(s.asInstanceOf[PState])
+
+ /**
+ * getLengthInBits converted to Int with a parse error on overflow.
+ *
+ * The default maxCacheSizeInBytes (256 MiB) yields a maximum bit length of
+ * exactly Int.MaxValue + 1, so a bare .toInt without this guard can overflow
+ * by one bit on a full-cache EOP element.
+ */
+ def getBitLengthAsInt(pstate: PState): Int = {
+ val len = getLengthInBits(pstate)
+ if (pstate.processorStatus ne Success) return 0
+ if (len > Int.MaxValue) {
+ pstate.setFailed(
+ new ParseError(
Review Comment:
I think this might want to be an SDE. We shoudl double check what we do in
other places where things exceeed implementation limits, but I don't think we
usually allow backtracking.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/ParserTraits.scala:
##########
@@ -170,18 +171,82 @@ trait PrefixedLengthParserMixin {
* Some parsers do not calculate their own length, but instead expect another
parser
* to set the bit limit, and then they use that bit limit as the length.
* An example of this is prefix length parsers. This trait can be used by those
- * parsers to do determine the length based on the bitLimit and position.
+ * parsers to determine the length based on the bitLimit and position.
+ *
+ * For dfdl:lengthKind='endOfParent' parsers that need to scan to end-of-stream
+ * when no bit limit is set, mix in [[EndOfParentBitLengthMixin]] instead.
*/
trait BitLengthFromBitLimitMixin {
- def getBitLength(s: ParseOrUnparseState): Int = {
- val pState = s.asInstanceOf[PState]
- val len = getLengthInBits(pState)
- len.toInt
+ def getBitLength(s: ParseOrUnparseState): Int =
getBitLengthAsInt(s.asInstanceOf[PState])
+
+ /**
+ * getLengthInBits converted to Int with a parse error on overflow.
+ *
+ * The default maxCacheSizeInBytes (256 MiB) yields a maximum bit length of
+ * exactly Int.MaxValue + 1, so a bare .toInt without this guard can overflow
+ * by one bit on a full-cache EOP element.
+ */
+ def getBitLengthAsInt(pstate: PState): Int = {
+ val len = getLengthInBits(pstate)
+ if (pstate.processorStatus ne Success) return 0
+ if (len > Int.MaxValue) {
+ pstate.setFailed(
+ new ParseError(
+ One(pstate.schemaFileLocation),
+ One(pstate.currentLocation),
+ "Bit length %d exceeds maximum (%d) for this parser.",
Review Comment:
I don't think we usualy say "this parser". That isn't really meaningful to
users. I think it's sufficient to just say the limit exceeededthe maximum
value. We probably have standard language we could copy, we have other limits
through the code.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/ParserTraits.scala:
##########
@@ -170,18 +171,82 @@ trait PrefixedLengthParserMixin {
* Some parsers do not calculate their own length, but instead expect another
parser
* to set the bit limit, and then they use that bit limit as the length.
* An example of this is prefix length parsers. This trait can be used by those
- * parsers to do determine the length based on the bitLimit and position.
+ * parsers to determine the length based on the bitLimit and position.
+ *
+ * For dfdl:lengthKind='endOfParent' parsers that need to scan to end-of-stream
+ * when no bit limit is set, mix in [[EndOfParentBitLengthMixin]] instead.
*/
trait BitLengthFromBitLimitMixin {
- def getBitLength(s: ParseOrUnparseState): Int = {
- val pState = s.asInstanceOf[PState]
- val len = getLengthInBits(pState)
- len.toInt
+ def getBitLength(s: ParseOrUnparseState): Int =
getBitLengthAsInt(s.asInstanceOf[PState])
+
+ /**
+ * getLengthInBits converted to Int with a parse error on overflow.
+ *
+ * The default maxCacheSizeInBytes (256 MiB) yields a maximum bit length of
+ * exactly Int.MaxValue + 1, so a bare .toInt without this guard can overflow
+ * by one bit on a full-cache EOP element.
+ */
+ def getBitLengthAsInt(pstate: PState): Int = {
+ val len = getLengthInBits(pstate)
+ if (pstate.processorStatus ne Success) return 0
+ if (len > Int.MaxValue) {
+ pstate.setFailed(
+ new ParseError(
+ One(pstate.schemaFileLocation),
+ One(pstate.currentLocation),
+ "Bit length %d exceeds maximum (%d) for this parser.",
+ len,
+ Int.MaxValue
+ )
+ )
+ 0
+ } else {
+ len.toInt
+ }
}
def getLengthInBits(pstate: PState): Long = {
- val len = pstate.bitLimit0b.get - pstate.bitPos0b
- len
+ if (pstate.bitLimit0b.isDefined) {
+ pstate.bitLimit0b.get - pstate.bitPos0b
+ } else {
+ Assert.invariantFailed("BitLimit not set for parser.")
+ }
+ }
+}
+
+/**
+ * Extends [[BitLengthFromBitLimitMixin]] with dfdl:lengthKind='endOfParent'
support.
+ * When no bit limit is set (i.e., the parser is at the root or outermost EOP
element),
+ * falls back to [[org.apache.daffodil.io.InputSource#optEndOfDataPosition]]
to locate
+ * the end of the data stream.
+ *
+ * Only mix this in for parsers that are instantiated specifically for EOP
elements.
+ * Parsers for prefixed or explicitly-bounded elements should use the plain
+ * [[BitLengthFromBitLimitMixin]]; mixing in this trait by mistake would
silently
+ * consume the entire remaining stream instead of failing at the missing bit
limit.
+ */
+trait EndOfParentBitLengthMixin extends BitLengthFromBitLimitMixin {
+
+ override def getLengthInBits(pstate: PState): Long = {
+ if (pstate.bitLimit0b.isDefined) {
+ pstate.bitLimit0b.get - pstate.bitPos0b
+ } else {
+ val dis = pstate.dataInputStream
+ dis.optEndOfDataPosition match {
+ case Some(endOfDataPosition) =>
+ (endOfDataPosition * 8) - dis.bitPos0b
+ case None =>
+ pstate.setFailed(
+ new ParseError(
Review Comment:
I think this wants to be a SDE. I think hitteing implementation defines
limits is not backtrackable.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -87,12 +76,39 @@ sealed abstract class SpecifiedLengthParserBase(eParser:
Parser, erd: RuntimeDat
// we want to capture the length before we do any skipping
// value length of simple types is captured by the eParser if needed
- // the SpecifiedLengthParserBase is extended by
SpecifiedLengthChoiceParser which should not have its valueLength captured here
+ // the SpecifiedLengthParserBase is extended by SpecifiedLengthChoiceParser
+ // which should not have its valueLength captured here
if (pState.infoset.isComplex && !erd.isInstanceOf[ChoiceRuntimeData])
captureValueLength(pState, ULong(startingBitPos0b), ULong(dis.bitPos0b))
Assert.invariant(dis eq pState.dataInputStream)
val bitsToSkip = finalEndPos0b - dis.bitPos0b
+ skipBits(pState, bitsToSkip, dis)
+ }
+
+ protected def checkLengthAndParseWithinBitLimits(
+ pState: PState,
+ shouldCheckDefinedForLength: Boolean
+ ): Option[(Long, InputSourceDataInputStream, Long)] = {
+ val maybeNBits = getBitLength(pState)
+
+ if (pState.processorStatus ne Success) return None
+ val nBits = maybeNBits.get
+ val dis = pState.dataInputStream
+
+ if (shouldCheckDefinedForLength && !dis.isDefinedForLength(nBits)) {
+ PENotEnoughBits(pState, nBits, dis)
+ return None
+ }
+
+ val startingBitPos0b = dis.bitPos0b
+ dis.withBitLengthLimit(nBits) {
+ eParser.parse1(pState)
+ }
+ Some((nBits, dis, startingBitPos0b))
Review Comment:
Do we need to return dis and startingBitPosition? Can those be captured by
the caller before calling this function? Then we only need to return nBits so
we don't need to return a tuple.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -52,29 +52,18 @@ sealed abstract class SpecifiedLengthParserBase(eParser:
Parser, erd: RuntimeDat
*/
protected def getBitLength(s: PState): MaybeULong
- final def parse(pState: PState): Unit = {
-
- val maybeNBits = getBitLength(pState)
-
- if (pState.processorStatus._ne_(Success)) return
- val nBits = maybeNBits.get
- val dis = pState.dataInputStream
-
- val shouldCheckDefinedForLength = erd match {
+ def parse(pState: PState): Unit = {
+ lazy val shouldCheckDefinedForLength = erd match {
Review Comment:
This doesn't need to be lazy, we will always evaluate this.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -132,6 +149,85 @@ class SpecifiedLengthPatternParser(
}
}
+class SpecifiedLengthEndOfParentParser(
+ eParser: Parser,
+ erd: ElementRuntimeData
+) extends SpecifiedLengthParserBase(eParser, erd),
+ EndOfParentBitLengthMixin {
+
+ override protected def getBitLength(s: PState): MaybeULong = {
+ MaybeULong(getLengthInBits(s))
+ }
+
+ override def parse(pState: PState): Unit = {
+ val dis = pState.dataInputStream
+
+ if (erd.isComplexType) {
+ if (dis.bitLimit0b.isDefined) {
+ val (nBits: Long, dis: InputSourceDataInputStream, startingBitPos0b:
Long) =
+ checkLengthAndParseWithinBitLimits(
+ pState,
+ shouldCheckDefinedForLength = false
+ ) match {
+ case Some(result) => result
+ case None => return
+ }
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+ val finalEndPos0b = startingBitPos0b + nBits
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex)
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+
+ Assert.invariant(dis eq pState.dataInputStream)
+ val bitsToSkip = finalEndPos0b - dis.bitPos0b
+ // if this is < 0, then the parsing of children went past the limit,
which it isn't supposed to.
+ skipBits(pState, bitsToSkip, dis)
+ } else {
+ val startingBitPos0b = dis.bitPos0b
+
+ eParser.parse1(pState)
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex) {
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+ }
+
+ val maybeNBits = getBitLength(pState)
+
+ if (pState.processorStatus ne Success) return
+ val nBits = maybeNBits.get
+
+ if (!dis.isDefinedForLength(nBits)) {
Review Comment:
We dont' need this check. If we got a result it means we hit EOD and so are
guarnateed to know we are defined for that length.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -132,6 +149,85 @@ class SpecifiedLengthPatternParser(
}
}
+class SpecifiedLengthEndOfParentParser(
+ eParser: Parser,
+ erd: ElementRuntimeData
+) extends SpecifiedLengthParserBase(eParser, erd),
+ EndOfParentBitLengthMixin {
+
+ override protected def getBitLength(s: PState): MaybeULong = {
+ MaybeULong(getLengthInBits(s))
+ }
+
+ override def parse(pState: PState): Unit = {
+ val dis = pState.dataInputStream
+
+ if (erd.isComplexType) {
+ if (dis.bitLimit0b.isDefined) {
+ val (nBits: Long, dis: InputSourceDataInputStream, startingBitPos0b:
Long) =
+ checkLengthAndParseWithinBitLimits(
+ pState,
+ shouldCheckDefinedForLength = false
+ ) match {
+ case Some(result) => result
+ case None => return
+ }
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+ val finalEndPos0b = startingBitPos0b + nBits
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex)
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+
+ Assert.invariant(dis eq pState.dataInputStream)
+ val bitsToSkip = finalEndPos0b - dis.bitPos0b
+ // if this is < 0, then the parsing of children went past the limit,
which it isn't supposed to.
+ skipBits(pState, bitsToSkip, dis)
+ } else {
+ val startingBitPos0b = dis.bitPos0b
Review Comment:
I think we need to comment explain why we have this separate logic for the
complex + no-bit limit case. Something that says that getLength in that case
will read to EOD, which we don't want to do for complex types because EOD could
be very far away. Instead we just let the children parse as much as they want,
and after that we figure out how much is left over to EOD and skip it
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -132,6 +149,85 @@ class SpecifiedLengthPatternParser(
}
}
+class SpecifiedLengthEndOfParentParser(
+ eParser: Parser,
+ erd: ElementRuntimeData
+) extends SpecifiedLengthParserBase(eParser, erd),
+ EndOfParentBitLengthMixin {
+
+ override protected def getBitLength(s: PState): MaybeULong = {
+ MaybeULong(getLengthInBits(s))
+ }
+
+ override def parse(pState: PState): Unit = {
+ val dis = pState.dataInputStream
+
+ if (erd.isComplexType) {
+ if (dis.bitLimit0b.isDefined) {
+ val (nBits: Long, dis: InputSourceDataInputStream, startingBitPos0b:
Long) =
Review Comment:
I think we can call super.parse() in this case too. If bit limit is defined
then getLength won't try to scan for EOD, it'll just return bit limit. I we did
that, I think that would simplify this quite a bit and would make it so you
wouldn't need a good chunk of the above changes. For example, I think the
`checkLengthAndParseWithinBitLimits` function is no longer needed since it's
only called by the super parser function.
##########
daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/SpecifiedLengthParsers.scala:
##########
@@ -132,6 +149,85 @@ class SpecifiedLengthPatternParser(
}
}
+class SpecifiedLengthEndOfParentParser(
+ eParser: Parser,
+ erd: ElementRuntimeData
+) extends SpecifiedLengthParserBase(eParser, erd),
+ EndOfParentBitLengthMixin {
+
+ override protected def getBitLength(s: PState): MaybeULong = {
+ MaybeULong(getLengthInBits(s))
+ }
+
+ override def parse(pState: PState): Unit = {
+ val dis = pState.dataInputStream
+
+ if (erd.isComplexType) {
+ if (dis.bitLimit0b.isDefined) {
+ val (nBits: Long, dis: InputSourceDataInputStream, startingBitPos0b:
Long) =
+ checkLengthAndParseWithinBitLimits(
+ pState,
+ shouldCheckDefinedForLength = false
+ ) match {
+ case Some(result) => result
+ case None => return
+ }
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+ val finalEndPos0b = startingBitPos0b + nBits
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex)
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+
+ Assert.invariant(dis eq pState.dataInputStream)
+ val bitsToSkip = finalEndPos0b - dis.bitPos0b
+ // if this is < 0, then the parsing of children went past the limit,
which it isn't supposed to.
+ skipBits(pState, bitsToSkip, dis)
+ } else {
+ val startingBitPos0b = dis.bitPos0b
+
+ eParser.parse1(pState)
+
+ // at this point the recursive parse of the children is finished
+ // so if we're still successful we need to advance the position
+ // to skip past any bits that the recursive child parse did not
+ // consume at the end. That is, the specified length can be an
+ // outer constraint, but the children may not use it all up, leaving
+ // a section at the end.
+ if (pState.processorStatus ne Success) return
+
+ // we want to capture the length before we do any skipping
+ // value length of simple types is captured by the eParser
+ if (pState.infoset.isComplex) {
+ captureValueLength(pState, ULong(startingBitPos0b),
ULong(dis.bitPos0b))
+ }
+
+ val maybeNBits = getBitLength(pState)
Review Comment:
myabe add a comment here that says this will find remaining bits to EOD. It
might also be worth noting that because getBitLength is called after the child
parse this value does not indicate the length of the complex type like it
normall does, but instead indicates the number of bits until EOD.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]