YAMLAnchorReplayingParser.java

package tools.jackson.dataformat.yaml;

import java.io.Reader;

import java.util.*;

import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.common.Anchor;
import org.snakeyaml.engine.v2.events.*;

import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.exc.StreamConstraintsException;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.util.BufferRecycler;

/**
 * A parser that remembers the events of anchored parts in yaml and repeats them
 * to inline these parts when an alias if found instead of only returning an alias.
 * <p>
 * Note: this overwrites the nextEvent() since the base {@code super.nextToken()}
 * manages too much state, and it seems to be much simpler to re-emit the events.
 *
 * @since 2.19 / 3.1
 */
public class YAMLAnchorReplayingParser extends YAMLParser
{
    private static class AnchorContext {
        public final String anchor;
        public final List<Event> events = new ArrayList<>();
        public int depth = 1;

        public AnchorContext(String anchor) {
            this.anchor = anchor;
        }
    }

    /**
     *  the maximum number of events that can be replayed
     */
    public static final int MAX_EVENTS = 9999;

    /**
     * the maximum limit of anchors to remember
     *
     * @deprecated Since 2.21.6 / 3.1.6 not used for anything: this counted anchors nested inside
     *   each other, and since every such anchor is an open collection, the (configurable)
     *   {@link tools.jackson.core.StreamReadConstraints#getMaxNestingDepth()} is
     *   always reached first.
     */
    @Deprecated
    public static final int MAX_ANCHORS = 9999;

    /**
     * the maximum limit of merges to follow
     *
     * @deprecated Since 2.21.6 / 3.1.6 not used for anything: nesting of merge keys is limited by
     *   {@link tools.jackson.core.StreamReadConstraints#getMaxNestingDepth()},
     *   which is both configurable and reached first (merge nesting can never exceed
     *   the nesting depth of the document it occurs in).
     */
    @Deprecated
    public static final int MAX_MERGES = 9999;

    /**
     * the maximum limit of references to remember
     */
    public static final int MAX_REFS = 9999;

    /**
     * Remembers when a merge has been started in order to skip the corresponding
     * sequence end which needs to be excluded
     */
    private final ArrayDeque<Integer> mergeStack = new ArrayDeque<>();

    /**
     * Collects nested anchor definitions
     */
    private final ArrayDeque<AnchorContext> tokenStack = new ArrayDeque<>();

    /**
     * Keeps track of the last sequentially found definition of each anchor
     */
    private final Map<String, List<Event>> referencedObjects = new HashMap<>();

    /**
     * Keeps track of events that have been insert when processing alias
     */
    private final ArrayDeque<Event> refEvents = new ArrayDeque<>();

    /**
     * keeps track of the global depth of nested collections
     */
    private int globalDepth = 0;

    /**
     * @deprecated Since 3.3 use
     *   {@link #YAMLAnchorReplayingParser(ObjectReadContext, IOContext, BufferRecycler, int, int, LoadSettings, Reader, boolean)}
     *   instead
     */
    @Deprecated // since 3.3
    public YAMLAnchorReplayingParser(ObjectReadContext readCtxt, IOContext ioCtxt, BufferRecycler br,
                      int streamReadFeatures, int formatFeatures,
                      LoadSettings loadSettings, Reader reader) {
        this(readCtxt, ioCtxt, br, streamReadFeatures, formatFeatures, loadSettings, reader, false);
    }

    /**
     * @param ownsReader Whether {@code reader} was constructed by Jackson (and hence must
     *    always be closed), or provided by the caller
     *
     * @since 3.3
     */
    public YAMLAnchorReplayingParser(ObjectReadContext readCtxt, IOContext ioCtxt, BufferRecycler br,
                      int streamReadFeatures, int formatFeatures,
                      LoadSettings loadSettings, Reader reader, boolean ownsReader) {
        super(readCtxt, ioCtxt, br, streamReadFeatures, formatFeatures, loadSettings, reader, ownsReader);
    }

    private void finishContext(AnchorContext context) throws StreamConstraintsException {
        if (referencedObjects.size() >= MAX_REFS) {
            throw constraintsException("too many references in the document",
                    referencedObjects.size() + 1, MAX_REFS, "MAX_REFS");
        }
        referencedObjects.put(context.anchor, context.events);
        if (!tokenStack.isEmpty()) {
            List<Event> events = tokenStack.peek().events;
            if (events.size() + context.events.size() > MAX_EVENTS) {
                throw constraintsException("too many events to replay",
                        events.size() + context.events.size(), MAX_EVENTS, "MAX_EVENTS");
            }
            events.addAll(context.events);
        }
    }

