001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.cbor;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.lang.reflect.*;
023
024import org.apache.juneau.marshall.*;
025import org.apache.juneau.marshall.parser.*;
026import org.apache.juneau.marshall.stream.*;
027
028/**
029 * Reference implementation of the public {@link TokenReader} surface for the CBOR format
030 * (RFC 8949).
031 *
032 * <p>
033 * Backed by the existing internal {@link CborInputStream} plumbing.  CBOR's wire format is already
034 * token-shaped (major-type encoded data items), so this cursor maps almost directly onto
035 * {@link CborInputStream#readDataType()}.
036 *
037 * <h5 class='section'>Notes:</h5><ul>
038 *    <li class='warn'>This class is not thread safe.
039 *    <li>The cursor is a true O(1)-memory streaming cursor ({@link #isStreaming()} == <jk>true</jk>).
040 *    <li>The cursor is purely structural; object swaps and {@code @Schema} annotations have no
041 *       effect.  Use the POJO databind path on {@link CborParser} for those.
042 * </ul>
043 */
044@SuppressWarnings({
045   "resource" // The cursor's underlying ParserPipe is owned by the caller via try-with-resources on the cursor itself; Eclipse JDT flags the inner pipe as unclosed but that's by design.
046})
047public class CborTokenReader implements TokenReader {
048
049   private static final long INDEFINITE = -1L;
050
051   /**
052    * Default maximum number of nested CBOR semantic tags (major type 6) the cursor will unwrap
053    * before throwing a {@link ParseException}.
054    *
055    * <p>
056    * CBOR allows a value to be wrapped in an arbitrary number of semantic tags
057    * (e.g. <c>0xC0 0xC0 ... value</c>).  Each tag is unwrapped recursively by {@link #next()}, so a
058    * pathologically deep chain of tags would otherwise blow the JVM call stack with a
059    * {@link StackOverflowError}.  This bound converts that failure mode into a clean, catchable
060    * {@link ParseException}.
061    */
062   static final int DEFAULT_MAX_TAG_NESTING_DEPTH = 64;
063
064   private final CborInputStream is;
065   private final ParserPipe pipe;
066   private final CborParserSession session;
067
068   private int maxTagNestingDepth = DEFAULT_MAX_TAG_NESTING_DEPTH;
069   // Current depth of the in-progress recursive tag unwrap (reset to 0 once a value is emitted).
070   private int tagNestingDepth;
071
072   private TokenType currentToken = TokenType.NOT_AVAILABLE;
073   private boolean ended;
074
075   // Container stack: parallel arrays — kind (0=array, 1=map) + remaining element count
076   // (INDEFINITE for indefinite-length).  For maps the count covers key+value pairs at 2 each.
077   private long[] stackRemaining = new long[16];
078   private boolean[] stackIsMap = new boolean[16];
079   // Per-level: at map-level, true when next emit should be a FIELD_NAME (key); false when next
080   // emit should be a value.  Stored per-level so nested containers don't corrupt each other.
081   private boolean[] stackAwaitingKey = new boolean[16];
082   private int depth;
083
084   // Scratch state for current token's value.
085   private String currentString;
086   private String currentNumberLexeme;
087   private Number currentNumber;
088   private boolean currentBoolean;
089   private byte[] currentBinary;
090
091   // Binary-native opt-in state (175ad).  Defaults preserve the normalize-by-default contract:
092   // nativeMode == false ⇒ tag numbers are dropped, simple values surface as VALUE_NULL with no
093   // metadata.  When nativeMode is true, the TAG branch accumulates tag numbers into 'tags' as
094   // it recurses, the SIMPLE branch captures the simple int into 'simpleValue', and 'nativeKind'
095   // classifies the resulting token for format-agnostic consumers.
096   private boolean nativeMode;
097   private BinaryNativeKind nativeKind = BinaryNativeKind.NONE;
098   private long[] tags = new long[4];
099   private int tagCount;
100   private int simpleValue;
101
102   /**
103    * Constructor with default settings.
104    *
105    * @param pipe The parser input pipe.  Must not be <jk>null</jk>.
106    * @throws IOException If a problem occurred opening the underlying stream.
107    */
108   public CborTokenReader(ParserPipe pipe) throws IOException {
109      this(pipe, null);
110   }
111
112   /**
113    * Constructor used by {@link CborParserSession#readTokens(Object)} to plumb the calling
114    * session through so that {@link #read(Class)} can delegate to the CBOR databind path.
115    *
116    * @param pipe The parser input pipe.  Must not be <jk>null</jk>.
117    * @param session The {@link CborParserSession} for {@link #read(Class)} delegation, or
118    *    <jk>null</jk> to disable {@code read}.
119    * @throws IOException If a problem occurred opening the underlying stream.
120    */
121   public CborTokenReader(ParserPipe pipe, CborParserSession session) throws IOException {
122      this.pipe = pipe;
123      this.is = new CborInputStream(pipe);
124      this.session = session;
125   }
126
127   /**
128    * Sets the maximum number of nested CBOR semantic tags this cursor will unwrap before throwing
129    * a {@link ParseException}.
130    *
131    * <p>
132    * Defaults to {@link #DEFAULT_MAX_TAG_NESTING_DEPTH} (64).  A chain of more than this many tags
133    * wrapping a single value causes {@link #next()} to throw rather than risk a
134    * {@link StackOverflowError}.
135    *
136    * @param value The new maximum nesting depth.  Must be {@code >= 0}.
137    * @return This object.
138    */
139   public CborTokenReader setMaxTagNestingDepth(int value) {
140      maxTagNestingDepth = value;
141      return this;
142   }
143
144   /**
145    * Enables or disables binary-native opt-in mode.
146    *
147    * <p>
148    * When enabled, CBOR semantic tags are captured on the tag stack rather than discarded
149    * (surface them via {@link #getTagCount()} / {@link #getTag(int)}), and CBOR simple values
150    * surface their int via {@link #getSimpleValue()}.  When disabled (default), tags are
151    * silently unwrapped and simple values collapse to {@link TokenType#VALUE_NULL} with no
152    * metadata &mdash; identical to the pre-native-mode behavior.
153    *
154    * @param value <jk>true</jk> to enable native mode.
155    * @return This object.
156    */
157   public CborTokenReader setNativeMode(boolean value) {
158      nativeMode = value;
159      return this;
160   }
161
162   /**
163    * Sets the maximum allowed wire-declared length (in bytes) for byte/text strings read by this cursor.
164    *
165    * <p>
166    * Applies to the length caps enforced while decoding definite- and indefinite-length strings.  The
167    * cursor is intentionally NOT bounded by an element-count cap for arrays/maps &mdash; it is an
168    * O(1)-memory streaming cursor and never materializes containers, so it accepts large definite-length
169    * containers.  Values {@code <= 0} disable the maximum-length cap (the negative-length check still
170    * applies).
171    *
172    * @param value The maximum length in bytes.
173    * @return This object.
174    */
175   public CborTokenReader setMaxLength(int value) {
176      is.setMaxLength(value);
177      return this;
178   }
179
180   // ==============================================================================================
181   // State-machine summary.  CBOR is already a token-shaped wire format (RFC 8949 major types), so
182   // next() reads the next data-type tag via CborInputStream.readDataType() and dispatches based
183   // on three things: (1) whether we're at end-of-input, (2) whether a definite-length container
184   // has exhausted its element count (emit END_*), or whether the data type is BREAK (also emit
185   // END_*), and (3) within a map, whether we're at a key position or value position.
186   //
187   // Per-level container state lives on parallel stacks (stackRemaining + stackIsMap +
188   // stackAwaitingKey, indexed by depth).  A stackRemaining value of -1 means indefinite-length
189   // in that case the next() loop relies on CborInputStream returning DataType.BREAK instead of
190   // counting elements.
191   // ==============================================================================================
192   @Override /* TokenReader */
193   @SuppressWarnings({
194      "java:S3776" // Token-reader state machine; complexity is inherent to the format decode loop.
195   })
196   public TokenType next() throws IOException, ParseException {
197      if (ended) {
198         currentToken = TokenType.END_OF_STREAM;
199         return currentToken;
200      }
201      // Reset scratch.
202      currentString = null;
203      currentNumberLexeme = null;
204      currentNumber = null;
205      currentBinary = null;
206      // Native-mode metadata: only reset at outermost entry — recursive tag unwraps preserve the
207      // accumulated tag stack so nested tag(tag(value)) round-trips losslessly.
208      if (tagNestingDepth == 0) {
209         nativeKind = BinaryNativeKind.NONE;
210         tagCount = 0;
211      }
212
213      // Inside a definite-length container at element-count zero: emit END_*.
214      if (depth > 0 && stackRemaining[depth - 1] == 0) {
215         popContainer();
216         return currentToken;
217      }
218
219      DataType dt;
220      try {
221         dt = is.readDataType();
222      } catch (IOException e) {
223         if (depth == 0) {
224            ended = true;
225            currentToken = TokenType.END_OF_STREAM;
226            return currentToken;
227         }
228         throw e;
229      }
230
231      // Inside an indefinite-length container, BREAK closes it.
232      if (dt == DataType.BREAK) {
233         if (depth == 0 || stackRemaining[depth - 1] != INDEFINITE)
234            throw new ParseException("Unexpected BREAK token outside indefinite-length container");
235         popContainer();
236         return currentToken;
237      }
238
239      // Map key position: CBOR keys can be any data type, but the token vocabulary expects
240      // FIELD_NAME with a string view.  Convert string keys directly; coerce other key types
241      // to their toString() form (matches the databind path's behavior).
242      if (depth > 0 && stackIsMap[depth - 1] && stackAwaitingKey[depth - 1]) {
243         currentToken = TokenType.FIELD_NAME;
244         currentString = readScalarAsString(dt);
245         consumedOneElement();  // flips awaiting-key: key->value, decrements remaining count
246         return currentToken;
247      }
248
249      // Value position (in array, in map after key, or at root).
250      switch (dt) {
251         case ARRAY -> {
252            pushContainer(false, is.isIndefinite() ? INDEFINITE : is.readLength());
253            currentToken = TokenType.START_ARRAY;
254         }
255         case MAP -> {
256            if (is.isIndefinite()) {
257               pushContainer(true, INDEFINITE);
258            } else {
259               var n = is.readLength();
260               // MAP length is pair-count; track it as 2*n element-counts.  Guard the doubling so a
261               // malformed/overflowing count (e.g. a negative 8-byte 0xFFFFFFFFFFFFFFFF argument)
262               // cannot wrap the element-count bookkeeping negative.
263               if (n < 0 || n > Long.MAX_VALUE / 2)
264                  throw new ParseException("Invalid CBOR map length: %s", n);
265               pushContainer(true, n * 2);
266            }
267            currentToken = TokenType.START_OBJECT;
268            // First emit inside the map will be a key; pushContainer already set this
269            // for map-level entries.
270         }
271         case UINT, NINT -> {
272            var v = is.readSignedLong();
273            currentNumber = v;
274            currentNumberLexeme = Long.toString(v);
275            currentToken = TokenType.VALUE_NUMBER;
276            consumedOneElement();
277         }
278         case FLOAT -> {
279            var v = is.readDouble();
280            currentNumber = v;
281            currentNumberLexeme = Double.toString(v);
282            currentToken = TokenType.VALUE_NUMBER;
283            consumedOneElement();
284         }
285         case BOOLEAN -> {
286            currentBoolean = is.readBoolean();
287            currentToken = TokenType.VALUE_BOOLEAN;
288            consumedOneElement();
289         }
290         case NULL, UNDEFINED -> {
291            currentToken = TokenType.VALUE_NULL;
292            consumedOneElement();
293         }
294         case STRING -> {
295            currentString = is.readString();
296            currentToken = TokenType.VALUE_STRING;
297            consumedOneElement();
298         }
299         case BINARY -> {
300            currentBinary = is.readBinary();
301            currentToken = TokenType.VALUE_BINARY;
302            consumedOneElement();
303         }
304         case TAG, SIMPLE -> {
305            // Q3: by default normalize to common scalars.  TAG: skip the tag and read the
306            // next item as the wrapped value.  SIMPLE: emit as VALUE_NULL (best-effort).
307            // In native mode (175ad): TAG numbers accumulate on the tag stack across
308            // recursion; SIMPLE captures the simple int as metadata on the VALUE_NULL token.
309            if (dt == DataType.TAG) {
310               var n = is.readLength();
311               if (nativeMode) {
312                  if (tagCount == tags.length)
313                     tags = java.util.Arrays.copyOf(tags, tags.length * 2);
314                  tags[tagCount++] = n;
315                  nativeKind = BinaryNativeKind.CBOR_TAG;
316               }
317               // Guard against pathologically deep tag chains blowing the JVM stack.
318               if (tagNestingDepth >= maxTagNestingDepth)
319                  throw new ParseException(
320                     "CBOR tag nesting depth exceeded the maximum of %s.  Increase the limit via CborTokenReader.setMaxTagNestingDepth(int) if this is intentional.",
321                     maxTagNestingDepth);
322               tagNestingDepth++;
323               try {
324                  return next();  // consume the wrapped value
325               } finally {
326                  tagNestingDepth--;
327               }
328            }
329            if (nativeMode) {
330               simpleValue = (int) is.readLength();
331               nativeKind = BinaryNativeKind.CBOR_SIMPLE;
332            }
333            currentToken = TokenType.VALUE_NULL;
334            consumedOneElement();
335         }
336         default -> throw new ParseException("Unexpected CBOR data type: %s", dt);
337      }
338      return currentToken;
339   }
340
341   private String readScalarAsString(DataType dt) throws IOException, ParseException {
342      switch (dt) {
343         case STRING:    return is.readString();
344         case UINT, NINT: return Long.toString(is.readSignedLong());
345         case BINARY: {
346            var b = is.readBinary();
347            return new String(b, java.nio.charset.StandardCharsets.UTF_8);
348         }
349         case BOOLEAN:   return Boolean.toString(is.readBoolean());
350         case FLOAT:     return Double.toString(is.readDouble());
351         default:
352            throw new ParseException("Cannot use CBOR data type %s as a map key", dt);
353      }
354   }
355
356   private void pushContainer(boolean isMap, long remaining) {
357      if (depth == stackRemaining.length) {
358         var n = depth * 2;
359         var nr = new long[n];
360         var nm = new boolean[n];
361         var nk = new boolean[n];
362         System.arraycopy(stackRemaining, 0, nr, 0, depth);
363         System.arraycopy(stackIsMap, 0, nm, 0, depth);
364         System.arraycopy(stackAwaitingKey, 0, nk, 0, depth);
365         stackRemaining = nr;
366         stackIsMap = nm;
367         stackAwaitingKey = nk;
368      }
369      stackRemaining[depth] = remaining;
370      stackIsMap[depth] = isMap;
371      stackAwaitingKey[depth] = isMap;  // map starts awaiting a key
372      depth++;
373   }
374
375   private void popContainer() {
376      depth--;
377      currentToken = stackIsMap[depth] ? TokenType.END_OBJECT : TokenType.END_ARRAY;
378      // The closed container counted as one value-emit in the parent's context.
379      consumedOneElement();
380   }
381
382   @SuppressWarnings({
383      "javabugs:S6466" // The stack arrays grow with every push, so depth minus one always indexes a live slot.
384   })
385   private void consumedOneElement() {
386      if (depth == 0)
387         return;
388      var top = depth - 1;
389      if (stackRemaining[top] != INDEFINITE)
390         stackRemaining[top]--;
391      // In a map, alternate key/value positions per element.
392      if (stackIsMap[top])
393         stackAwaitingKey[top] = !stackAwaitingKey[top];
394   }
395
396   @Override /* TokenReader */
397   public TokenType getCurrentToken() {
398      return currentToken;
399   }
400
401   @Override /* TokenReader */
402   public int getDepth() {
403      return depth;
404   }
405
406   @Override /* TokenReader */
407   public String getFieldName() {
408      if (currentToken != TokenType.FIELD_NAME)
409         throw isex("Current token is not FIELD_NAME (was %s)", currentToken);
410      return currentString;
411   }
412
413   @Override /* TokenReader */
414   public String getString() {
415      switch (currentToken) {
416         case VALUE_STRING:  return currentString;
417         case FIELD_NAME:    return currentString;
418         case VALUE_NUMBER:  return currentNumberLexeme;
419         case VALUE_BOOLEAN: return Boolean.toString(currentBoolean);
420         case VALUE_NULL:    return null;
421         default: throw isex("Current token does not have a string view (was %s)", currentToken);
422      }
423   }
424
425   @Override /* TokenReader */
426   public Number getNumber() throws ParseException {
427      if (currentToken != TokenType.VALUE_NUMBER)
428         throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken);
429      return currentNumber;
430   }
431
432   @Override /* TokenReader */
433   public String getNumberLexeme() {
434      if (currentToken != TokenType.VALUE_NUMBER)
435         throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken);
436      return currentNumberLexeme;
437   }
438
439   @Override /* TokenReader */
440   public boolean getBool() {
441      if (currentToken != TokenType.VALUE_BOOLEAN)
442         throw isex("Current token is not VALUE_BOOLEAN (was %s)", currentToken);
443      return currentBoolean;
444   }
445
446   @Override /* TokenReader */
447   public byte[] getBinary() {
448      if (currentToken != TokenType.VALUE_BINARY)
449         throw isex("Current token is not VALUE_BINARY (was %s)", currentToken);
450      return cp(currentBinary);
451   }
452
453   @Override /* TokenReader */
454   public boolean canRead() throws IOException, ParseException {
455      // CBOR has no separators to advance through; the cursor is "at a value boundary" iff
456      // (a) we're not inside a definite-length container that's exhausted, and
457      // (b) inside an indefinite-length container, the next byte isn't BREAK, and
458      // (c) the stream isn't ended.
459      if (ended)
460         return false;
461      if (depth > 0) {
462         var remaining = stackRemaining[depth - 1];
463         if (remaining == 0)
464            return false;
465         if (remaining == INDEFINITE && is.peekBreak())
466            return false;
467      }
468      return true;
469   }
470
471   @Override /* TokenReader */
472   public <T> T read(Class<T> type) throws IOException, ParseException {
473      return read(session().getClassMeta(type));
474   }
475
476   @Override /* TokenReader */
477   public <T> T read(ClassMeta<T> type) throws IOException, ParseException {
478      var s = session();
479      if (!canRead())
480         throw new IllegalStateException(MSG_READ_NON_VALUE_STATE);
481      try {
482         T o = s.readAnything(type, is, null, null);
483         consumedOneElement();
484         currentToken = TokenType.NOT_AVAILABLE;
485         return o;
486      } catch (Exception e) {
487         if (e instanceof IOException e2) throw e2;
488         if (e instanceof ParseException e2) throw e2;
489         throw new ParseException(s, "read failed: %s", e.getMessage());
490      }
491   }
492
493   @Override /* TokenReader */
494   public <T> T read(Type type, Type... args) throws IOException, ParseException {
495      return read(session().<T>getClassMeta(type, args));
496   }
497
498   private CborParserSession session() {
499      if (session == null)
500         throw new UnsupportedOperationException(
501            "read is not available on this CborTokenReader (no session attached). " +
502            "Open the cursor via CborParser.readTokens(...) to enable read.");
503      return session;
504   }
505
506   @Override /* TokenReader */
507   public void skipChildren() throws IOException, ParseException {
508      if (currentToken != TokenType.START_OBJECT && currentToken != TokenType.START_ARRAY)
509         return;
510      var startDepth = depth - 1;
511      while (depth > startDepth) {
512         var t = next();
513         if (t == TokenType.END_OF_STREAM)
514            throw new ParseException("Unexpected end of input while skipping children");
515      }
516   }
517
518   @Override /* TokenReader */
519   public boolean isStreaming() {
520      return true;
521   }
522
523   @Override /* TokenReader */
524   public void close() throws IOException {
525      pipe.close();
526   }
527
528   @Override /* TokenReader */
529   public BinaryNativeKind getNativeKind() {
530      return nativeKind;
531   }
532
533   @Override /* TokenReader */
534   public int getTagCount() {
535      return tagCount;
536   }
537
538   @Override /* TokenReader */
539   public long getTag(int index) {
540      if (index < 0 || index >= tagCount)
541         throw new IndexOutOfBoundsException("Tag index " + index + " out of bounds for tagCount " + tagCount);
542      return tags[index];
543   }
544
545   @Override /* TokenReader */
546   public int getExtType() {
547      throw new IllegalStateException("CBOR cursor has no MsgPack ext type.");
548   }
549
550   @Override /* TokenReader */
551   public int getSimpleValue() {
552      if (nativeKind != BinaryNativeKind.CBOR_SIMPLE)
553         throw isex("Current token is not a CBOR simple value (nativeKind=%s)", nativeKind);
554      return simpleValue;
555   }
556}