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.uon;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022
023import org.apache.juneau.marshall.parser.*;
024
025/**
026 * Same functionality as {@link ParserReader} except automatically decoded <c>%xx</c> escape sequences.
027 *
028 * <p>
029 * Escape sequences are assumed to be encoded UTF-8.  Extended Unicode (&gt;\u10000) is supported.
030 *
031 * <p>
032 * If decoding is enabled, the following character replacements occur so that boundaries are not lost:
033 * <ul>
034 *    <li><js>'&amp;'</js> -&gt; <js>'\u0001'</js>
035 *    <li><js>'='</js> -&gt; <js>'\u0002'</js>
036 * </ul>
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a>
040
041 * </ul>
042 */
043@SuppressWarnings({
044   "resource" // Reader resource managed by calling code
045})
046public class UonReader extends ParserReader {
047
048   private static int fromHexChar(int c) throws IOException {
049      if (c >= '0' && c <= '9')
050         return c - '0';
051      if (c >= 'a' && c <= 'f')
052         return 10 + c - 'a';
053      if (c >= 'A' && c <= 'F')
054         return 10 + c - 'A';
055      throw ioex("Invalid hex character '%s' found in escape pattern.", c);
056   }
057
058   private final boolean decodeChars;
059
060   private final char[] buff;
061
062   // Writable properties.
063   private int iCurrent;
064   private int iEnd;
065
066   /**
067    * Constructor.
068    *
069    * @param pipe The parser input.
070    *    <br>Must not be <jk>null</jk>.
071    * @param decodeChars Whether the input is URL-encoded.
072    * @throws IOException Thrown by underlying stream.
073    */
074   public UonReader(ParserPipe pipe, boolean decodeChars) throws IOException {
075      super(pipe);
076      this.decodeChars = decodeChars;
077      if (pipe.isString()) {
078         var in = pipe.getInputAsString();
079         this.buff = new char[in.length() < 1024 ? in.length() : 1024];
080      } else {
081         this.buff = new char[1024];
082      }
083   }
084
085   @Override /* Overridden from Reader */
086   @SuppressWarnings({
087      "java:S3776" // Cognitive complexity acceptable for UON character reading with encoding handling
088   })
089   public int read(char[] cbuf, int off, int len) throws IOException {
090
091      if (! decodeChars)
092         return super.read(cbuf, off, len);
093
094      // Copy any remainder to the beginning of the buffer.
095      var remainder = iEnd - iCurrent;
096      if (remainder > 0)
097         System.arraycopy(buff, iCurrent, buff, 0, remainder);
098      iCurrent = 0;
099
100      var expected = buff.length - remainder;
101
102      var x = super.read(buff, remainder, expected);
103      if (x == -1 && remainder == 0)
104         return -1;
105
106      iEnd = remainder + (x == -1 ? 0 : x);
107
108      var i = 0;
109      while (i < len) {
110         if (iCurrent >= iEnd)
111            return i;
112         var c = buff[iCurrent++];
113         if (c == '+') {
114            cbuf[off + i++] = ' ';
115         } else if (c == '&') {
116            cbuf[off + i++] = '\u0001';
117         } else if (c == '=') {
118            cbuf[off + i++] = '\u0002';
119         } else if (c != '%') {
120            cbuf[off + i++] = c;
121         } else {
122            var iMark = iCurrent - 1;  // Keep track of current position.
123
124            // Stop if there aren't at least two more characters following '%' in the buffer,
125            // or there aren't at least two more positions open in cbuf to handle double-char chars.
126            if (iMark + 2 >= iEnd || i + 2 > len) {
127               iCurrent--;
128               return i;
129            }
130
131            var b0 = readEncodedByte();
132            int cx;
133
134            // 0xxxxxxx
135            if (b0 < 128) {
136               cx = b0;
137
138            } else if (b0 < 192) {
139               // 10xxxxxx
140               throw ioex("Invalid hex value for first escape pattern in UTF-8 sequence: %s", b0);
141
142            } else if (b0 < 224) {
143               // 110xxxxx 10xxxxxx
144               // 11000000(192) - 11011111(223)
145               cx = readUTF8(b0 - 192, 1);
146               if (cx == -1) {
147                  iCurrent = iMark;
148                  return i;
149               }
150
151            } else if (b0 < 240) {
152               // 1110xxxx 10xxxxxx 10xxxxxx
153               // 11100000(224) - 11101111(239)
154               cx = readUTF8(b0 - 224, 2);
155               if (cx == -1) {
156                  iCurrent = iMark;
157                  return i;
158               }
159
160            } else if (b0 < 248) {
161               // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
162               // 11110000(240) - 11110111(247)
163               cx = readUTF8(b0 - 240, 3);
164               if (cx == -1) {
165                  iCurrent = iMark;
166                  return i;
167               }
168
169            } else
170               throw ioex("Invalid hex value for first escape pattern in UTF-8 sequence: %s", b0);
171
172            if (cx < 0x10000)
173               cbuf[off + i++] = (char)cx;
174            else {
175               cx -= 0x10000;
176               cbuf[off + i++] = (char)(0xd800 + (cx >> 10));
177               cbuf[off + i++] = (char)(0xdc00 + (cx & 0x3ff));
178            }
179         }
180      }
181      return i;
182   }
183
184   @Override /* Overridden from ParserReader */
185   public UonReader unread() throws IOException {
186      super.unread();
187      return this;
188   }
189
190   private int readEncodedByte() throws IOException {
191      if (iEnd <= iCurrent + 1)
192         throw ioex("Incomplete trailing escape pattern");
193      int h = buff[iCurrent++];
194      int l = buff[iCurrent++];
195      h = fromHexChar(h);
196      l = fromHexChar(l);
197      return (h << 4) + l;
198   }
199
200   private int readHex() throws IOException {
201      var c = buff[iCurrent++];
202      if (c != '%')
203         throw ioex("Did not find expected '%' character in UTF-8 sequence.");
204      return readEncodedByte();
205   }
206
207   private int readUTF8(int n, int numBytes) throws IOException {
208      if (iCurrent + numBytes * 3 > iEnd)
209         return -1;
210      for (var i = 0; i < numBytes; i++) {
211         n <<= 6;
212         n += readHex() - 128;
213      }
214      return n;
215   }
216}