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 — 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 — 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}