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;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.util.*;
023
024/**
025 * Supported wire formats for <code><jk>byte</jk>[]</code> values and stream-based serialization output.
026 *
027 * <p>
028 * Used by {@link MarshallingContext.Builder#binaryFormat(BinaryFormat)},
029 * {@link Marshalled#binaryFormat()},
030 * {@link MarshalledProp#binaryFormat()}, and
031 * {@link MarshalledConfig#binaryFormat()} to control how
032 * <code><jk>byte</jk>[]</code> values are written to and read from the wire on text formats, as well as
033 * the encoding used by stream-based serializers when converting their byte output to a string.
034 *
035 * <p>
036 * The default is {@link #NOT_SET} — leaves {@code byte[]} handling to the surrounding serializer / parser
037 * (e.g. textual serializers fall back to the natural language-level array representation, and binary
038 * serializers emit native bytes).  Callers wanting a textual encoding on the wire should set this to
039 * {@link #HEX}, {@link #BASE64}, {@link #SPACED_HEX}, or {@link #BASE64_URL}.
040 *
041 * <p>
042 * Binary serializers (BSON / CBOR / MsgPack / Prototext) emit native bytes regardless of the configured
043 * {@code BinaryFormat}; this setting only affects text-based serializers and the
044 * {@code OutputStreamSerializerSession#writeToString(Object)} stream-to-string convenience.
045 */
046public enum BinaryFormat {
047
048   /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */
049   NOT_SET,
050
051   /**
052    * Spaced-hex.
053    *
054    * <h5 class='figure'>Example:</h5>
055    * <p class='bcode'>
056    *    12 34 56 78 90 AB CD EF
057    * </p>
058    */
059   SPACED_HEX,
060
061   /**
062    * Hex.
063    *
064    * <h5 class='figure'>Example:</h5>
065    * <p class='bcode'>
066    *    1234567890ABCDEF
067    * </p>
068    */
069   HEX,
070
071   /** Standard {@link Base64} encoding. */
072   BASE64,
073
074   /**
075    * URL-safe {@link Base64} encoding (RFC 4648 §5).
076    *
077    * <p>
078    * Uses {@code -} and {@code _} in place of {@code +} and {@code /}. Padding ({@code =}) is omitted on
079    * the wire but accepted on input.
080    */
081   BASE64_URL;
082
083   private static final byte[] EMPTY_BYTES = new byte[0];
084
085   /**
086    * Formats the specified value using this format.
087    *
088    * @param value The value to format.  Can be <jk>null</jk> (returns <jk>null</jk>).
089    * @return The formatted string, or <jk>null</jk> if {@code value} is <jk>null</jk>.
090    */
091   public String format(byte[] value) {
092      if (value == null)
093         return null;
094      return switch (this) {
095         case NOT_SET, BASE64 -> base64Encode(value);
096         case BASE64_URL -> Base64.getUrlEncoder().withoutPadding().encodeToString(value);
097         case HEX -> toHex(value);
098         case SPACED_HEX -> toSpacedHex(value);
099      };
100   }
101
102   /**
103    * Parses the specified wire value using this format.
104    *
105    * <p>
106    * The parser accepts any of the formats produced by {@link #format(byte[])} regardless of the
107    * configured constant, mirroring the format-agnostic parser convention used by other {@code <type>Format}
108    * enums in this package.  The configured format is consulted only as a hint for ambiguous input.
109    *
110    * @param value The wire value.  Can be <jk>null</jk> or blank (returns an empty array).
111    * @return The parsed bytes, or an empty array if {@code value} is <jk>null</jk> or blank.
112    */
113   public byte[] parse(String value) {
114      if (value == null)
115         return EMPTY_BYTES;
116      var s = value.trim();
117      if (s.isEmpty())
118         return EMPTY_BYTES;
119      // Honor the configured format constant for BASE64_URL: the URL-safe encoder emits without padding,
120      // so a non-3-aligned payload that happens not to contain `-`/`_` (because the encoded chars all fall
121      // in the base64-alphabet intersection) would otherwise be misrouted to the standard decoder, which
122      // rejects missing padding.  The URL-safe decoder accepts both with-padding and without-padding input.
123      if (this == BASE64_URL) {
124         try {
125            return Base64.getUrlDecoder().decode(s);
126         } catch (IllegalArgumentException e) {
127            throw iaex("Invalid binary value '%s' for format %s: %s", s, this, e.getMessage());
128         }
129      }
130      // Format-agnostic parsing: sniff the wire shape and decode accordingly, regardless of the
131      // configured format on `this`.  A space anywhere implies spaced-hex; a `-`/`_` implies url-safe
132      // base64; otherwise pure hex shape (even-length, hex-only chars) implies hex.  Anything left
133      // is standard base64.
134      if (s.indexOf(' ') >= 0)
135         return fromSpacedHex(s);
136      if (s.indexOf('-') >= 0 || s.indexOf('_') >= 0)
137         return Base64.getUrlDecoder().decode(s);
138      if (isHex(s))
139         return fromHex(s);
140      try {
141         return base64Decode(s);
142      } catch (IllegalArgumentException e) {
143         throw iaex("Invalid binary value '%s' for format %s: %s", s, this, e.getMessage());
144      }
145   }
146
147   private static boolean isHex(String s) {
148      if (s.isEmpty() || (s.length() & 1) == 1)
149         return false;
150      for (var i = 0; i < s.length(); i++) {
151         var c = s.charAt(i);
152         if (! ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
153            return false;
154      }
155      return true;
156   }
157}