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 (>\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>'&'</js> -> <js>'\u0001'</js> 035 * <li><js>'='</js> -> <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}