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.msgpack; 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 MessagePack format. 030 * 031 * <p> 032 * Backed by the existing internal {@link MsgPackInputStream} plumbing. MessagePack containers 033 * are length-prefixed; this cursor tracks the remaining-element-count per level on a stack and 034 * emits END_* when the count is exhausted. 035 * 036 * <h5 class='section'>Notes:</h5><ul> 037 * <li class='warn'>This class is not thread safe. 038 * <li>The cursor is a true O(1)-memory streaming cursor ({@link #isStreaming()} == <jk>true</jk>). 039 * </ul> 040 */ 041@SuppressWarnings({ 042 "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. 043}) 044public class MsgPackTokenReader implements TokenReader { 045 046 private final MsgPackInputStream is; 047 private final ParserPipe pipe; 048 private final MsgPackParserSession session; 049 050 private TokenType currentToken = TokenType.NOT_AVAILABLE; 051 private boolean ended; 052 053 // Container stack (parallel arrays). 054 private long[] stackRemaining = new long[16]; 055 private boolean[] stackIsMap = new boolean[16]; 056 private boolean[] stackAwaitingKey = new boolean[16]; 057 private int depth; 058 059 // Scratch for current value. 060 private String currentString; 061 private String currentNumberLexeme; 062 private Number currentNumber; 063 private boolean currentBoolean; 064 private byte[] currentBinary; 065 066 // Binary-native opt-in state (175ad). Defaults preserve the normalize-by-default contract: 067 // EXT payloads surface as VALUE_BINARY with no metadata. When nativeMode is true, the EXT 068 // branch additionally captures the signed type byte into 'extType' and sets 069 // 'nativeKind' = MSGPACK_EXT. 070 private boolean nativeMode; 071 private BinaryNativeKind nativeKind = BinaryNativeKind.NONE; 072 private int extType; 073 074 /** 075 * Constructor with default settings. 076 * 077 * @param pipe The parser input pipe. Must not be <jk>null</jk>. 078 * @throws IOException If a problem occurred opening the underlying stream. 079 */ 080 public MsgPackTokenReader(ParserPipe pipe) throws IOException { 081 this(pipe, null); 082 } 083 084 /** 085 * Constructor used by {@link MsgPackParserSession#readTokens(Object)} to plumb the calling 086 * session through. 087 * 088 * @param pipe The parser input pipe. Must not be <jk>null</jk>. 089 * @param session The {@link MsgPackParserSession} for {@link #read(Class)} delegation, 090 * or <jk>null</jk> to disable {@code read}. 091 * @throws IOException If a problem occurred opening the underlying stream. 092 */ 093 public MsgPackTokenReader(ParserPipe pipe, MsgPackParserSession session) throws IOException { 094 this.pipe = pipe; 095 this.is = new MsgPackInputStream(pipe); 096 this.session = session; 097 } 098 099 /** 100 * Enables or disables binary-native opt-in mode. 101 * 102 * <p> 103 * When enabled, MsgPack {@code ext} tokens carry the signed type byte via 104 * {@link #getExtType()}. When disabled (default), the payload still surfaces as 105 * {@link TokenType#VALUE_BINARY} but the type byte is dropped — identical to the 106 * pre-native-mode behavior. 107 * 108 * @param value <jk>true</jk> to enable native mode. 109 * @return This object. 110 */ 111 public MsgPackTokenReader setNativeMode(boolean value) { 112 nativeMode = value; 113 return this; 114 } 115 116 /** 117 * Sets the maximum allowed wire-declared length (in bytes) for binary/string payloads read by this cursor. 118 * 119 * <p> 120 * The cursor is intentionally NOT bounded by an element-count cap for arrays/maps — it is an 121 * O(1)-memory streaming cursor and never materializes containers, so it accepts large length-prefixed 122 * containers. Values {@code <= 0} disable the maximum-length cap (the negative-length check still 123 * applies). 124 * 125 * @param value The maximum length in bytes. 126 * @return This object. 127 */ 128 public MsgPackTokenReader setMaxLength(int value) { 129 is.setMaxLength(value); 130 return this; 131 } 132 133 // ============================================================================================== 134 // State-machine summary. MsgPack containers are length-prefixed (no indefinite-length 135 // encoding), so next() reads the data-type tag via MsgPackInputStream.readDataType() and 136 // dispatches based on: (1) whether we're at end-of-input, (2) whether the current container's 137 // remaining element count has reached zero (emit END_*), and (3) within a map, whether we're 138 // at a key position or value position. 139 // 140 // Per-level container state lives on parallel stacks (stackRemaining + stackIsMap + 141 // stackAwaitingKey, indexed by depth). Each emit decrements stackRemaining[top]; map-level 142 // emits also flip stackAwaitingKey[top] between key and value positions. 143 // ============================================================================================== 144 @Override /* TokenReader */ 145 public TokenType next() throws IOException, ParseException { 146 if (ended) { 147 currentToken = TokenType.END_OF_STREAM; 148 return currentToken; 149 } 150 currentString = null; 151 currentNumberLexeme = null; 152 currentNumber = null; 153 currentBinary = null; 154 nativeKind = BinaryNativeKind.NONE; 155 156 // Inside a container at element-count zero: emit END_*. 157 if (depth > 0 && stackRemaining[depth - 1] == 0) { 158 popContainer(); 159 return currentToken; 160 } 161 162 DataType dt; 163 try { 164 dt = is.readDataType(); 165 } catch (IOException e) { 166 if (depth == 0) { 167 ended = true; 168 currentToken = TokenType.END_OF_STREAM; 169 return currentToken; 170 } 171 throw e; 172 } 173 174 // Map key position: emit FIELD_NAME. 175 if (depth > 0 && stackIsMap[depth - 1] && stackAwaitingKey[depth - 1]) { 176 currentToken = TokenType.FIELD_NAME; 177 currentString = readScalarAsString(dt); 178 consumedOneElement(); 179 return currentToken; 180 } 181 182 switch (dt) { 183 case ARRAY -> { 184 pushContainer(false, is.readLength()); 185 currentToken = TokenType.START_ARRAY; 186 } 187 case MAP -> { 188 var n = is.readLength(); 189 // Pair-count -> element-count. Guard the doubling so a malformed/overflowing count cannot 190 // wrap the element-count bookkeeping negative (the streaming cursor is intentionally exempt 191 // from the maxLength element-count cap, which is enforced on the databind path). 192 if (n < 0 || n > Long.MAX_VALUE / 2) 193 throw new ParseException("Invalid MessagePack map length: %s", n); 194 pushContainer(true, n * 2); 195 currentToken = TokenType.START_OBJECT; 196 } 197 case INT -> { 198 var v = (long) is.readInt(); 199 currentNumber = v; 200 currentNumberLexeme = Long.toString(v); 201 currentToken = TokenType.VALUE_NUMBER; 202 consumedOneElement(); 203 } 204 case LONG -> { 205 var v = is.readLong(); 206 currentNumber = v; 207 currentNumberLexeme = Long.toString(v); 208 currentToken = TokenType.VALUE_NUMBER; 209 consumedOneElement(); 210 } 211 case FLOAT -> { 212 var v = (double) is.readFloat(); 213 currentNumber = v; 214 currentNumberLexeme = Double.toString(v); 215 currentToken = TokenType.VALUE_NUMBER; 216 consumedOneElement(); 217 } 218 case DOUBLE -> { 219 var v = is.readDouble(); 220 currentNumber = v; 221 currentNumberLexeme = Double.toString(v); 222 currentToken = TokenType.VALUE_NUMBER; 223 consumedOneElement(); 224 } 225 case BOOLEAN -> { 226 currentBoolean = is.readBoolean(); 227 currentToken = TokenType.VALUE_BOOLEAN; 228 consumedOneElement(); 229 } 230 case NULL -> { 231 currentToken = TokenType.VALUE_NULL; 232 consumedOneElement(); 233 } 234 case STRING -> { 235 currentString = is.readString(); 236 currentToken = TokenType.VALUE_STRING; 237 consumedOneElement(); 238 } 239 case BIN -> { 240 currentBinary = is.readBinary(); 241 currentToken = TokenType.VALUE_BINARY; 242 consumedOneElement(); 243 } 244 case EXT -> { 245 // Q3 default: normalize to common scalars; the extension payload surfaces as 246 // VALUE_BINARY. In native mode (175ad), the signed ext type byte is also 247 // captured for retrieval via getExtType(). 248 if (nativeMode) { 249 extType = (byte) is.getExtType(); 250 nativeKind = BinaryNativeKind.MSGPACK_EXT; 251 } 252 currentBinary = is.readBinary(); 253 currentToken = TokenType.VALUE_BINARY; 254 consumedOneElement(); 255 } 256 default -> throw new ParseException("Unexpected MsgPack data type: %s", dt); 257 } 258 return currentToken; 259 } 260 261 private String readScalarAsString(DataType dt) throws IOException, ParseException { 262 switch (dt) { 263 case STRING: return is.readString(); 264 case INT: return Long.toString(is.readInt()); 265 case LONG: return Long.toString(is.readLong()); 266 case BIN: { 267 var b = is.readBinary(); 268 return new String(b, java.nio.charset.StandardCharsets.UTF_8); 269 } 270 case BOOLEAN: return Boolean.toString(is.readBoolean()); 271 case FLOAT: return Float.toString(is.readFloat()); 272 case DOUBLE: return Double.toString(is.readDouble()); 273 default: 274 throw new ParseException("Cannot use MsgPack data type %s as a map key", dt); 275 } 276 } 277 278 private void pushContainer(boolean isMap, long remaining) { 279 if (depth == stackRemaining.length) { 280 var n = depth * 2; 281 var nr = new long[n]; 282 var nm = new boolean[n]; 283 var nk = new boolean[n]; 284 System.arraycopy(stackRemaining, 0, nr, 0, depth); 285 System.arraycopy(stackIsMap, 0, nm, 0, depth); 286 System.arraycopy(stackAwaitingKey, 0, nk, 0, depth); 287 stackRemaining = nr; 288 stackIsMap = nm; 289 stackAwaitingKey = nk; 290 } 291 stackRemaining[depth] = remaining; 292 stackIsMap[depth] = isMap; 293 stackAwaitingKey[depth] = isMap; 294 depth++; 295 } 296 297 private void popContainer() { 298 depth--; 299 currentToken = stackIsMap[depth] ? TokenType.END_OBJECT : TokenType.END_ARRAY; 300 consumedOneElement(); 301 } 302 303 private void consumedOneElement() { 304 if (depth == 0) 305 return; 306 var top = depth - 1; 307 stackRemaining[top]--; 308 if (stackIsMap[top]) 309 stackAwaitingKey[top] = !stackAwaitingKey[top]; 310 } 311 312 @Override /* TokenReader */ 313 public TokenType getCurrentToken() { return currentToken; } 314 315 @Override /* TokenReader */ 316 public int getDepth() { return depth; } 317 318 @Override /* TokenReader */ 319 public String getFieldName() { 320 if (currentToken != TokenType.FIELD_NAME) 321 throw isex("Current token is not FIELD_NAME (was %s)", currentToken); 322 return currentString; 323 } 324 325 @Override /* TokenReader */ 326 public String getString() { 327 switch (currentToken) { 328 case VALUE_STRING: return currentString; 329 case FIELD_NAME: return currentString; 330 case VALUE_NUMBER: return currentNumberLexeme; 331 case VALUE_BOOLEAN: return Boolean.toString(currentBoolean); 332 case VALUE_NULL: return null; 333 default: throw isex("Current token does not have a string view (was %s)", currentToken); 334 } 335 } 336 337 @Override /* TokenReader */ 338 public Number getNumber() throws ParseException { 339 if (currentToken != TokenType.VALUE_NUMBER) 340 throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken); 341 return currentNumber; 342 } 343 344 @Override /* TokenReader */ 345 public String getNumberLexeme() { 346 if (currentToken != TokenType.VALUE_NUMBER) 347 throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken); 348 return currentNumberLexeme; 349 } 350 351 @Override /* TokenReader */ 352 public boolean getBool() { 353 if (currentToken != TokenType.VALUE_BOOLEAN) 354 throw isex("Current token is not VALUE_BOOLEAN (was %s)", currentToken); 355 return currentBoolean; 356 } 357 358 @Override /* TokenReader */ 359 public byte[] getBinary() { 360 if (currentToken != TokenType.VALUE_BINARY) 361 throw isex("Current token is not VALUE_BINARY (was %s)", currentToken); 362 return cp(currentBinary); 363 } 364 365 @Override /* TokenReader */ 366 public boolean canRead() throws IOException, ParseException { 367 return !ended && !(depth > 0 && stackRemaining[depth - 1] == 0); 368 } 369 370 @Override /* TokenReader */ 371 public <T> T read(Class<T> type) throws IOException, ParseException { 372 return read(session().getClassMeta(type)); 373 } 374 375 @Override /* TokenReader */ 376 public <T> T read(ClassMeta<T> type) throws IOException, ParseException { 377 var s = session(); 378 if (!canRead()) 379 throw new IllegalStateException(MSG_READ_NON_VALUE_STATE); 380 try { 381 T o = s.readAnything(type, is, null, null); 382 consumedOneElement(); 383 currentToken = TokenType.NOT_AVAILABLE; 384 return o; 385 } catch (Exception e) { 386 if (e instanceof IOException e2) throw e2; 387 if (e instanceof ParseException e2) throw e2; 388 throw new ParseException(s, "read failed: %s", e.getMessage()); 389 } 390 } 391 392 @Override /* TokenReader */ 393 public <T> T read(Type type, Type... args) throws IOException, ParseException { 394 return read(session().<T>getClassMeta(type, args)); 395 } 396 397 private MsgPackParserSession session() { 398 if (session == null) 399 throw new UnsupportedOperationException( 400 "read is not available on this MsgPackTokenReader (no session attached). " + 401 "Open the cursor via MsgPackParser.readTokens(...) to enable read."); 402 return session; 403 } 404 405 @Override /* TokenReader */ 406 public void skipChildren() throws IOException, ParseException { 407 if (currentToken != TokenType.START_OBJECT && currentToken != TokenType.START_ARRAY) 408 return; 409 var startDepth = depth - 1; 410 while (depth > startDepth) { 411 var t = next(); 412 if (t == TokenType.END_OF_STREAM) 413 throw new ParseException("Unexpected end of input while skipping children"); 414 } 415 } 416 417 @Override /* TokenReader */ 418 public boolean isStreaming() { return true; } 419 420 @Override /* TokenReader */ 421 public void close() throws IOException { pipe.close(); } 422 423 @Override /* TokenReader */ 424 public BinaryNativeKind getNativeKind() { 425 return nativeKind; 426 } 427 428 @Override /* TokenReader */ 429 public int getTagCount() { 430 return 0; 431 } 432 433 @Override /* TokenReader */ 434 public long getTag(int index) { 435 throw new IndexOutOfBoundsException("Tag index " + index + " out of bounds for tagCount 0"); 436 } 437 438 @Override /* TokenReader */ 439 public int getExtType() { 440 if (nativeKind != BinaryNativeKind.MSGPACK_EXT) 441 throw isex("Current token is not a MsgPack ext (nativeKind=%s)", nativeKind); 442 return extType; 443 } 444 445 @Override /* TokenReader */ 446 public int getSimpleValue() { 447 throw new IllegalStateException("MsgPack cursor has no CBOR simple value."); 448 } 449}