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.*;
020import static org.apache.juneau.marshall.cbor.DataType.*;
021
022import java.io.*;
023import java.math.*;
024import java.util.*;
025
026import org.apache.juneau.commons.bean.*;
027import org.apache.juneau.commons.reflect.*;
028import org.apache.juneau.marshall.*;
029import org.apache.juneau.marshall.parser.*;
030import org.apache.juneau.marshall.stream.*;
031import org.apache.juneau.marshall.swap.spi.*;
032
033/**
034 * Session object that lives for the duration of a single use of {@link CborParser}.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
038 * </ul>
039 *
040 * <h5 class='section'>See Also:</h5><ul>
041 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Cbor">CBOR</a>
042 * </ul>
043 */
044@SuppressWarnings({
045   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
046   "rawtypes", // Raw types necessary for generic type handling
047   "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
048   "unchecked" // Type erasure requires unchecked casts
049})
050public class CborParserSession extends InputStreamParserSession implements TokenReadable, ArrayRecordReadable {
051
052   /**
053    * Builder class.
054    */
055   public static class Builder extends InputStreamParserSession.Builder<Builder> {
056
057      final boolean nativeMode;
058      final int maxLength;
059
060      /**
061       * Constructor
062       *
063       * @param ctx The context creating this session.
064       *    <br>Cannot be <jk>null</jk>.
065       */
066      protected Builder(CborParser ctx) {
067         super(reqnn("ctx", ctx));
068         this.nativeMode = ctx.isNativeMode();
069         this.maxLength = ctx.getMaxLength();
070      }
071
072      @Override
073      public CborParserSession build() {
074         return new CborParserSession(this);
075      }
076
077   }
078
079   /**
080    * Creates a new builder for this object.
081    *
082    * @param ctx The context creating this session.
083    *    <br>Cannot be <jk>null</jk>.
084    * @return A new builder.
085    */
086   public static Builder create(CborParser ctx) {
087      return new Builder(reqnn("ctx", ctx));
088   }
089
090   private final boolean nativeMode;
091   private final int maxLength;
092
093   /**
094    * Constructor.
095    *
096    * @param builder The builder for this object.
097    */
098   protected CborParserSession(Builder builder) {
099      super(builder);
100      this.nativeMode = builder.nativeMode;
101      this.maxLength = builder.maxLength;
102   }
103
104   /**
105    * Opens a low-level pull-parser cursor over a CBOR document, bound to this live session.
106    *
107    * <p>
108    * CBOR is already a token-shaped wire format (RFC 8949), so this cursor maps almost directly
109    * onto {@link CborInputStream#readDataType()}.  Indefinite-length containers
110    * (<c>0x9F</c> / <c>0xBF</c>) are handled by tracking a sentinel element-count of -1 on the
111    * container stack and emitting END_* on encountering the BREAK byte.
112    *
113    * <h5 class='section'>Builder properties honored:</h5>
114    * <c>autoCloseStreams</c>, <c>unbuffered</c>.
115    *
116    * <h5 class='section'>Builder properties NOT honored:</h5>
117    * <c>listener</c>, <c>debugOutputLines</c>, <c>consumes</c>, swaps, {@code @Schema}.
118    *
119    * @param input The input.
120    * @return A new {@link CborTokenReader}.
121    * @throws IOException If a problem occurred opening the underlying input.
122    */
123   @SuppressWarnings({
124      "java:S2095" // ParserPipe lifecycle is transferred to the returned CborTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources.
125   })
126   @Override /* TokenReadable */
127   public TokenReader readTokens(Object input) throws IOException {
128      var pipe = new ParserPipe(input, isDebug(), isAutoCloseStreams(), isUnbuffered(), null);
129      return new CborTokenReader(pipe, this).setNativeMode(nativeMode).setMaxLength(maxLength);
130   }
131
132   /**
133    * Streaming array-element {@link RecordReader} backed by {@link CborTokenReader}.
134    * Memory is O(1) in the array length &mdash; CBOR's indefinite-length array support
135    * (0x9F .. 0xFF) makes the wrapper trivial.
136    *
137    * @param input The input.
138    * @return A new element-streamed {@link RecordReader}.
139    * @throws IOException If a problem occurred opening the underlying input.
140    */
141   @Override /* ArrayRecordReadable */
142   public RecordReader readArrayRecords(Object input) throws IOException {
143      try {
144         return StreamingArrayRecord.reader(readTokens(input));
145      } catch (ParseException e) {
146         throw new IOException(e);
147      }
148   }
149
150   /*
151    * Workhorse entry point.  Wraps readAnything0 with the shared ParserSession recursion-depth guard so that
152    * an adversarial deeply-nested document fails with a ParseException instead of a StackOverflowError.
153    */
154   <T> T readAnything(ClassMeta<?> eType, CborInputStream is, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
155      enterParseDepth();
156      try {
157         return readAnything0(eType, is, outer, pMeta);
158      } finally {
159         exitParseDepth();
160      }
161   }
162
163   /*
164    * Workhorse method.
165    */
166   @SuppressWarnings({
167      "java:S3776", // Cognitive complexity acceptable for this specific logic
168      "java:S6541" // Single-threaded session contexts do not require synchronization
169   })
170   private <T> T readAnything0(ClassMeta<?> eType, CborInputStream is, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
171
172      if (eType == null)
173         eType = object();
174      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
175      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
176      ClassMeta<?> sType = null;
177      if (nn(builder))
178         sType = builder.getBuilderClassMeta(this);
179      else if (nn(swap))
180         sType = swap.getSwapClassMeta(this);
181      else
182         sType = eType;
183
184      if (sType.isOptional())
185         return (T)o(readAnything(eType.getElementType(), is, outer, pMeta));
186
187      setCurrentClass(sType);
188
189      Object o = null;
190      DataType dt = is.readDataType();
191      var indefinite = is.isIndefinite();
192      long len = indefinite ? -1 : is.readLength();
193
194      // Handle CBOR semantic tags: skip tag, parse following data item
195      while (dt == TAG) {
196         dt = is.readDataType();
197         indefinite = is.isIndefinite();
198         len = indefinite ? -1 : is.readLength();
199      }
200
201      // Bound a definite array/map element count up front.  This is the databind path (elements
202      // accumulate into a collection below); the O(1)-memory streaming cursor is intentionally exempt so
203      // it can still emit tokens for large definite-length containers.  A definite container whose
204      // declared count is negative (e.g. an 8-byte 0xFFFFFFFFFFFFFFFF argument) is rejected here too.
205      if ((dt == ARRAY || dt == MAP) && !indefinite)
206         is.checkLength(len, dt == ARRAY ? "array" : "map");
207
208      if (dt != NULL) {
209         if (dt == BOOLEAN)
210            o = is.readBoolean();
211         else if (dt == UINT || dt == NINT) {
212            // Surface unsigned 64-bit by the target Java type (175fc GAP-1, aligned with the
213            // proto-binary R5 decision): a BigInteger field carries the full magnitude (values
214            // beyond signed long widen losslessly); any other type keeps the raw 64-bit bits as
215            // a long (preserving the native int64 round-trip used by epoch/millis temporal and
216            // duration values).
217            o = sType.inner() == BigInteger.class ? is.readBigInteger() : (Object)is.readLong();
218         }
219         else if (dt == FLOAT) {
220            // Prefer double for conversion; single-precision if target is Float
221            if (sType.isFloat() && !sType.isDouble())
222               o = is.readFloat();
223            else
224               o = is.readDouble();
225         } else if (dt == STRING)
226            o = trim(is.readString());
227         else if (dt == BINARY)
228            o = is.readBinary();
229         else if (dt == ARRAY && sType.isObject()) {
230            var jl = newGenericList();
231            for (var i = 0; shouldContinueContainer(is, len, i); i++)
232               jl.add(readAnything(object(), is, outer, pMeta));
233            o = jl;
234         } else if (dt == MAP && sType.isObject()) {
235            var jm = newGenericMap();
236            for (var i = 0; shouldContinueContainer(is, len, i); i++)
237               jm.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, jm, pMeta));
238            o = cast(jm, pMeta, eType);
239         }
240
241         if (sType.isObject()) {
242            // Do nothing.
243         } else if (nn(builder) || sType.canCreateNewBean(outer)) {
244            if (dt == MAP) {
245               BeanMap m = builder == null ? newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
246               var pb = swapParentBean(m.getBean(false));
247               try {
248                  for (var i = 0; shouldContinueContainer(is, len, i); i++) {
249                     String pName = readAnything(string(), is, m.getBean(false), null);
250                     var bpm = m.getPropertyMeta(pName);
251                     if (bpm == null) {
252                        if (pName.equals(getBeanTypePropertyName(eType)))
253                           readAnything(string(), is, null, null);
254                        else
255                           onUnknownProperty(pName, m, readAnything(string(), is, null, null));
256                     } else {
257                        var cm = (ClassMeta<?>) bpm.getBeanInfo();
258                        Object value = readAnything(cm, is, m.getBean(false), bpm);
259                        setName(cm, value, pName);
260                        try {
261                           bpm.set(m, pName, value);
262                        } catch (BeanRuntimeException e) {
263                           onBeanSetterException(pMeta, e);
264                           throw e;
265                        }
266                     }
267                  }
268               } finally {
269                  swapParentBean(pb);
270               }
271               o = builder == null ? checkRequired(m).getBean() : builder.build(this, checkRequired(m).getBean(), eType);
272            } else {
273               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
274            }
275         } else if (sType.isMap()) {
276            if (dt == MAP) {
277               Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
278               for (var i = 0; shouldContinueContainer(is, len, i); i++) {
279                  Object key = readAnything(sType.getKeyType(), is, outer, pMeta);
280                  var vt = sType.getValueType();
281                  Object value = readAnything(vt, is, m, pMeta);
282                  setName(vt, value, key);
283                  m.put(key, value);
284               }
285               o = m;
286            } else {
287               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
288            }
289         } else if (sType.isBoolean() || sType.isCharSequence() || sType.isChar() || sType.isNumber() || sType.isByteArray()) {
290            // Merged scalar tier: one CBOR data type (BOOLEAN/UINT/NINT/FLOAT/STRING/BINARY) covers
291            // many scalar Java types — the read already happened above and convertToType narrows.
292            // A byte[]-targeted string element is a BinaryFormat-encoded payload (mirroring the
293            // serializer) rather than a literal string - decode it back to bytes directly, since
294            // the generic String-to-byte[] conversion doesn't know about BinaryFormat.
295            var binaryFormat = getBinaryFormat();
296            if (dt == STRING && sType.isByteArray() && binaryFormat != BinaryFormat.NOT_SET)
297               o = binaryFormat.parse((String)o);
298            else
299               o = convertToType(o, sType);
300         } else if (sType.isDate()) {
301            o = readDate(String.valueOf(o), sType);
302         } else if (sType.isCalendar()) {
303            o = readCalendar(String.valueOf(o), sType);
304         } else if (sType.isTemporal()) {
305            o = readTemporal(String.valueOf(o), sType);
306         } else if (sType.isDuration()) {
307            o = readDuration(String.valueOf(o));
308         } else if (sType.isPeriod()) {
309            o = readPeriod(String.valueOf(o));
310         } else if (sType.canCreateNewInstanceFromString(outer) && dt == STRING) {
311            o = sType.newInstanceFromString(outer, o == null ? "" : o.toString());
312         } else if (sType.isCollection()) {
313            if (dt == MAP) {
314               var m = newGenericMap();
315               for (var i = 0; shouldContinueContainer(is, len, i); i++)
316                  m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
317               o = cast(m, pMeta, eType);
318            } else if (dt == ARRAY) {
319               Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
320               for (var i = 0; shouldContinueContainer(is, len, i); i++)
321                  l.add(readAnything(sType.getElementType(), is, l, pMeta));
322               o = l;
323            } else {
324               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
325            }
326         } else if (sType.isArray() || sType.isArgs()) {
327            if (dt == MAP) {
328               var m = newGenericMap();
329               for (var i = 0; shouldContinueContainer(is, len, i); i++)
330                  m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
331               o = cast(m, pMeta, eType);
332            } else if (dt == ARRAY) {
333               Collection l = (sType.isCollection() && sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
334               for (var i = 0; shouldContinueContainer(is, len, i); i++)
335                  l.add(readAnything(sType.isArgs() ? sType.getArg(i) : sType.getElementType(), is, l, pMeta));
336               o = toArray(sType, l);
337            } else {
338               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
339            }
340         } else if (dt == MAP) {
341            var m = newGenericMap();
342            for (var i = 0; shouldContinueContainer(is, len, i); i++)
343               m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
344            if (m.containsKey(getBeanTypePropertyName(eType)))
345               o = cast(m, pMeta, eType);
346            else if (nn(sType.getProxyInvocationHandler()))
347               o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
348            else
349               throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
350         } else if (dt == UNDEFINED || dt == SIMPLE) {
351            // Treat as null for unknown simple values
352            o = null;
353         } else {
354            throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
355         }
356      }
357
358      if (nn(swap) && nn(o))
359         o = unswap(swap, o, eType);
360
361      if (nn(parentBean()))
362         setParent(eType, o, parentBean());
363
364      return (T)o;
365   }
366
367   @Override /* Overridden from ParserSession */
368   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
369      try (CborInputStream is = new CborInputStream(pipe)) {
370         is.setMaxLength(maxLength);
371         return readAnything(type, is, getOuter(), null);
372      }
373   }
374
375   /**
376    * Returns whether the next iteration of a CBOR container loop should run.
377    *
378    * <p>
379    * For definite-length containers (<c>len &gt;= 0</c>) returns <jk>true</jk> while
380    * <c>i &lt; len</c>.  For indefinite-length containers (<c>len == -1</c>) peeks for the BREAK
381    * marker and returns <jk>false</jk> when found (consuming the BREAK byte), <jk>true</jk>
382    * otherwise.  In the indefinite case the accumulated element count is bounded by the configured
383    * maximum so a never-terminating stream of elements cannot grow the collection without limit.
384    */
385   private static boolean shouldContinueContainer(CborInputStream is, long len, int i) throws IOException {
386      if (len == -1) {
387         is.checkLength(i, "container");
388         return !is.peekBreak();
389      }
390      return i < len;
391   }
392}