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.IoUtils.*;
020
021import java.io.*;
022import java.math.*;
023import java.util.concurrent.atomic.*;
024
025import org.apache.juneau.marshall.serializer.*;
026
027/**
028 * Specialized output stream for serializing CBOR streams (RFC 8949).
029 *
030 * <p>
031 * CBOR uses a self-describing binary format where each data item starts with an initial byte
032 * containing a 3-bit major type and 5-bit additional information. Integers use compact encoding;
033 * strings, arrays, and maps use definite-length encoding.
034 *
035 * <h5 class='section'>Notes:</h5><ul>
036 *    <li class='note'>This class is not intended for external use.
037 *    <li class='note'>This class is not thread-safe; each serialization session uses its own instance.
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 *    <li class='link'><a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8949.html">RFC 8949</a>
043 * </ul>
044 */
045@SuppressWarnings({
046   "java:S127", // getUtf8ByteLength and writeUtf8To advance i past the low surrogate of a surrogate pair inside the loop
047   "resource" // appendXxx(...) methods return this stream for chaining; Eclipse JDT @Owning warning is by design.
048})
049public class CborOutputStream extends OutputStream {
050
051   private final OutputStream os;
052
053   /**
054    * Constructor.
055    *
056    * @param os The output stream being wrapped.
057    */
058   protected CborOutputStream(OutputStream os) {
059      this.os = os;
060   }
061
062   @Override /* Overridden from OutputStream */
063   public void write(int b) {
064      try {
065         os.write(b);
066      } catch (IOException e) {
067         throw new SerializeException(e);
068      }
069   }
070
071   @Override /* Overridden from OutputStream */
072   public void write(byte[] b, int off, int len) {
073      try {
074         os.write(b, off, len);
075      } catch (IOException e) {
076         throw new SerializeException(e);
077      }
078   }
079
080   /**
081    * Writes a single byte to the stream.
082    *
083    * @param b The byte to write (lower 8 bits used).
084    */
085   void write1(int b) {
086      try {
087         os.write(b & 0xFF);
088      } catch (IOException e) {
089         throw new SerializeException(e);
090      }
091   }
092
093   /**
094    * Writes a 2-byte big-endian unsigned integer.
095    *
096    * @param value The value to write.
097    */
098   void write2(int value) {
099      write1(value >> 8);
100      write1(value);
101   }
102
103   /**
104    * Writes a 4-byte big-endian unsigned integer.
105    *
106    * @param value The value to write.
107    */
108   void write4(int value) {
109      write1(value >> 24);
110      write1(value >> 16);
111      write1(value >> 8);
112      write1(value);
113   }
114
115   /**
116    * Writes an 8-byte big-endian unsigned integer.
117    *
118    * @param value The value to write.
119    */
120   void write8(long value) {
121      write1((int)(value >> 56));
122      write1((int)(value >> 48));
123      write1((int)(value >> 40));
124      write1((int)(value >> 32));
125      write1((int)(value >> 24));
126      write1((int)(value >> 16));
127      write1((int)(value >> 8));
128      write1((int)(value));
129   }
130
131   /**
132    * Encodes and writes a CBOR initial byte with major type and argument.
133    *
134    * <p>
135    * Argument encoding per RFC 8949:
136    * <ul>
137    *    <li>0-23: Single byte (majorType &lt;&lt; 5 | argument)
138    *    <li>24-255: 2 bytes (type | 24, 1-byte argument)
139    *    <li>256-65535: 3 bytes (type | 25, 2-byte argument)
140    *    <li>65536-2^32-1: 5 bytes (type | 26, 4-byte argument)
141    *    <li>2^32 and above: 9 bytes (type | 27, 8-byte argument)
142    * </ul>
143    *
144    * @param majorType The CBOR major type (0-7).
145    * @param argument The additional information or length value.
146    */
147   void writeHead(int majorType, long argument) {
148      int typeBits = (majorType & 0x07) << 5;
149      if (argument <= 23)
150         write1(typeBits | (int)argument);
151      else if (argument <= 0xFF) {
152         write1(typeBits | 24);
153         write1((int)argument);
154      } else if (argument <= 0xFFFF) {
155         write1(typeBits | 25);
156         write2((int)argument);
157      } else if (argument <= 0xFFFFFFFFL) {
158         write1(typeBits | 26);
159         write4((int)argument);
160      } else {
161         write1(typeBits | 27);
162         write8(argument);
163      }
164   }
165
166   /**
167    * Appends CBOR null (0xF6).
168    *
169    * @return This stream.
170    */
171   CborOutputStream appendNull() {
172      write1(0xF6);
173      return this;
174   }
175
176   /**
177    * Appends a boolean value (0xF4 for false, 0xF5 for true).
178    *
179    * @param value The boolean value.
180    * @return This stream.
181    */
182   CborOutputStream appendBoolean(boolean value) {
183      write1(value ? 0xF5 : 0xF4);
184      return this;
185   }
186
187   /**
188    * Appends an integer using CBOR compact encoding.
189    * Positive values use major type 0; negative values use major type 1 with argument -1-value.
190    *
191    * @param value The integer value.
192    * @return This stream.
193    */
194   CborOutputStream appendInt(int value) {
195      if (value >= 0)
196         writeHead(0, value & 0xFFFFFFFFL);
197      else
198         writeHead(1, -1L - value);
199      return this;
200   }
201
202   /**
203    * Appends a long integer using CBOR compact encoding.
204    *
205    * @param value The long value.
206    * @return This stream.
207    */
208   CborOutputStream appendLong(long value) {
209      if (value >= 0)
210         writeHead(0, value);
211      else
212         writeHead(1, -1L - value);
213      return this;
214   }
215
216   /**
217    * Appends a float as IEEE 754 single-precision (0xFA + 4 bytes).
218    *
219    * @param value The float value.
220    * @return This stream.
221    */
222   CborOutputStream appendFloat(float value) {
223      write1(0xFA);
224      write4(Float.floatToRawIntBits(value));
225      return this;
226   }
227
228   /**
229    * Appends a double as IEEE 754 double-precision (0xFB + 8 bytes).
230    *
231    * @param value The double value.
232    * @return This stream.
233    */
234   CborOutputStream appendDouble(double value) {
235      write1(0xFB);
236      write8(Double.doubleToRawLongBits(value));
237      return this;
238   }
239
240   /**
241    * Appends a generic Number, dispatching to the appropriate append method.
242    *
243    * @param n The number.
244    * @return This stream.
245    */
246   CborOutputStream appendNumber(Number n) {
247      var c = n.getClass();
248      if (c == Integer.class || c == Short.class || c == Byte.class || c == AtomicInteger.class)
249         return appendInt(n.intValue());
250      if (c == Long.class || c == AtomicLong.class)
251         return appendLong(n.longValue());
252      if (c == Float.class)
253         return appendFloat(n.floatValue());
254      if (c == Double.class)
255         return appendDouble(n.doubleValue());
256      if (c == BigInteger.class)
257         return appendBigInteger((BigInteger)n);
258      if (c == BigDecimal.class)
259         return appendString(n.toString());
260      return appendInt(0);
261   }
262
263   /**
264    * Appends a {@link BigInteger} losslessly.
265    *
266    * <p>
267    * Values within the signed {@code long} range use the compact integer encoding.  Values that
268    * still fit CBOR's native integer range (unsigned 64-bit for positives via major type 0, down to
269    * {@code -2^64} for negatives via major type 1) are emitted with the full 64-bit magnitude rather
270    * than truncating.  Values beyond {@code ±2^64} (which have no native CBOR integer head) fall back
271    * to a lossless decimal string so no magnitude is lost.
272    *
273    * @param value The value.
274    * @return This stream.
275    */
276   CborOutputStream appendBigInteger(BigInteger value) {
277      if (value.bitLength() <= 63)
278         return appendLong(value.longValueExact());
279      if (value.signum() > 0) {
280         if (value.bitLength() <= 64) {
281            write1(0x1B);  // major type 0 | additional info 27 (8-byte argument)
282            write8(value.longValue());  // low 64 bits == full unsigned magnitude here
283            return this;
284         }
285      } else {
286         var arg = value.negate().subtract(BigInteger.ONE);  // CBOR major type 1 argument == -1 - value
287         if (arg.bitLength() <= 64) {
288            write1(0x3B);  // major type 1 | additional info 27 (8-byte argument)
289            write8(arg.longValue());
290            return this;
291         }
292      }
293      // Beyond ±2^64: no native CBOR integer head exists; emit a lossless decimal string.
294      return appendString(value.toString());
295   }
296
297   private static int getUtf8ByteLength(CharSequence cs) {
298      var count = 0;
299      for (int i = 0, len = cs.length(); i < len; i++) {
300         var ch = cs.charAt(i);
301         if (ch <= 0x7F)
302            count++;
303         else if (ch <= 0x7FF)
304            count += 2;
305         else if (Character.isHighSurrogate(ch) && i + 1 < len && Character.isLowSurrogate(cs.charAt(i + 1))) {
306            count += 4;
307            ++i;
308         } else
309            // BMP char, or an unpaired surrogate which is emitted as the 3-byte U+FFFD replacement.
310            count += 3;
311      }
312      return count;
313   }
314
315   private int writeUtf8To(CharSequence in) {
316      var count = 0;
317      for (int i = 0, len = in.length(); i < len; i++) {
318         var c = (in.charAt(i) & 0xFFFF);
319         if (c <= 0x7F) {
320            write((byte)(c & 0xFF));
321            count++;
322         } else if (c <= 0x7FF) {
323            write((byte)(0xC0 + ((c >> 6) & 0x1F)));
324            write((byte)(0x80 + (c & 0x3F)));
325            count += 2;
326         } else if (c >= 0xD800 && c <= 0xDBFF && i + 1 < len && Character.isLowSurrogate(in.charAt(i + 1))) {
327            int jchar2 = in.charAt(++i) & 0xFFFF;
328            int n = (c << 10) + jchar2 + 0xFCA02400;
329            write((byte)(0xF0 + ((n >> 18) & 0x07)));
330            write((byte)(0x80 + ((n >> 12) & 0x3F)));
331            write((byte)(0x80 + ((n >> 6) & 0x3F)));
332            write((byte)(0x80 + (n & 0x3F)));
333            count += 4;
334         } else {
335            // BMP char, or an unpaired surrogate.  An unpaired surrogate is not a valid Unicode
336            // scalar value, so emit the U+FFFD replacement character rather than crashing or
337            // producing malformed UTF-8.
338            if (c >= 0xD800 && c <= 0xDFFF)
339               c = 0xFFFD;
340            write((byte)(0xE0 + ((c >> 12) & 0x0F)));
341            write((byte)(0x80 + ((c >> 6) & 0x3F)));
342            write((byte)(0x80 + (c & 0x3F)));
343            count += 3;
344         }
345      }
346      return count;
347   }
348
349   /**
350    * Appends a UTF-8 text string (major type 3).
351    *
352    * @param s The string.
353    * @return This stream.
354    */
355   CborOutputStream appendString(CharSequence s) {
356      int length = getUtf8ByteLength(s);
357      writeHead(3, length);
358      int length2 = writeUtf8To(s);
359      if (length != length2)
360         throw new SerializeException("Unexpected length.  Expected=%s, Actual=%s", length, length2);
361      return this;
362   }
363
364   /**
365    * Appends a byte string (major type 2).
366    *
367    * @param data The bytes.
368    * @return This stream.
369    */
370   CborOutputStream appendBinary(byte[] data) {
371      writeHead(2, data.length);
372      try {
373         os.write(data);
374      } catch (IOException e) {
375         throw new SerializeException(e);
376      }
377      return this;
378   }
379
380   /**
381    * Appends a byte string from an input stream.
382    *
383    * @param data The input stream.
384    * @return This stream.
385    */
386   CborOutputStream appendBinary(InputStream data) {
387      var baos = new ByteArrayOutputStream();
388      pipe(data, baos, x -> {
389         throw new SerializeException(x);
390      });
391      return appendBinary(baos.toByteArray());
392   }
393
394   /**
395    * Starts a definite-length array (major type 4).
396    *
397    * @param size The number of elements.
398    * @return This stream.
399    */
400   CborOutputStream startArray(int size) {
401      writeHead(4, size);
402      return this;
403   }
404
405   /**
406    * Starts a definite-length map (major type 5).
407    *
408    * @param size The number of key-value pairs.
409    * @return This stream.
410    */
411   CborOutputStream startMap(int size) {
412      writeHead(5, size);
413      return this;
414   }
415
416   /**
417    * Writes a CBOR semantic tag (major type 6).
418    *
419    * @param tagNumber The tag number.
420    * @return This stream.
421    */
422   CborOutputStream writeTag(long tagNumber) {
423      writeHead(6, tagNumber);
424      return this;
425   }
426
427   /**
428    * Writes a CBOR simple value (major type 7).
429    *
430    * <p>
431    * Caller responsibility: reserved encodings (20-23 = bool/null/undefined; 25-27 =
432    * float16/32/64; 31 = break) collide with native scalar/structural emits and must not be used
433    * here.
434    *
435    * @param value The simple value (range {@code 0..255}; reserved values noted above).
436    * @return This stream.
437    */
438   CborOutputStream writeSimple(int value) {
439      writeHead(7, value);
440      return this;
441   }
442}