    protected Event trackDepth(Event event) {
        if (event instanceof CollectionStartEvent) {
            ++globalDepth;
        } else if (event instanceof CollectionEndEvent) {
            --globalDepth;
        }
        return event;
    }

    protected Event filterEvent(Event event) {
        if (event instanceof MappingEndEvent) {
            if (!mergeStack.isEmpty()) {
                if (mergeStack.peek() > globalDepth) {
                    mergeStack.pop();
                    return null;
                }
            }
        }
        return event;
    }

    protected void recordEvent(Event event) {
	    if (tokenStack.isEmpty()) return;
	    AnchorContext context = tokenStack.peek();
	    if (context.events.size() >= MAX_EVENTS) {
	        throw constraintsException("too many events to replay",
	                context.events.size() + 1, MAX_EVENTS, "MAX_EVENTS");
	    }
	    context.events.add(event);
	    if (event instanceof CollectionStartEvent) {
	        ++context.depth;
	    } else if (event instanceof CollectionEndEvent) {
	        --context.depth;
	        if (context.depth == 0) {
	            tokenStack.pop();
	            finishContext(context);
	        }
	    }
    }

    @Override
    protected Event nextEvent() {
        return nextEvent(true);
    }

    // @since 3.1.1
    protected Event nextEvent(boolean recordEvents) {
        // 2.21.6 / 3.1.6: merge keys used to be followed by recursing into this method,
        //   which let a document with deeply nested `<<` keys exhaust the stack (with
        //   `StackOverflowError`) before any of the limits here was reached. Both the
        //   merge and the alias continuation are tail calls, so loop instead.
        while (true) {
            while (!refEvents.isEmpty()) {
                Event event = filterEvent(trackDepth(refEvents.removeFirst()));
                if (event != null) {
                    if (recordEvents) {
                        recordEvent(event);
                    }
                    return event;
                }
            }

            Event event = null;
            while (event == null) {
                event = trackDepth(super.nextEvent());
                if (event == null) {
                    return null;
                }
                event = filterEvent(event);
            }

            if (event instanceof AliasEvent alias) {
                List<Event> events = referencedObjects.get(alias.getAlias().getValue());
                if (events != null) {
                    if (refEvents.size() + events.size() > MAX_EVENTS) {
                        throw constraintsException("too many events to replay",
                                refEvents.size() + events.size(), MAX_EVENTS, "MAX_EVENTS");
                    }
                    refEvents.addAll(events);
                    continue;
                }
                _reportError("invalid alias: " + alias.getAlias());
            }

            if (event instanceof NodeEvent nodeEvent) {
                String anchor = nodeEvent.getAnchor().map(Anchor::getValue).orElse(null);
                if (anchor != null) {
                    AnchorContext context = new AnchorContext(anchor);
                    context.events.add(event);
                    if (event instanceof CollectionStartEvent) {
                        tokenStack.push(context);
                    } else {
                        // directly store it
                        finishContext(context);
                    }
                    // no need to record this event as it was handled above
                    return event;
                }
            }

            if (event instanceof ScalarEvent scalarEvent) {
                if (scalarEvent.getValue().equals("<<")) {
                    // expect next node to be a map
                    // this mapping start event must not be registered by anchors; it is dropped so the contents are merged
                    Event next = nextEvent(false);
                    if (next instanceof MappingStartEvent) {
                        // 2.21.6 / 3.1.6: [dataformats-text#707] the `MappingStartEvent` of a
                        //   merged map is consumed here and its `MappingEndEvent` filtered out,
                        //   so merge nesting never reaches the usual nesting depth accounting
                        //   and has to be validated explicitly. Use `globalDepth`, which counts
                        //   all collection starts of the underlying event stream: structural
                        //   and merge nesting then share one limit.
                        streamReadConstraints().validateNestingDepth(globalDepth);
                        mergeStack.push(globalDepth);
                        // and then continue with the first event of the merged map
                        continue;
                    }
                    _reportError("found field '<<' but value isn't a map");
                }
            }

            if (recordEvents) {
                recordEvent(event);
            }
            return event;
        }
    }

    /**
     * Builds exception for one of the {@code MAX_xxx} limits of this class, including both
     * the count that tripped it and the limit itself (limits are constants, not settings,
     * so the constant is named to make it discoverable).
     */
    private StreamConstraintsException constraintsException(String problem,
            int count, int maxAllowed, String constantName)
    {
        return new StreamConstraintsException(String.format(
                "%s: %d exceeds the maximum allowed (%d, from `%s.%s`)",
                problem, count, maxAllowed,
                YAMLAnchorReplayingParser.class.getSimpleName(), constantName));
    }
}