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 — 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 >= 0</c>) returns <jk>true</jk> while 380 * <c>i < 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}