diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java index 3f08e84a9..0d9371173 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/Restful2ActionMapper.java @@ -26,9 +26,9 @@ import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.struts2.StrutsConstants; +import org.apache.struts2.util.URLDecoderUtil; import javax.servlet.http.HttpServletRequest; -import java.net.URLDecoder; import java.util.HashMap; import java.util.StringTokenizer; @@ -133,10 +133,10 @@ public class Restful2ActionMapper extends DefaultActionMapper { while (st.hasMoreTokens()) { if (isNameTok) { - paramName = URLDecoder.decode(st.nextToken(), "UTF-8"); + paramName = URLDecoderUtil.decode(st.nextToken(), "UTF-8"); isNameTok = false; } else { - paramValue = URLDecoder.decode(st.nextToken(), "UTF-8"); + paramValue = URLDecoderUtil.decode(st.nextToken(), "UTF-8"); if ((paramName != null) && (paramName.length() > 0)) { parameters.put(paramName, paramValue); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java index b2378f40c..4b98409d4 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java @@ -25,9 +25,9 @@ import com.opensymphony.xwork2.config.ConfigurationManager; import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.struts2.RequestUtils; +import org.apache.struts2.util.URLDecoderUtil; import javax.servlet.http.HttpServletRequest; -import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; @@ -67,10 +67,10 @@ public class RestfulActionMapper implements ActionMapper { while (st.hasMoreTokens()) { if (isNameTok) { - paramName = URLDecoder.decode(st.nextToken(), "UTF-8"); + paramName = URLDecoderUtil.decode(st.nextToken(), "UTF-8"); isNameTok = false; } else { - paramValue = URLDecoder.decode(st.nextToken(), "UTF-8"); + paramValue = URLDecoderUtil.decode(st.nextToken(), "UTF-8"); if ((paramName != null) && (paramName.length() > 0)) { parameters.put(paramName, paramValue); diff --git a/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java b/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java new file mode 100644 index 000000000..10f2a7855 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/URLDecoderUtil.java @@ -0,0 +1,22 @@ +package org.apache.struts2.util; + +import org.apache.struts2.util.tomcat.buf.UDecoder; + +/** + * URLDecoderUtil serves as a facade for a correct URL decoding implementation. + * As of Struts 2.3.25 it uses Tomcat URLDecoder functionality rather than the one found in java.io. + */ +public class URLDecoderUtil { + + /** + * Decodes a x-www-form-urlencoded string. + * @param sequence the String to decode + * @param charset The name of a supported character encoding. + * @return the newly decoded String + * @exception IllegalArgumentException If the encoding is not valid + */ + public static String decode(String sequence, String charset) { + return UDecoder.URLDecode(sequence, charset); + } + +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java new file mode 100644 index 000000000..1b0ccb6c0 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Ascii.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +/** + * This class implements some basic ASCII character handling functions. + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + */ +public final class Ascii { + /* + * Character translation tables. + */ + + private static final byte[] toUpper = new byte[256]; + private static final byte[] toLower = new byte[256]; + + /* + * Character type tables. + */ + + private static final boolean[] isAlpha = new boolean[256]; + private static final boolean[] isUpper = new boolean[256]; + private static final boolean[] isLower = new boolean[256]; + private static final boolean[] isWhite = new boolean[256]; + private static final boolean[] isDigit = new boolean[256]; + + private static final long OVERFLOW_LIMIT = Long.MAX_VALUE / 10; + + /* + * Initialize character translation and type tables. + */ + static { + for (int i = 0; i < 256; i++) { + toUpper[i] = (byte)i; + toLower[i] = (byte)i; + } + + for (int lc = 'a'; lc <= 'z'; lc++) { + int uc = lc + 'A' - 'a'; + + toUpper[lc] = (byte)uc; + toLower[uc] = (byte)lc; + isAlpha[lc] = true; + isAlpha[uc] = true; + isLower[lc] = true; + isUpper[uc] = true; + } + + isWhite[ ' '] = true; + isWhite['\t'] = true; + isWhite['\r'] = true; + isWhite['\n'] = true; + isWhite['\f'] = true; + isWhite['\b'] = true; + + for (int d = '0'; d <= '9'; d++) { + isDigit[d] = true; + } + } + + /** + * Returns the upper case equivalent of the specified ASCII character. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static int toUpper(int c) { + return toUpper[c & 0xff] & 0xff; + } + + /** + * Returns the lower case equivalent of the specified ASCII character. + */ + + public static int toLower(int c) { + return toLower[c & 0xff] & 0xff; + } + + /** + * Returns true if the specified ASCII character is upper or lower case. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static boolean isAlpha(int c) { + return isAlpha[c & 0xff]; + } + + /** + * Returns true if the specified ASCII character is upper case. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static boolean isUpper(int c) { + return isUpper[c & 0xff]; + } + + /** + * Returns true if the specified ASCII character is lower case. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static boolean isLower(int c) { + return isLower[c & 0xff]; + } + + /** + * Returns true if the specified ASCII character is white space. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static boolean isWhite(int c) { + return isWhite[c & 0xff]; + } + + /** + * Returns true if the specified ASCII character is a digit. + */ + + public static boolean isDigit(int c) { + return isDigit[c & 0xff]; + } + + /** + * Parses an unsigned integer from the specified subarray of bytes. + * @param b the bytes to parse + * @param off the start offset of the bytes + * @param len the length of the bytes + * @exception NumberFormatException if the integer format was invalid + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static int parseInt(byte[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + int n = c - '0'; + + while (--len > 0) { + if (!isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + n = n * 10 + c - '0'; + } + + return n; + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static int parseInt(char[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + int n = c - '0'; + + while (--len > 0) { + if (!isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + n = n * 10 + c - '0'; + } + + return n; + } + + /** + * Parses an unsigned long from the specified subarray of bytes. + * @param b the bytes to parse + * @param off the start offset of the bytes + * @param len the length of the bytes + * @exception NumberFormatException if the long format was invalid + */ + public static long parseLong(byte[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + long n = c - '0'; + while (--len > 0) { + if (isDigit(c = b[off++]) && + (n < OVERFLOW_LIMIT || (n == OVERFLOW_LIMIT && (c - '0') < 8))) { + n = n * 10 + c - '0'; + } else { + throw new NumberFormatException(); + } + } + + return n; + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static long parseLong(char[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + long n = c - '0'; + long m; + + while (--len > 0) { + if (!isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + m = n * 10 + c - '0'; + + if (m < n) { + // Overflow + throw new NumberFormatException(); + } else { + n = m; + } + } + + return n; + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java new file mode 100644 index 000000000..a3fc6d11f --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/B2CConverter.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * NIO based character decoder. + */ +public class B2CConverter { + + private static final Map encodingToCharsetCache = + new HashMap(); + + public static final Charset ISO_8859_1; + public static final Charset UTF_8; + + // Protected so unit tests can use it + protected static final int LEFTOVER_SIZE = 9; + + static { + for (Charset charset: Charset.availableCharsets().values()) { + encodingToCharsetCache.put( + charset.name().toLowerCase(Locale.ENGLISH), charset); + for (String alias : charset.aliases()) { + encodingToCharsetCache.put( + alias.toLowerCase(Locale.ENGLISH), charset); + } + } + Charset iso88591 = null; + Charset utf8 = null; + try { + iso88591 = getCharset("ISO-8859-1"); + utf8 = getCharset("UTF-8"); + } catch (UnsupportedEncodingException e) { + // Impossible. All JVMs must support these. + e.printStackTrace(); + } + ISO_8859_1 = iso88591; + UTF_8 = utf8; + } + + public static Charset getCharset(String enc) + throws UnsupportedEncodingException { + + // Encoding names should all be ASCII + String lowerCaseEnc = enc.toLowerCase(Locale.ENGLISH); + + return getCharsetLower(lowerCaseEnc); + } + + /** + * Only to be used when it is known that the encoding name is in lower case. + */ + public static Charset getCharsetLower(String lowerCaseEnc) + throws UnsupportedEncodingException { + + Charset charset = encodingToCharsetCache.get(lowerCaseEnc); + + if (charset == null) { + // Pre-population of the cache means this must be invalid + throw new UnsupportedEncodingException("The character encoding " + lowerCaseEnc + " is not supported"); + } + return charset; + } + + private final CharsetDecoder decoder; + private ByteBuffer bb = null; + private CharBuffer cb = null; + + /** + * Leftover buffer used for incomplete characters. + */ + private final ByteBuffer leftovers; + + public B2CConverter(String encoding) throws IOException { + this(encoding, false); + } + + public B2CConverter(String encoding, boolean replaceOnError) + throws IOException { + byte[] left = new byte[LEFTOVER_SIZE]; + leftovers = ByteBuffer.wrap(left); + CodingErrorAction action; + if (replaceOnError) { + action = CodingErrorAction.REPLACE; + } else { + action = CodingErrorAction.REPORT; + } + Charset charset = getCharset(encoding); + // Special case. Use the Apache Harmony based UTF-8 decoder because it + // - a) rejects invalid sequences that the JVM decoder does not + // - b) fails faster for some invalid sequences + if (charset.equals(UTF_8)) { + decoder = new Utf8Decoder(); + } else { + decoder = charset.newDecoder(); + } + decoder.onMalformedInput(action); + decoder.onUnmappableCharacter(action); + } + + /** + * Reset the decoder state. + */ + public void recycle() { + decoder.reset(); + leftovers.position(0); + } + + /** + * Convert the given bytes to characters. + * + * @param bc byte input + * @param cc char output + * @param endOfInput Is this all of the available data + */ + public void convert(ByteChunk bc, CharChunk cc, boolean endOfInput) + throws IOException { + if ((bb == null) || (bb.array() != bc.getBuffer())) { + // Create a new byte buffer if anything changed + bb = ByteBuffer.wrap(bc.getBuffer(), bc.getStart(), bc.getLength()); + } else { + // Initialize the byte buffer + bb.limit(bc.getEnd()); + bb.position(bc.getStart()); + } + if ((cb == null) || (cb.array() != cc.getBuffer())) { + // Create a new char buffer if anything changed + cb = CharBuffer.wrap(cc.getBuffer(), cc.getEnd(), + cc.getBuffer().length - cc.getEnd()); + } else { + // Initialize the char buffer + cb.limit(cc.getBuffer().length); + cb.position(cc.getEnd()); + } + CoderResult result = null; + // Parse leftover if any are present + if (leftovers.position() > 0) { + int pos = cb.position(); + // Loop until one char is decoded or there is a decoder error + do { + leftovers.put(bc.substractB()); + leftovers.flip(); + result = decoder.decode(leftovers, cb, endOfInput); + leftovers.position(leftovers.limit()); + leftovers.limit(leftovers.array().length); + } while (result.isUnderflow() && (cb.position() == pos)); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } + bb.position(bc.getStart()); + leftovers.position(0); + } + // Do the decoding and get the results into the byte chunk and the char + // chunk + result = decoder.decode(bb, cb, endOfInput); + if (result.isError() || result.isMalformed()) { + result.throwException(); + } else if (result.isOverflow()) { + // Propagate current positions to the byte chunk and char chunk, if + // this continues the char buffer will get resized + bc.setOffset(bb.position()); + cc.setEnd(cb.position()); + } else if (result.isUnderflow()) { + // Propagate current positions to the byte chunk and char chunk + bc.setOffset(bb.position()); + cc.setEnd(cb.position()); + // Put leftovers in the leftovers byte buffer + if (bc.getLength() > 0) { + leftovers.limit(leftovers.array().length); + leftovers.position(bc.getLength()); + bc.substract(leftovers.array(), 0, bc.getLength()); + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java new file mode 100644 index 000000000..aff247f0e --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/ByteChunk.java @@ -0,0 +1,935 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +/* + * In a server it is very important to be able to operate on + * the original byte[] without converting everything to chars. + * Some protocols are ASCII only, and some allow different + * non-UNICODE encodings. The encoding is not known beforehand, + * and can even change during the execution of the protocol. + * ( for example a multipart message may have parts with different + * encoding ) + * + * For HTTP it is not very clear how the encoding of RequestURI + * and mime values can be determined, but it is a great advantage + * to be able to parse the request without converting to string. + */ + +// TODO: This class could either extend ByteBuffer, or better a ByteBuffer +// inside this way it could provide the search/etc on ByteBuffer, as a helper. + +/** + * This class is used to represent a chunk of bytes, and + * utilities to manipulate byte[]. + * + * The buffer can be modified and used for both input and output. + * + * There are 2 modes: The chunk can be associated with a sink - ByteInputChannel + * or ByteOutputChannel, which will be used when the buffer is empty (on input) + * or filled (on output). + * For output, it can also grow. This operating mode is selected by calling + * setLimit() or allocate(initial, limit) with limit != -1. + * + * Various search and append method are defined - similar with String and + * StringBuffer, but operating on bytes. + * + * This is important because it allows processing the http headers directly on + * the received bytes, without converting to chars and Strings until the strings + * are needed. In addition, the charset is determined later, from headers or + * user code. + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public final class ByteChunk implements Cloneable, Serializable { + + private static final long serialVersionUID = 1L; + + /** Input interface, used when the buffer is empty + * + * Same as java.nio.channel.ReadableByteChannel + */ + public static interface ByteInputChannel { + /** + * Read new bytes ( usually the internal conversion buffer ). + * The implementation is allowed to ignore the parameters, + * and mutate the chunk if it wishes to implement its own buffering. + */ + public int realReadBytes(byte cbuf[], int off, int len) + throws IOException; + } + + /** Same as java.nio.channel.WrittableByteChannel. + */ + public static interface ByteOutputChannel { + /** + * Send the bytes ( usually the internal conversion buffer ). + * Expect 8k output if the buffer is full. + */ + public void realWriteBytes(byte cbuf[], int off, int len) + throws IOException; + } + + // -------------------- + + /** Default encoding used to convert to strings. It should be UTF8, + as most standards seem to converge, but the servlet API requires + 8859_1, and this object is used mostly for servlets. + */ + public static final Charset DEFAULT_CHARSET = B2CConverter.ISO_8859_1; + + // byte[] + private byte[] buff; + + private int start=0; + private int end; + + private Charset charset; + + private boolean isSet=false; // XXX + + // How much can it grow, when data is added + private int limit=-1; + + private ByteInputChannel in = null; + private ByteOutputChannel out = null; + + private boolean optimizedWrite=true; + + /** + * Creates a new, uninitialized ByteChunk object. + */ + public ByteChunk() { + // NO-OP + } + + public ByteChunk( int initial ) { + allocate( initial, -1 ); + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public ByteChunk getClone() { + try { + return (ByteChunk)this.clone(); + } catch( Exception ex) { + return null; + } + } + + public boolean isNull() { + return ! isSet; // buff==null; + } + + /** + * Resets the message buff to an uninitialized state. + */ + public void recycle() { + // buff = null; + charset=null; + start=0; + end=0; + isSet=false; + } + + public void reset() { + buff=null; + } + + // -------------------- Setup -------------------- + + public void allocate( int initial, int limit ) { + if( buff==null || buff.length < initial ) { + buff=new byte[initial]; + } + this.limit=limit; + start=0; + end=0; + isSet=true; + } + + /** + * Sets the message bytes to the specified subarray of bytes. + * + * @param b the ascii bytes + * @param off the start offset of the bytes + * @param len the length of the bytes + */ + public void setBytes(byte[] b, int off, int len) { + buff = b; + start = off; + end = start+ len; + isSet=true; + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public void setOptimizedWrite(boolean optimizedWrite) { + this.optimizedWrite = optimizedWrite; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + public Charset getCharset() { + if (charset == null) { + charset = DEFAULT_CHARSET; + } + return charset; + } + + /** + * Returns the message bytes. + */ + public byte[] getBytes() { + return getBuffer(); + } + + /** + * Returns the message bytes. + */ + public byte[] getBuffer() { + return buff; + } + + /** + * Returns the start offset of the bytes. + * For output this is the end of the buffer. + */ + public int getStart() { + return start; + } + + public int getOffset() { + return start; + } + + public void setOffset(int off) { + if (end < off ) { + end=off; + } + start=off; + } + + /** + * Returns the length of the bytes. + * XXX need to clean this up + */ + public int getLength() { + return end-start; + } + + /** Maximum amount of data in this buffer. + * + * If -1 or not set, the buffer will grow indefinitely. + * Can be smaller than the current buffer size ( which will not shrink ). + * When the limit is reached, the buffer will be flushed ( if out is set ) + * or throw exception. + */ + public void setLimit(int limit) { + this.limit=limit; + } + + public int getLimit() { + return limit; + } + + /** + * When the buffer is empty, read the data from the input channel. + */ + public void setByteInputChannel(ByteInputChannel in) { + this.in = in; + } + + /** When the buffer is full, write the data to the output channel. + * Also used when large amount of data is appended. + * + * If not set, the buffer will grow to the limit. + */ + public void setByteOutputChannel(ByteOutputChannel out) { + this.out=out; + } + + public int getEnd() { + return end; + } + + public void setEnd( int i ) { + end=i; + } + + // -------------------- Adding data to the buffer -------------------- + /** Append a char, by casting it to byte. This IS NOT intended for unicode. + * + * @param c + * @throws IOException + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public void append( char c ) + throws IOException + { + append( (byte)c); + } + + public void append( byte b ) + throws IOException + { + makeSpace( 1 ); + + // couldn't make space + if( limit >0 && end >= limit ) { + flushBuffer(); + } + buff[end++]=b; + } + + public void append( ByteChunk src ) + throws IOException + { + append( src.getBytes(), src.getStart(), src.getLength()); + } + + /** Add data to the buffer + */ + public void append( byte src[], int off, int len ) + throws IOException + { + // will grow, up to limit + makeSpace( len ); + + // if we don't have limit: makeSpace can grow as it wants + if( limit < 0 ) { + // assert: makeSpace made enough space + System.arraycopy( src, off, buff, end, len ); + end+=len; + return; + } + + // Optimize on a common case. + // If the buffer is empty and the source is going to fill up all the + // space in buffer, may as well write it directly to the output, + // and avoid an extra copy + if ( optimizedWrite && len == limit && end == start && out != null ) { + out.realWriteBytes( src, off, len ); + return; + } + // if we have limit and we're below + if( len <= limit - end ) { + // makeSpace will grow the buffer to the limit, + // so we have space + System.arraycopy( src, off, buff, end, len ); + end+=len; + return; + } + + // need more space than we can afford, need to flush + // buffer + + // the buffer is already at ( or bigger than ) limit + + // We chunk the data into slices fitting in the buffer limit, although + // if the data is written directly if it doesn't fit + + int avail=limit-end; + System.arraycopy(src, off, buff, end, avail); + end += avail; + + flushBuffer(); + + int remain = len - avail; + + while (remain > (limit - end)) { + out.realWriteBytes( src, (off + len) - remain, limit - end ); + remain = remain - (limit - end); + } + + System.arraycopy(src, (off + len) - remain, buff, end, remain); + end += remain; + + } + + + // -------------------- Removing data from the buffer -------------------- + + public int substract() + throws IOException { + + if ((end - start) == 0) { + if (in == null) { + return -1; + } + int n = in.realReadBytes( buff, 0, buff.length ); + if (n < 0) { + return -1; + } + } + + return (buff[start++] & 0xFF); + + } + + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public int substract(ByteChunk src) + throws IOException { + + if ((end - start) == 0) { + if (in == null) { + return -1; + } + int n = in.realReadBytes( buff, 0, buff.length ); + if (n < 0) { + return -1; + } + } + + int len = getLength(); + src.append(buff, start, len); + start = end; + return len; + + } + + + public byte substractB() + throws IOException { + + if ((end - start) == 0) { + if (in == null) + return -1; + int n = in.realReadBytes( buff, 0, buff.length ); + if (n < 0) + return -1; + } + + return (buff[start++]); + + } + + + public int substract( byte src[], int off, int len ) + throws IOException { + + if ((end - start) == 0) { + if (in == null) { + return -1; + } + int n = in.realReadBytes( buff, 0, buff.length ); + if (n < 0) { + return -1; + } + } + + int n = len; + if (len > getLength()) { + n = getLength(); + } + System.arraycopy(buff, start, src, off, n); + start += n; + return n; + + } + + + /** + * Send the buffer to the sink. Called by append() when the limit is + * reached. You can also call it explicitly to force the data to be written. + * + * @throws IOException + */ + public void flushBuffer() + throws IOException + { + //assert out!=null + if( out==null ) { + throw new IOException( "Buffer overflow, no sink " + limit + " " + + buff.length ); + } + out.realWriteBytes( buff, start, end-start ); + end=start; + } + + /** + * Make space for len chars. If len is small, allocate a reserve space too. + * Never grow bigger than limit. + */ + public void makeSpace(int count) { + byte[] tmp = null; + + int newSize; + int desiredSize=end + count; + + // Can't grow above the limit + if( limit > 0 && + desiredSize > limit) { + desiredSize=limit; + } + + if( buff==null ) { + if( desiredSize < 256 ) + { + desiredSize=256; // take a minimum + } + buff=new byte[desiredSize]; + } + + // limit < buf.length ( the buffer is already big ) + // or we already have space XXX + if( desiredSize <= buff.length ) { + return; + } + // grow in larger chunks + if( desiredSize < 2 * buff.length ) { + newSize= buff.length * 2; + if( limit >0 && + newSize > limit ) { + newSize=limit; + } + tmp=new byte[newSize]; + } else { + newSize= buff.length * 2 + count ; + if( limit > 0 && + newSize > limit ) { + newSize=limit; + } + tmp=new byte[newSize]; + } + + System.arraycopy(buff, start, tmp, 0, end-start); + buff = tmp; + tmp = null; + end=end-start; + start=0; + } + + // -------------------- Conversion and getters -------------------- + + @Override + public String toString() { + if (null == buff) { + return null; + } else if (end-start == 0) { + return ""; + } + return StringCache.toString(this); + } + + public String toStringInternal() { + if (charset == null) { + charset = DEFAULT_CHARSET; + } + // new String(byte[], int, int, Charset) takes a defensive copy of the + // entire byte array. This is expensive if only a small subset of the + // bytes will be used. The code below is from Apache Harmony. + CharBuffer cb; + cb = charset.decode(ByteBuffer.wrap(buff, start, end-start)); + return new String(cb.array(), cb.arrayOffset(), cb.length()); + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public int getInt() + { + return Ascii.parseInt(buff, start,end-start); + } + + public long getLong() { + return Ascii.parseLong(buff, start,end-start); + } + + + // -------------------- equals -------------------- + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + // XXX ENCODING - this only works if encoding is UTF8-compat + // ( ok for tomcat, where we compare ascii - header names, etc )!!! + + byte[] b = buff; + int blen = end-start; + if (b == null || blen != s.length()) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (b[boff++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + byte[] b = buff; + int blen = end-start; + if (b == null || blen != s.length()) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (Ascii.toLower(b[boff++]) != Ascii.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + public boolean equals( ByteChunk bb ) { + return equals( bb.getBytes(), bb.getStart(), bb.getLength()); + } + + public boolean equals( byte b2[], int off2, int len2) { + byte b1[]=buff; + if( b1==null && b2==null ) { + return true; + } + + int len=end-start; + if ( len2 != len || b1==null || b2==null ) { + return false; + } + + int off1 = start; + + while ( len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + public boolean equals( CharChunk cc ) { + return equals( cc.getChars(), cc.getStart(), cc.getLength()); + } + + public boolean equals( char c2[], int off2, int len2) { + // XXX works only for enc compatible with ASCII/UTF !!! + byte b1[]=buff; + if( c2==null && b1==null ) { + return true; + } + + if (b1== null || c2==null || end-start != len2 ) { + return false; + } + int off1 = start; + int len=end-start; + + while ( len-- > 0) { + if ( (char)b1[off1++] != c2[off2++]) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes starts with the specified string. + * @param s the string + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public boolean startsWith(String s) { + // Works only if enc==UTF + byte[] b = buff; + int blen = s.length(); + if (b == null || blen > end-start) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (b[boff++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes start with the specified byte array. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public boolean startsWith(byte[] b2) { + byte[] b1 = buff; + if (b1 == null && b2 == null) { + return true; + } + + int len = end - start; + if (b1 == null || b2 == null || b2.length > len) { + return false; + } + for (int i = start, j = 0; i < end && j < b2.length;) { + if (b1[i++] != b2[j++]) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes starts with the specified string. + * @param s the string + * @param pos The position + */ + public boolean startsWithIgnoreCase(String s, int pos) { + byte[] b = buff; + int len = s.length(); + if (b == null || len+pos > end-start) { + return false; + } + int off = start+pos; + for (int i = 0; i < len; i++) { + if (Ascii.toLower( b[off++] ) != Ascii.toLower( s.charAt(i))) { + return false; + } + } + return true; + } + + public int indexOf( String src, int srcOff, int srcLen, int myOff ) { + char first=src.charAt( srcOff ); + + // Look for first char + int srcEnd = srcOff + srcLen; + + mainLoop: + for( int i=myOff+start; i <= (end - srcLen); i++ ) { + if( buff[i] != first ) { + continue; + } + // found first char, now look for a match + int myPos=i+1; + for( int srcPos=srcOff + 1; srcPos< srcEnd;) { + if( buff[myPos++] != src.charAt( srcPos++ )) { + continue mainLoop; + } + } + return i-start; // found it + } + return -1; + } + + // -------------------- Hash code -------------------- + + // normal hash. + public int hash() { + return hashBytes( buff, start, end-start); + } + + /** + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public int hashIgnoreCase() { + return hashBytesIC( buff, start, end-start ); + } + + private static int hashBytes( byte buff[], int start, int bytesLen ) { + int max=start+bytesLen; + byte bb[]=buff; + int code=0; + for (int i = start; i < max ; i++) { + code = code * 37 + bb[i]; + } + return code; + } + + private static int hashBytesIC( byte bytes[], int start, + int bytesLen ) + { + int max=start+bytesLen; + byte bb[]=bytes; + int code=0; + for (int i = start; i < max ; i++) { + code = code * 37 + Ascii.toLower(bb[i]); + } + return code; + } + + /** + * Returns the first instance of the given character in this ByteChunk + * starting at the specified byte. If the character is not found, -1 is + * returned. + *
+ * NOTE: This only works for characters in the range 0-127. + * + * @param c The character + * @param starting The start position + * @return The position of the first instance of the character or + * -1 if the character is not found. + */ + public int indexOf(char c, int starting) { + int ret = indexOf(buff, start + starting, end, c); + return (ret >= start) ? ret - start : -1; + } + + /** + * Returns the first instance of the given character in the given byte array + * between the specified start and end. + *
+ * NOTE: This only works for characters in the range 0-127. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param c The character to search for + * @return The position of the first instance of the character or -1 + * if the character is not found. + */ + public static int indexOf(byte bytes[], int start, int end, char c) { + int offset = start; + + while (offset < end) { + byte b=bytes[offset]; + if (b == c) { + return offset; + } + offset++; + } + return -1; + } + + /** + * Returns the first instance of the given byte in the byte array between + * the specified start and end. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param b The byte to search for + * @return The position of the first instance of the byte or -1 if the + * byte is not found. + */ + public static int findByte(byte bytes[], int start, int end, byte b) { + int offset = start; + while (offset < end) { + if (bytes[offset] == b) { + return offset; + } + offset++; + } + return -1; + } + + /** + * Returns the first instance of any of the given bytes in the byte array + * between the specified start and end. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param b The array of bytes to search for + * @return The position of the first instance of the byte or -1 if the + * byte is not found. + */ + public static int findBytes(byte bytes[], int start, int end, byte b[]) { + int blen = b.length; + int offset = start; + while (offset < end) { + for (int i = 0; i < blen; i++) { + if (bytes[offset] == b[i]) { + return offset; + } + } + offset++; + } + return -1; + } + + /** + * Returns the first instance of any byte that is not one of the given bytes + * in the byte array between the specified start and end. + * + * @param bytes The byte array to search + * @param start The point to start searching from in the byte array + * @param end The point to stop searching in the byte array + * @param b The list of bytes to search for + * @return The position of the first instance a byte that is not + * in the list of bytes to search for or -1 if no such byte + * is found. + * @deprecated Unused. Will be removed in Tomcat 8.0.x onwards. + */ + @Deprecated + public static int findNotBytes(byte bytes[], int start, int end, byte b[]) { + int blen = b.length; + int offset = start; + boolean found; + + while (offset < end) { + found = true; + for (int i = 0; i < blen; i++) { + if (bytes[offset] == b[i]) { + found=false; + break; + } + } + if (found) { + return offset; + } + offset++; + } + return -1; + } + + + /** + * Convert specified String to a byte array. This ONLY WORKS for ascii, UTF + * chars will be truncated. + * + * @param value to convert to byte array + * @return the byte array value + */ + public static final byte[] convertToBytes(String value) { + byte[] result = new byte[value.length()]; + for (int i = 0; i < value.length(); i++) { + result[i] = (byte) value.charAt(i); + } + return result; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java new file mode 100644 index 000000000..527707aac --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/CharChunk.java @@ -0,0 +1,700 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import java.io.IOException; +import java.io.Serializable; + +/** + * Utilities to manipulate char chunks. While String is + * the easiest way to manipulate chars ( search, substrings, etc), + * it is known to not be the most efficient solution - Strings are + * designed as immutable and secure objects. + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public final class CharChunk implements Cloneable, Serializable, CharSequence { + + private static final long serialVersionUID = 1L; + + // Input interface, used when the buffer is emptied. + public static interface CharInputChannel { + /** + * Read new bytes ( usually the internal conversion buffer ). + * The implementation is allowed to ignore the parameters, + * and mutate the chunk if it wishes to implement its own buffering. + */ + public int realReadChars(char cbuf[], int off, int len) + throws IOException; + } + /** + * When we need more space we'll either + * grow the buffer ( up to the limit ) or send it to a channel. + */ + public static interface CharOutputChannel { + /** Send the bytes ( usually the internal conversion buffer ). + * Expect 8k output if the buffer is full. + */ + public void realWriteChars(char cbuf[], int off, int len) + throws IOException; + } + + // -------------------- + + private int hashCode = 0; + // did we compute the hashcode ? + private boolean hasHashCode = false; + + // char[] + private char buff[]; + + private int start; + private int end; + + private boolean isSet=false; // XXX + + // -1: grow indefinitely + // maximum amount to be cached + private int limit=-1; + + private CharInputChannel in = null; + private CharOutputChannel out = null; + + private boolean optimizedWrite=true; + + /** + * Creates a new, uninitialized CharChunk object. + */ + public CharChunk() { + } + + public CharChunk(int size) { + allocate( size, -1 ); + } + + // -------------------- + + public boolean isNull() { + if( end > 0 ) { + return false; + } + return !isSet; //XXX + } + + /** + * Resets the message bytes to an uninitialized state. + */ + public void recycle() { + // buff=null; + isSet=false; // XXX + hasHashCode = false; + start=0; + end=0; + } + + // -------------------- Setup -------------------- + + public void allocate( int initial, int limit ) { + if( buff==null || buff.length < initial ) { + buff=new char[initial]; + } + this.limit=limit; + start=0; + end=0; + isSet=true; + hasHashCode = false; + } + + + public void setOptimizedWrite(boolean optimizedWrite) { + this.optimizedWrite = optimizedWrite; + } + + public void setChars( char[] c, int off, int len ) { + buff=c; + start=off; + end=start + len; + isSet=true; + hasHashCode = false; + } + + /** Maximum amount of data in this buffer. + * + * If -1 or not set, the buffer will grow indefinitely. + * Can be smaller than the current buffer size ( which will not shrink ). + * When the limit is reached, the buffer will be flushed ( if out is set ) + * or throw exception. + */ + public void setLimit(int limit) { + this.limit=limit; + } + + public int getLimit() { + return limit; + } + + /** + * When the buffer is empty, read the data from the input channel. + */ + public void setCharInputChannel(CharInputChannel in) { + this.in = in; + } + + /** When the buffer is full, write the data to the output channel. + * Also used when large amount of data is appended. + * + * If not set, the buffer will grow to the limit. + */ + public void setCharOutputChannel(CharOutputChannel out) { + this.out=out; + } + + // compat + public char[] getChars() + { + return getBuffer(); + } + + public char[] getBuffer() + { + return buff; + } + + /** + * Returns the start offset of the bytes. + * For output this is the end of the buffer. + */ + public int getStart() { + return start; + } + + public int getOffset() { + return start; + } + + /** + * Returns the start offset of the bytes. + */ + public void setOffset(int off) { + start=off; + } + + /** + * Returns the length of the bytes. + */ + public int getLength() { + return end-start; + } + + + public int getEnd() { + return end; + } + + public void setEnd( int i ) { + end=i; + } + + // -------------------- Adding data -------------------- + + public void append( char b ) + throws IOException + { + makeSpace( 1 ); + + // couldn't make space + if( limit >0 && end >= limit ) { + flushBuffer(); + } + buff[end++]=b; + } + + public void append( CharChunk src ) + throws IOException + { + append( src.getBuffer(), src.getOffset(), src.getLength()); + } + + /** Add data to the buffer + */ + public void append( char src[], int off, int len ) + throws IOException + { + // will grow, up to limit + makeSpace( len ); + + // if we don't have limit: makeSpace can grow as it wants + if( limit < 0 ) { + // assert: makeSpace made enough space + System.arraycopy( src, off, buff, end, len ); + end+=len; + return; + } + + // Optimize on a common case. + // If the source is going to fill up all the space in buffer, may + // as well write it directly to the output, and avoid an extra copy + if ( optimizedWrite && len == limit && end == start && out != null ) { + out.realWriteChars( src, off, len ); + return; + } + + // if we have limit and we're below + if( len <= limit - end ) { + // makeSpace will grow the buffer to the limit, + // so we have space + System.arraycopy( src, off, buff, end, len ); + + end+=len; + return; + } + + // need more space than we can afford, need to flush + // buffer + + // the buffer is already at ( or bigger than ) limit + + // Optimization: + // If len-avail < length ( i.e. after we fill the buffer with + // what we can, the remaining will fit in the buffer ) we'll just + // copy the first part, flush, then copy the second part - 1 write + // and still have some space for more. We'll still have 2 writes, but + // we write more on the first. + + if( len + end < 2 * limit ) { + /* If the request length exceeds the size of the output buffer, + flush the output buffer and then write the data directly. + We can't avoid 2 writes, but we can write more on the second + */ + int avail=limit-end; + System.arraycopy(src, off, buff, end, avail); + end += avail; + + flushBuffer(); + + System.arraycopy(src, off+avail, buff, end, len - avail); + end+= len - avail; + + } else { // len > buf.length + avail + // long write - flush the buffer and write the rest + // directly from source + flushBuffer(); + + out.realWriteChars( src, off, len ); + } + } + + + /** Append a string to the buffer + */ + public void append(String s) throws IOException { + append(s, 0, s.length()); + } + + /** Append a string to the buffer + */ + public void append(String s, int off, int len) throws IOException { + if (s==null) { + return; + } + + // will grow, up to limit + makeSpace( len ); + + // if we don't have limit: makeSpace can grow as it wants + if( limit < 0 ) { + // assert: makeSpace made enough space + s.getChars(off, off+len, buff, end ); + end+=len; + return; + } + + int sOff = off; + int sEnd = off + len; + while (sOff < sEnd) { + int d = min(limit - end, sEnd - sOff); + s.getChars( sOff, sOff+d, buff, end); + sOff += d; + end += d; + if (end >= limit) { + flushBuffer(); + } + } + } + + // -------------------- Removing data from the buffer -------------------- + + public int substract() + throws IOException { + + if ((end - start) == 0) { + if (in == null) { + return -1; + } + int n = in.realReadChars(buff, end, buff.length - end); + if (n < 0) { + return -1; + } + } + + return (buff[start++]); + + } + + public int substract( char src[], int off, int len ) + throws IOException { + + if ((end - start) == 0) { + if (in == null) { + return -1; + } + int n = in.realReadChars( buff, end, buff.length - end); + if (n < 0) { + return -1; + } + } + + int n = len; + if (len > getLength()) { + n = getLength(); + } + System.arraycopy(buff, start, src, off, n); + start += n; + return n; + + } + + + public void flushBuffer() + throws IOException + { + //assert out!=null + if( out==null ) { + throw new IOException( "Buffer overflow, no sink " + limit + " " + + buff.length ); + } + out.realWriteChars( buff, start, end - start ); + end=start; + } + + /** Make space for len chars. If len is small, allocate + * a reserve space too. Never grow bigger than limit. + */ + public void makeSpace(int count) + { + char[] tmp = null; + + int newSize; + int desiredSize=end + count; + + // Can't grow above the limit + if( limit > 0 && + desiredSize > limit) { + desiredSize=limit; + } + + if( buff==null ) { + if( desiredSize < 256 ) + { + desiredSize=256; // take a minimum + } + buff=new char[desiredSize]; + } + + // limit < buf.length ( the buffer is already big ) + // or we already have space XXX + if( desiredSize <= buff.length) { + return; + } + // grow in larger chunks + if( desiredSize < 2 * buff.length ) { + newSize= buff.length * 2; + if( limit >0 && + newSize > limit ) { + newSize=limit; + } + tmp=new char[newSize]; + } else { + newSize= buff.length * 2 + count ; + if( limit > 0 && + newSize > limit ) { + newSize=limit; + } + tmp=new char[newSize]; + } + + System.arraycopy(buff, 0, tmp, 0, end); + buff = tmp; + tmp = null; + } + + // -------------------- Conversion and getters -------------------- + + @Override + public String toString() { + if (null == buff) { + return null; + } else if (end-start == 0) { + return ""; + } + return StringCache.toString(this); + } + + public String toStringInternal() { + return new String(buff, start, end-start); + } + + // -------------------- equals -------------------- + + @Override + public boolean equals(Object obj) { + if (obj instanceof CharChunk) { + return equals((CharChunk) obj); + } + return false; + } + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + char[] c = buff; + int len = end-start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + char[] c = buff; + int len = end-start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (Ascii.toLower( c[off++] ) != Ascii.toLower( s.charAt(i))) { + return false; + } + } + return true; + } + + public boolean equals(CharChunk cc) { + return equals( cc.getChars(), cc.getOffset(), cc.getLength()); + } + + public boolean equals(char b2[], int off2, int len2) { + char b1[]=buff; + if( b1==null && b2==null ) { + return true; + } + + if (b1== null || b2==null || end-start != len2) { + return false; + } + int off1 = start; + int len=end-start; + while ( len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes starts with the specified string. + * @param s the string + */ + public boolean startsWith(String s) { + char[] c = buff; + int len = s.length(); + if (c == null || len > end-start) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes starts with the specified string. + * @param s the string + */ + public boolean startsWithIgnoreCase(String s, int pos) { + char[] c = buff; + int len = s.length(); + if (c == null || len+pos > end-start) { + return false; + } + int off = start+pos; + for (int i = 0; i < len; i++) { + if (Ascii.toLower( c[off++] ) != Ascii.toLower( s.charAt(i))) { + return false; + } + } + return true; + } + + + /** + * Returns true if the message bytes end with the specified string. + * @param s the string + */ + public boolean endsWith(String s) { + char[] c = buff; + int len = s.length(); + if (c == null || len > end-start) { + return false; + } + int off = end - len; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + // -------------------- Hash code -------------------- + + @Override + public int hashCode() { + if (hasHashCode) { + return hashCode; + } + int code = 0; + + code = hash(); + hashCode = code; + hasHashCode = true; + return code; + } + + // normal hash. + public int hash() { + int code=0; + for (int i = start; i < start + end-start; i++) { + code = code * 37 + buff[i]; + } + return code; + } + + public int indexOf(char c) { + return indexOf( c, start); + } + + /** + * Returns true if the message bytes starts with the specified string. + * @param c the character + */ + public int indexOf(char c, int starting) { + int ret = indexOf( buff, start+starting, end, c ); + return (ret >= start) ? ret - start : -1; + } + + public static int indexOf( char chars[], int off, int cend, char qq ) + { + while( off < cend ) { + char b=chars[off]; + if( b==qq ) { + return off; + } + off++; + } + return -1; + } + + + public int indexOf(String src, int srcOff, int srcLen, int myOff ) { + char first=src.charAt( srcOff ); + + // Look for first char + int srcEnd = srcOff + srcLen; + + for( int i=myOff+start; i <= (end - srcLen); i++ ) { + if( buff[i] != first ) { + continue; + } + // found first char, now look for a match + int myPos=i+1; + for( int srcPos=srcOff + 1; srcPos< srcEnd;) { + if( buff[myPos++] != src.charAt( srcPos++ )) { + break; + } + if( srcPos==srcEnd ) + { + return i-start; // found it + } + } + } + return -1; + } + + // -------------------- utils + private int min(int a, int b) { + if (a < b) { + return a; + } + return b; + } + + // Char sequence impl + + public char charAt(int index) { + return buff[index + start]; + } + + public CharSequence subSequence(int start, int end) { + try { + CharChunk result = (CharChunk) this.clone(); + result.setOffset(this.start + start); + result.setEnd(this.start + end); + return result; + } catch (CloneNotSupportedException e) { + // Cannot happen + return null; + } + } + + public int length() { + return end - start; + } + +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java new file mode 100644 index 000000000..0b9a11646 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/HexUtils.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +/** + * Tables useful when converting byte arrays to and from strings of hexadecimal + * digits. + * Code from Ajp11, from Apache's JServ. + * + * @author Craig R. McClanahan + */ +public final class HexUtils { + + // -------------------------------------------------------------- Constants + + /** + * Table for HEX to DEC byte translation. + */ + private static final int[] DEC = { + 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, + }; + + + /** + * Table for DEC to HEX byte translation. + */ + private static final byte[] HEX = + { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', + (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' }; + + + /** + * Table for byte to hex string translation. + */ + private static final char[] hex = "0123456789abcdef".toCharArray(); + + + // --------------------------------------------------------- Static Methods + + public static int getDec(int index) { + // Fast for correct values, slower for incorrect ones + try { + return DEC[index - '0']; + } catch (ArrayIndexOutOfBoundsException ex) { + return -1; + } + } + + + public static byte getHex(int index) { + return HEX[index]; + } + + + public static String toHexString(byte[] bytes) { + if (null == bytes) { + return null; + } + + StringBuilder sb = new StringBuilder(bytes.length << 1); + + for(int i = 0; i < bytes.length; ++i) { + sb.append(hex[(bytes[i] & 0xf0) >> 4]) + .append(hex[(bytes[i] & 0x0f)]) + ; + } + + return sb.toString(); + } + + + public static byte[] fromHexString(String input) { + if (input == null) { + return null; + } + + if ((input.length() & 1) == 1) { + // Odd number of characters + throw new IllegalArgumentException("The input must consist of an even number of hex digits"); + } + + char[] inputChars = input.toCharArray(); + byte[] result = new byte[input.length() >> 1]; + for (int i = 0; i < result.length; i++) { + int upperNibble = getDec(inputChars[2*i]); + int lowerNibble = getDec(inputChars[2*i + 1]); + if (upperNibble < 0 || lowerNibble < 0) { + // Non hex character + throw new IllegalArgumentException("The input must consist only of hex digits"); + } + result[i] = (byte) ((upperNibble << 4) + lowerNibble); + } + return result; + } +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java new file mode 100644 index 000000000..df072847f --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/MessageBytes.java @@ -0,0 +1,546 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Locale; + +/** + * This class is used to represent a subarray of bytes in an HTTP message. + * It represents all request/response elements. The byte/char conversions are + * delayed and cached. Everything is recyclable. + * + * The object can represent a byte[], a char[], or a (sub) String. All + * operations can be made in case sensitive mode or not. + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + * @author Costin Manolache + */ +public final class MessageBytes implements Cloneable, Serializable { + private static final long serialVersionUID = 1L; + + // primary type ( whatever is set as original value ) + private int type = T_NULL; + + public static final int T_NULL = 0; + /** getType() is T_STR if the the object used to create the MessageBytes + was a String */ + public static final int T_STR = 1; + /** getType() is T_STR if the the object used to create the MessageBytes + was a byte[] */ + public static final int T_BYTES = 2; + /** getType() is T_STR if the the object used to create the MessageBytes + was a char[] */ + public static final int T_CHARS = 3; + + private int hashCode=0; + // did we compute the hashcode ? + private boolean hasHashCode=false; + + // Internal objects to represent array + offset, and specific methods + private final ByteChunk byteC=new ByteChunk(); + private final CharChunk charC=new CharChunk(); + + // String + private String strValue; + // true if a String value was computed. Probably not needed, + // strValue!=null is the same + private boolean hasStrValue=false; + + /** + * Creates a new, uninitialized MessageBytes object. + * Use static newInstance() in order to allow + * future hooks. + */ + private MessageBytes() { + } + + /** Construct a new MessageBytes instance + */ + public static MessageBytes newInstance() { + return factory.newInstance(); + } + + public boolean isNull() { + // should we check also hasStrValue ??? + return byteC.isNull() && charC.isNull() && ! hasStrValue; + // bytes==null && strValue==null; + } + + /** + * Resets the message bytes to an uninitialized (NULL) state. + */ + public void recycle() { + type=T_NULL; + byteC.recycle(); + charC.recycle(); + + strValue=null; + + hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } + + + /** + * Sets the content to the specified subarray of bytes. + * + * @param b the bytes + * @param off the start offset of the bytes + * @param len the length of the bytes + */ + public void setBytes(byte[] b, int off, int len) { + byteC.setBytes( b, off, len ); + type=T_BYTES; + hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } + + /** + * Sets the content to be a char[] + * + * @param c the bytes + * @param off the start offset of the bytes + * @param len the length of the bytes + */ + public void setChars( char[] c, int off, int len ) { + charC.setChars( c, off, len ); + type=T_CHARS; + hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } + + /** + * Set the content to be a string + */ + public void setString( String s ) { + strValue=s; + hasHashCode=false; + hasLongValue=false; + if (s == null) { + hasStrValue=false; + type=T_NULL; + } else { + hasStrValue=true; + type=T_STR; + } + } + + // -------------------- Conversion and getters -------------------- + + /** Compute the string value + */ + @Override + public String toString() { + if( hasStrValue ) { + return strValue; + } + + switch (type) { + case T_CHARS: + strValue=charC.toString(); + hasStrValue=true; + return strValue; + case T_BYTES: + strValue=byteC.toString(); + hasStrValue=true; + return strValue; + } + return null; + } + + //---------------------------------------- + /** Return the type of the original content. Can be + * T_STR, T_BYTES, T_CHARS or T_NULL + */ + public int getType() { + return type; + } + + /** + * Returns the byte chunk, representing the byte[] and offset/length. + * Valid only if T_BYTES or after a conversion was made. + */ + public ByteChunk getByteChunk() { + return byteC; + } + + /** + * Returns the char chunk, representing the char[] and offset/length. + * Valid only if T_CHARS or after a conversion was made. + */ + public CharChunk getCharChunk() { + return charC; + } + + /** + * Returns the string value. + * Valid only if T_STR or after a conversion was made. + */ + public String getString() { + return strValue; + } + + /** + * Get the Charset used for string<->byte conversions. + */ + public Charset getCharset() { + return byteC.getCharset(); + } + + /** + * Set the Charset used for string<->byte conversions. + */ + public void setCharset(Charset charset) { + byteC.setCharset(charset); + } + + /** Do a char->byte conversion. + */ + public void toBytes() { + if (!byteC.isNull()) { + type=T_BYTES; + return; + } + toString(); + type=T_BYTES; + Charset charset = byteC.getCharset(); + ByteBuffer result = charset.encode(strValue); + byteC.setBytes(result.array(), result.arrayOffset(), result.limit()); + } + + /** Convert to char[] and fill the CharChunk. + * XXX Not optimized - it converts to String first. + */ + public void toChars() { + if( ! charC.isNull() ) { + type=T_CHARS; + return; + } + // inefficient + toString(); + type=T_CHARS; + char cc[]=strValue.toCharArray(); + charC.setChars(cc, 0, cc.length); + } + + + /** + * Returns the length of the original buffer. + * Note that the length in bytes may be different from the length + * in chars. + */ + public int getLength() { + if(type==T_BYTES) { + return byteC.getLength(); + } + if(type==T_CHARS) { + return charC.getLength(); + } + if(type==T_STR) { + return strValue.length(); + } + toString(); + if( strValue==null ) { + return 0; + } + return strValue.length(); + } + + // -------------------- equals -------------------- + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + switch (type) { + case T_STR: + if (strValue == null) { + return s == null; + } + return strValue.equals( s ); + case T_CHARS: + return charC.equals( s ); + case T_BYTES: + return byteC.equals( s ); + default: + return false; + } + } + + /** + * Compares the message bytes to the specified String object. + * @param s the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + switch (type) { + case T_STR: + if (strValue == null) { + return s == null; + } + return strValue.equalsIgnoreCase( s ); + case T_CHARS: + return charC.equalsIgnoreCase( s ); + case T_BYTES: + return byteC.equalsIgnoreCase( s ); + default: + return false; + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MessageBytes) { + return equals((MessageBytes) obj); + } + return false; + } + + public boolean equals(MessageBytes mb) { + switch (type) { + case T_STR: + return mb.equals( strValue ); + } + + if( mb.type != T_CHARS && + mb.type!= T_BYTES ) { + // it's a string or int/date string value + return equals( mb.toString() ); + } + + // mb is either CHARS or BYTES. + // this is either CHARS or BYTES + // Deal with the 4 cases ( in fact 3, one is symmetric) + + if( mb.type == T_CHARS && type==T_CHARS ) { + return charC.equals( mb.charC ); + } + if( mb.type==T_BYTES && type== T_BYTES ) { + return byteC.equals( mb.byteC ); + } + if( mb.type== T_CHARS && type== T_BYTES ) { + return byteC.equals( mb.charC ); + } + if( mb.type== T_BYTES && type== T_CHARS ) { + return mb.byteC.equals( charC ); + } + // can't happen + return true; + } + + + /** + * Returns true if the message bytes starts with the specified string. + * @param s the string + * @param pos The start position + */ + public boolean startsWithIgnoreCase(String s, int pos) { + switch (type) { + case T_STR: + if( strValue==null ) { + return false; + } + if( strValue.length() < pos + s.length() ) { + return false; + } + + for( int i=0; i 0) { + int digit = (int) (current % 10); + current = current / 10; + buf[end++] = HexUtils.getHex(digit); + } + byteC.setOffset(0); + byteC.setEnd(end); + // Inverting buffer + end--; + if (l < 0) { + start++; + } + while (end > start) { + byte temp = buf[start]; + buf[start] = buf[end]; + buf[end] = temp; + start++; + end--; + } + longValue=l; + hasStrValue=false; + hasHashCode=false; + hasLongValue=true; + type=T_BYTES; + } + + // Used for headers conversion + /** Convert the buffer to an long, cache the value + */ + public long getLong() { + if( hasLongValue ) { + return longValue; + } + + switch (type) { + case T_BYTES: + longValue=byteC.getLong(); + break; + default: + longValue= Long.parseLong(toString()); + } + + hasLongValue=true; + return longValue; + + } + + // -------------------- Future may be different -------------------- + + private static final MessageBytesFactory factory=new MessageBytesFactory(); + + private static class MessageBytesFactory { + protected MessageBytesFactory() { + } + public MessageBytes newInstance() { + return new MessageBytes(); + } + } +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java new file mode 100644 index 000000000..3a72d4977 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/StringCache.java @@ -0,0 +1,695 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * This class implements a String cache for ByteChunk and CharChunk. + * + * @author Remy Maucherat + */ +public class StringCache { + + + private static final Logger log = LoggerFactory.getLogger(StringCache.class); + + + // ------------------------------------------------------- Static Variables + + + /** + * Enabled ? + */ + protected static boolean byteEnabled = ("true".equals(System.getProperty( + "tomcat.util.buf.StringCache.byte.enabled", "false"))); + + + protected static boolean charEnabled = ("true".equals(System.getProperty( + "tomcat.util.buf.StringCache.char.enabled", "false"))); + + + protected static int trainThreshold = Integer.parseInt(System.getProperty( + "tomcat.util.buf.StringCache.trainThreshold", "20000")); + + + protected static int cacheSize = Integer.parseInt(System.getProperty( + "tomcat.util.buf.StringCache.cacheSize", "200")); + + + protected static final int maxStringSize = + Integer.parseInt(System.getProperty( + "tomcat.util.buf.StringCache.maxStringSize", "128")); + + + /** + * Statistics hash map for byte chunk. + */ + protected static final HashMap bcStats = + new HashMap(cacheSize); + + + /** + * toString count for byte chunk. + */ + protected static int bcCount = 0; + + + /** + * Cache for byte chunk. + */ + protected static ByteEntry[] bcCache = null; + + + /** + * Statistics hash map for char chunk. + */ + protected static final HashMap ccStats = + new HashMap(cacheSize); + + + /** + * toString count for char chunk. + */ + protected static int ccCount = 0; + + + /** + * Cache for char chunk. + */ + protected static CharEntry[] ccCache = null; + + + /** + * Access count. + */ + protected static int accessCount = 0; + + + /** + * Hit count. + */ + protected static int hitCount = 0; + + + // ------------------------------------------------------------ Properties + + + /** + * @return Returns the cacheSize. + */ + public int getCacheSize() { + return cacheSize; + } + + + /** + * @param cacheSize The cacheSize to set. + */ + public void setCacheSize(int cacheSize) { + StringCache.cacheSize = cacheSize; + } + + + /** + * @return Returns the enabled. + */ + public boolean getByteEnabled() { + return byteEnabled; + } + + + /** + * @param byteEnabled The enabled to set. + */ + public void setByteEnabled(boolean byteEnabled) { + StringCache.byteEnabled = byteEnabled; + } + + + /** + * @return Returns the enabled. + */ + public boolean getCharEnabled() { + return charEnabled; + } + + + /** + * @param charEnabled The enabled to set. + */ + public void setCharEnabled(boolean charEnabled) { + StringCache.charEnabled = charEnabled; + } + + + /** + * @return Returns the trainThreshold. + */ + public int getTrainThreshold() { + return trainThreshold; + } + + + /** + * @param trainThreshold The trainThreshold to set. + */ + public void setTrainThreshold(int trainThreshold) { + StringCache.trainThreshold = trainThreshold; + } + + + /** + * @return Returns the accessCount. + */ + public int getAccessCount() { + return accessCount; + } + + + /** + * @return Returns the hitCount. + */ + public int getHitCount() { + return hitCount; + } + + + // -------------------------------------------------- Public Static Methods + + + public void reset() { + hitCount = 0; + accessCount = 0; + synchronized (bcStats) { + bcCache = null; + bcCount = 0; + } + synchronized (ccStats) { + ccCache = null; + ccCount = 0; + } + } + + + public static String toString(ByteChunk bc) { + + // If the cache is null, then either caching is disabled, or we're + // still training + if (bcCache == null) { + String value = bc.toStringInternal(); + if (byteEnabled && (value.length() < maxStringSize)) { + // If training, everything is synced + synchronized (bcStats) { + // If the cache has been generated on a previous invocation + // while waiting for the lock, just return the toString + // value we just calculated + if (bcCache != null) { + return value; + } + // Two cases: either we just exceeded the train count, in + // which case the cache must be created, or we just update + // the count for the string + if (bcCount > trainThreshold) { + long t1 = System.currentTimeMillis(); + // Sort the entries according to occurrence + TreeMap> tempMap = + new TreeMap>(); + for (Entry item : bcStats.entrySet()) { + ByteEntry entry = item.getKey(); + int[] countA = item.getValue(); + Integer count = Integer.valueOf(countA[0]); + // Add to the list for that count + ArrayList list = tempMap.get(count); + if (list == null) { + // Create list + list = new ArrayList(); + tempMap.put(count, list); + } + list.add(entry); + } + // Allocate array of the right size + int size = bcStats.size(); + if (size > cacheSize) { + size = cacheSize; + } + ByteEntry[] tempbcCache = new ByteEntry[size]; + // Fill it up using an alphabetical order + // and a dumb insert sort + ByteChunk tempChunk = new ByteChunk(); + int n = 0; + while (n < size) { + Object key = tempMap.lastKey(); + ArrayList list = tempMap.get(key); + for (int i = 0; i < list.size() && n < size; i++) { + ByteEntry entry = list.get(i); + tempChunk.setBytes(entry.name, 0, + entry.name.length); + int insertPos = findClosest(tempChunk, + tempbcCache, n); + if (insertPos == n) { + tempbcCache[n + 1] = entry; + } else { + System.arraycopy(tempbcCache, insertPos + 1, + tempbcCache, insertPos + 2, + n - insertPos - 1); + tempbcCache[insertPos + 1] = entry; + } + n++; + } + tempMap.remove(key); + } + bcCount = 0; + bcStats.clear(); + bcCache = tempbcCache; + if (log.isDebugEnabled()) { + long t2 = System.currentTimeMillis(); + log.debug("ByteCache generation time: " + + (t2 - t1) + "ms"); + } + } else { + bcCount++; + // Allocate new ByteEntry for the lookup + ByteEntry entry = new ByteEntry(); + entry.value = value; + int[] count = bcStats.get(entry); + if (count == null) { + int end = bc.getEnd(); + int start = bc.getStart(); + // Create byte array and copy bytes + entry.name = new byte[bc.getLength()]; + System.arraycopy(bc.getBuffer(), start, entry.name, + 0, end - start); + // Set encoding + entry.charset = bc.getCharset(); + // Initialize occurrence count to one + count = new int[1]; + count[0] = 1; + // Set in the stats hash map + bcStats.put(entry, count); + } else { + count[0] = count[0] + 1; + } + } + } + } + return value; + } else { + accessCount++; + // Find the corresponding String + String result = find(bc); + if (result == null) { + return bc.toStringInternal(); + } + // Note: We don't care about safety for the stats + hitCount++; + return result; + } + + } + + + public static String toString(CharChunk cc) { + + // If the cache is null, then either caching is disabled, or we're + // still training + if (ccCache == null) { + String value = cc.toStringInternal(); + if (charEnabled && (value.length() < maxStringSize)) { + // If training, everything is synced + synchronized (ccStats) { + // If the cache has been generated on a previous invocation + // while waiting for the lock, just return the toString + // value we just calculated + if (ccCache != null) { + return value; + } + // Two cases: either we just exceeded the train count, in + // which case the cache must be created, or we just update + // the count for the string + if (ccCount > trainThreshold) { + long t1 = System.currentTimeMillis(); + // Sort the entries according to occurrence + TreeMap> tempMap = + new TreeMap>(); + for (Entry item : ccStats.entrySet()) { + CharEntry entry = item.getKey(); + int[] countA = item.getValue(); + Integer count = Integer.valueOf(countA[0]); + // Add to the list for that count + ArrayList list = tempMap.get(count); + if (list == null) { + // Create list + list = new ArrayList(); + tempMap.put(count, list); + } + list.add(entry); + } + // Allocate array of the right size + int size = ccStats.size(); + if (size > cacheSize) { + size = cacheSize; + } + CharEntry[] tempccCache = new CharEntry[size]; + // Fill it up using an alphabetical order + // and a dumb insert sort + CharChunk tempChunk = new CharChunk(); + int n = 0; + while (n < size) { + Object key = tempMap.lastKey(); + ArrayList list = tempMap.get(key); + for (int i = 0; i < list.size() && n < size; i++) { + CharEntry entry = list.get(i); + tempChunk.setChars(entry.name, 0, + entry.name.length); + int insertPos = findClosest(tempChunk, + tempccCache, n); + if (insertPos == n) { + tempccCache[n + 1] = entry; + } else { + System.arraycopy(tempccCache, insertPos + 1, + tempccCache, insertPos + 2, + n - insertPos - 1); + tempccCache[insertPos + 1] = entry; + } + n++; + } + tempMap.remove(key); + } + ccCount = 0; + ccStats.clear(); + ccCache = tempccCache; + if (log.isDebugEnabled()) { + long t2 = System.currentTimeMillis(); + log.debug("CharCache generation time: " + + (t2 - t1) + "ms"); + } + } else { + ccCount++; + // Allocate new CharEntry for the lookup + CharEntry entry = new CharEntry(); + entry.value = value; + int[] count = ccStats.get(entry); + if (count == null) { + int end = cc.getEnd(); + int start = cc.getStart(); + // Create char array and copy chars + entry.name = new char[cc.getLength()]; + System.arraycopy(cc.getBuffer(), start, entry.name, + 0, end - start); + // Initialize occurrence count to one + count = new int[1]; + count[0] = 1; + // Set in the stats hash map + ccStats.put(entry, count); + } else { + count[0] = count[0] + 1; + } + } + } + } + return value; + } else { + accessCount++; + // Find the corresponding String + String result = find(cc); + if (result == null) { + return cc.toStringInternal(); + } + // Note: We don't care about safety for the stats + hitCount++; + return result; + } + + } + + + // ----------------------------------------------------- Protected Methods + + + /** + * Compare given byte chunk with byte array. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + protected static final int compare(ByteChunk name, byte[] compareTo) { + int result = 0; + + byte[] b = name.getBuffer(); + int start = name.getStart(); + int end = name.getEnd(); + int len = compareTo.length; + + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (b[i + start] > compareTo[i]) { + result = 1; + } else if (b[i + start] < compareTo[i]) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length > (end - start)) { + result = -1; + } else if (compareTo.length < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find an entry given its name in the cache and return the associated + * String. + */ + protected static final String find(ByteChunk name) { + int pos = findClosest(name, bcCache, bcCache.length); + if ((pos < 0) || (compare(name, bcCache[pos].name) != 0) + || !(name.getCharset().equals(bcCache[pos].charset))) { + return null; + } else { + return bcCache[pos].value; + } + } + + + /** + * Find an entry given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ + protected static final int findClosest(ByteChunk name, ByteEntry[] array, + int len) { + + int a = 0; + int b = len - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, array[0].name) < 0) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, array[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, array[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + /** + * Compare given char chunk with char array. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + protected static final int compare(CharChunk name, char[] compareTo) { + int result = 0; + + char[] c = name.getBuffer(); + int start = name.getStart(); + int end = name.getEnd(); + int len = compareTo.length; + + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (c[i + start] > compareTo[i]) { + result = 1; + } else if (c[i + start] < compareTo[i]) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length > (end - start)) { + result = -1; + } else if (compareTo.length < (end - start)) { + result = 1; + } + } + return result; + } + + + /** + * Find an entry given its name in the cache and return the associated + * String. + */ + protected static final String find(CharChunk name) { + int pos = findClosest(name, ccCache, ccCache.length); + if ((pos < 0) || (compare(name, ccCache[pos].name) != 0)) { + return null; + } else { + return ccCache[pos].value; + } + } + + + /** + * Find an entry given its name in a sorted array of map elements. + * This will return the index for the closest inferior or equal item in the + * given array. + */ + protected static final int findClosest(CharChunk name, CharEntry[] array, + int len) { + + int a = 0; + int b = len - 1; + + // Special cases: -1 and 0 + if (b == -1) { + return -1; + } + + if (compare(name, array[0].name) < 0 ) { + return -1; + } + if (b == 0) { + return 0; + } + + int i = 0; + while (true) { + i = (b + a) >>> 1; + int result = compare(name, array[i].name); + if (result == 1) { + a = i; + } else if (result == 0) { + return i; + } else { + b = i; + } + if ((b - a) == 1) { + int result2 = compare(name, array[b].name); + if (result2 < 0) { + return a; + } else { + return b; + } + } + } + + } + + + // -------------------------------------------------- ByteEntry Inner Class + + + private static class ByteEntry { + + private byte[] name = null; + private Charset charset = null; + private String value = null; + + @Override + public String toString() { + return value; + } + @Override + public int hashCode() { + return value.hashCode(); + } + @Override + public boolean equals(Object obj) { + if (obj instanceof ByteEntry) { + return value.equals(((ByteEntry) obj).value); + } + return false; + } + + } + + + // -------------------------------------------------- CharEntry Inner Class + + + private static class CharEntry { + + private char[] name = null; + private String value = null; + + @Override + public String toString() { + return value; + } + @Override + public int hashCode() { + return value.hashCode(); + } + @Override + public boolean equals(Object obj) { + if (obj instanceof CharEntry) { + return value.equals(((CharEntry) obj).value); + } + return false; + } + + } + + +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java new file mode 100644 index 000000000..b52cda749 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/UDecoder.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import com.opensymphony.xwork2.util.logging.LoggerFactory; + +import com.opensymphony.xwork2.util.logging.Logger; +import java.io.CharConversionException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * All URL decoding happens here. This way we can reuse, review, optimize + * without adding complexity to the buffers. + * + * The conversion will modify the original buffer. + * + * @author Costin Manolache + */ +public final class UDecoder { + + private static final Logger log = LoggerFactory.getLogger(UDecoder.class); + + public static final boolean ALLOW_ENCODED_SLASH = + Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "false")); + + private static class DecodeException extends CharConversionException { + private static final long serialVersionUID = 1L; + public DecodeException(String s) { + super(s); + } + + @Override + public synchronized Throwable fillInStackTrace() { + // This class does not provide a stack trace + return this; + } + } + + /** Unexpected end of data. */ + private static final IOException EXCEPTION_EOF = new DecodeException("EOF"); + + /** %xx with not-hex digit */ + private static final IOException EXCEPTION_NOT_HEX_DIGIT = new DecodeException( + "isHexDigit"); + + /** %-encoded slash is forbidden in resource path */ + private static final IOException EXCEPTION_SLASH = new DecodeException( + "noSlash"); + + public UDecoder() + { + } + + /** URLDecode, will modify the source. + */ + public void convert( ByteChunk mb, boolean query ) + throws IOException + { + int start=mb.getOffset(); + byte buff[]=mb.getBytes(); + int end=mb.getEnd(); + + int idx= ByteChunk.findByte( buff, start, end, (byte) '%' ); + int idx2=-1; + if( query ) { + idx2= ByteChunk.findByte( buff, start, (idx >= 0 ? idx : end), (byte) '+' ); + } + if( idx<0 && idx2<0 ) { + return; + } + + // idx will be the smallest positive index ( first % or + ) + if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) { + idx=idx2; + } + + final boolean noSlash = !(ALLOW_ENCODED_SLASH || query); + + for( int j=idx; j= end ) { + throw EXCEPTION_EOF; + } + byte b1= buff[j+1]; + byte b2=buff[j+2]; + if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) { + throw EXCEPTION_NOT_HEX_DIGIT; + } + + j+=2; + int res=x2c( b1, b2 ); + if (noSlash && (res == '/')) { + throw EXCEPTION_SLASH; + } + buff[idx]=(byte)res; + } + } + + mb.setEnd( idx ); + + return; + } + + // -------------------- Additional methods -------------------- + // XXX What do we do about charset ???? + + /** In-buffer processing - the buffer will be modified + */ + public void convert( CharChunk mb, boolean query ) + throws IOException + { + // log( "Converting a char chunk "); + int start=mb.getOffset(); + char buff[]=mb.getBuffer(); + int cend=mb.getEnd(); + + int idx= CharChunk.indexOf( buff, start, cend, '%' ); + int idx2=-1; + if( query ) { + idx2= CharChunk.indexOf( buff, start, (idx >= 0 ? idx : cend), '+' ); + } + if( idx<0 && idx2<0 ) { + return; + } + + // idx will be the smallest positive index ( first % or + ) + if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) { + idx=idx2; + } + + final boolean noSlash = !(ALLOW_ENCODED_SLASH || query); + + for( int j=idx; j= cend ) { + // invalid + throw EXCEPTION_EOF; + } + char b1= buff[j+1]; + char b2=buff[j+2]; + if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) { + throw EXCEPTION_NOT_HEX_DIGIT; + } + + j+=2; + int res=x2c( b1, b2 ); + if (noSlash && (res == '/')) { + throw EXCEPTION_SLASH; + } + buff[idx]=(char)res; + } + } + mb.setEnd( idx ); + } + + /** URLDecode, will modify the source + */ + public void convert(MessageBytes mb, boolean query) + throws IOException + { + + switch (mb.getType()) { + case MessageBytes.T_STR: + String strValue=mb.toString(); + if( strValue==null ) { + return; + } + try { + mb.setString( convert( strValue, query )); + } catch (RuntimeException ex) { + throw new DecodeException(ex.getMessage()); + } + break; + case MessageBytes.T_CHARS: + CharChunk charC=mb.getCharChunk(); + convert( charC, query ); + break; + case MessageBytes.T_BYTES: + ByteChunk bytesC=mb.getByteChunk(); + convert( bytesC, query ); + break; + } + } + + // XXX Old code, needs to be replaced !!!! + // + public final String convert(String str, boolean query) + { + if (str == null) { + return null; + } + + if( (!query || str.indexOf( '+' ) < 0) && str.indexOf( '%' ) < 0 ) { + return str; + } + + final boolean noSlash = !(ALLOW_ENCODED_SLASH || query); + + StringBuilder dec = new StringBuilder(); // decoded string output + int strPos = 0; + int strLen = str.length(); + + dec.ensureCapacity(str.length()); + while (strPos < strLen) { + int laPos; // lookahead position + + // look ahead to next URLencoded metacharacter, if any + for (laPos = strPos; laPos < strLen; laPos++) { + char laChar = str.charAt(laPos); + if ((laChar == '+' && query) || (laChar == '%')) { + break; + } + } + + // if there were non-metacharacters, copy them all as a block + if (laPos > strPos) { + dec.append(str.substring(strPos,laPos)); + strPos = laPos; + } + + // shortcut out of here if we're at the end of the string + if (strPos >= strLen) { + break; + } + + // process next metacharacter + char metaChar = str.charAt(strPos); + if (metaChar == '+') { + dec.append(' '); + strPos++; + continue; + } else if (metaChar == '%') { + // We throw the original exception - the super will deal with + // it + // try { + char res = (char) Integer.parseInt( + str.substring(strPos + 1, strPos + 3), 16); + if (noSlash && (res == '/')) { + throw new IllegalArgumentException("noSlash"); + } + dec.append(res); + strPos += 3; + } + } + + return dec.toString(); + } + + + /** + * Decode and return the specified URL-encoded String. + * When the byte array is converted to a string, the system default + * character encoding is used... This may be different than some other + * servers. It is assumed the string is not a query string. + * + * @param str The url-encoded string + * + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str) { + return URLDecode(str, null); + } + + + /** + * Decode and return the specified URL-encoded String. It is assumed the + * string is not a query string. + * + * @param str The url-encoded string + * @param enc The encoding to use; if null, the default encoding is used. If + * an unsupported encoding is specified null will be returned + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str, String enc) { + return URLDecode(str, enc, false); + } + + + /** + * Decode and return the specified URL-encoded String. + * + * @param str The url-encoded string + * @param enc The encoding to use; if null, the default encoding is used. If + * an unsupported encoding is specified null will be returned + * @param isQuery Is this a query string being processed + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(String str, String enc, boolean isQuery) { + if (str == null) + return (null); + + // use the specified encoding to extract bytes out of the + // given string so that the encoding is not lost. If an + // encoding is not specified, use ISO-8859-1 + byte[] bytes = null; + try { + if (enc == null) { + bytes = str.getBytes("ISO-8859-1"); + } else { + bytes = str.getBytes(B2CConverter.getCharset(enc)); + } + } catch (UnsupportedEncodingException uee) { + if (log.isDebugEnabled()) { + log.debug("Unable to URL decode the specified input since the encoding "+ enc + " is not supported.", uee); + } + } + + return URLDecode(bytes, enc, isQuery); + + } + + + /** + * Decode and return the specified URL-encoded byte array. + * + * @param bytes The url-encoded byte array + * @param enc The encoding to use; if null, the default encoding is used. If + * an unsupported encoding is specified null will be returned + * @param isQuery Is this a query string being processed + * @exception IllegalArgumentException if a '%' character is not followed + * by a valid 2-digit hexadecimal number + */ + public static String URLDecode(byte[] bytes, String enc, boolean isQuery) { + + if (bytes == null) + return null; + + int len = bytes.length; + int ix = 0; + int ox = 0; + while (ix < len) { + byte b = bytes[ix++]; // Get byte to test + if (b == '+' && isQuery) { + b = (byte)' '; + } else if (b == '%') { + if (ix + 2 > len) { + throw new IllegalArgumentException( + "The % character must be followed by two hexademical digits"); + } + b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + + convertHexDigit(bytes[ix++])); + } + bytes[ox++] = b; + } + if (enc != null) { + try { + return new String(bytes, 0, ox, B2CConverter.getCharset(enc)); + } catch (UnsupportedEncodingException uee) { + if (log.isDebugEnabled()) { + log.debug("Unable to URL decode the specified input since the encoding " + enc + " is not supported.", uee); + } + return null; + } + } + return new String(bytes, 0, ox); + + } + + + private static byte convertHexDigit( byte b ) { + if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); + if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); + if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); + throw new IllegalArgumentException(((char) b) + " is not a hexadecimal digit"); + } + + + private static boolean isHexDigit( int c ) { + return ( ( c>='0' && c<='9' ) || + ( c>='a' && c<='f' ) || + ( c>='A' && c<='F' )); + } + + + private static int x2c( byte b1, byte b2 ) { + int digit= (b1>='A') ? ( (b1 & 0xDF)-'A') + 10 : + (b1 -'0'); + digit*=16; + digit +=(b2>='A') ? ( (b2 & 0xDF)-'A') + 10 : + (b2 -'0'); + return digit; + } + + + private static int x2c( char b1, char b2 ) { + int digit= (b1>='A') ? ( (b1 & 0xDF)-'A') + 10 : + (b1 -'0'); + digit*=16; + digit +=(b2>='A') ? ( (b2 & 0xDF)-'A') + 10 : + (b2 -'0'); + return digit; + } +} diff --git a/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java new file mode 100644 index 000000000..b08b23690 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/tomcat/buf/Utf8Decoder.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.util.tomcat.buf; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; + +/** + * Decodes bytes to UTF-8. Extracted from Apache Harmony and modified to reject + * code points from U+D800 to U+DFFF as per RFC3629. The standard Java decoder + * does not reject these. It has also been modified to reject code points + * greater than U+10FFFF which the standard Java decoder rejects but the harmony + * one does not. + */ +public class Utf8Decoder extends CharsetDecoder { + + // The next table contains information about UTF-8 charset and + // correspondence of 1st byte to the length of sequence + // For information please visit http://www.ietf.org/rfc/rfc3629.txt + // + // Please note, o means 0, actually. + // ------------------------------------------------------------------- + // 0 1 2 3 Value + // ------------------------------------------------------------------- + // oxxxxxxx 00000000 00000000 0xxxxxxx + // 11oyyyyy 1oxxxxxx 00000000 00000yyy yyxxxxxx + // 111ozzzz 1oyyyyyy 1oxxxxxx 00000000 zzzzyyyy yyxxxxxx + // 1111ouuu 1ouuzzzz 1oyyyyyy 1oxxxxxx 000uuuuu zzzzyyyy yyxxxxxx + private static final int remainingBytes[] = { + // 1owwwwww + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + // 11oyyyyy + -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // 111ozzzz + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + // 1111ouuu + 3, 3, 3, 3, 3, -1, -1, -1, + // > 11110111 + -1, -1, -1, -1, -1, -1, -1, -1}; + private static final int remainingNumbers[] = {0, // 0 1 2 3 + 4224, // (01o00000b << 6)+(1o000000b) + 401536, // (011o0000b << 12)+(1o000000b << 6)+(1o000000b) + 29892736 // (0111o000b << 18)+(1o000000b << 12)+(1o000000b << + // 6)+(1o000000b) + }; + private static final int lowerEncodingLimit[] = {-1, 0x80, 0x800, 0x10000}; + + + public Utf8Decoder() { + super(B2CConverter.UTF_8, 1.0f, 1.0f); + } + + + @Override + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { + if (in.hasArray() && out.hasArray()) { + return decodeHasArray(in, out); + } + return decodeNotHasArray(in, out); + } + + + private CoderResult decodeNotHasArray(ByteBuffer in, CharBuffer out) { + int outRemaining = out.remaining(); + int pos = in.position(); + int limit = in.limit(); + try { + while (pos < limit) { + if (outRemaining == 0) { + return CoderResult.OVERFLOW; + } + int jchar = in.get(); + if (jchar < 0) { + jchar = jchar & 0x7F; + int tail = remainingBytes[jchar]; + if (tail == -1) { + return CoderResult.malformedForLength(1); + } + if (limit - pos < 1 + tail) { + // No early test for invalid sequences here as peeking + // at the next byte is harder + return CoderResult.UNDERFLOW; + } + int nextByte; + for (int i = 0; i < tail; i++) { + nextByte = in.get() & 0xFF; + if ((nextByte & 0xC0) != 0x80) { + return CoderResult.malformedForLength(1 + i); + } + jchar = (jchar << 6) + nextByte; + } + jchar -= remainingNumbers[tail]; + if (jchar < lowerEncodingLimit[tail]) { + // Should have been encoded in a fewer octets + return CoderResult.malformedForLength(1); + } + pos += tail; + } + // Apache Tomcat added test + if (jchar >= 0xD800 && jchar <= 0xDFFF) { + return CoderResult.unmappableForLength(3); + } + // Apache Tomcat added test + if (jchar > 0x10FFFF) { + return CoderResult.unmappableForLength(4); + } + if (jchar <= 0xffff) { + out.put((char) jchar); + outRemaining--; + } else { + if (outRemaining < 2) { + return CoderResult.OVERFLOW; + } + out.put((char) ((jchar >> 0xA) + 0xD7C0)); + out.put((char) ((jchar & 0x3FF) + 0xDC00)); + outRemaining -= 2; + } + pos++; + } + return CoderResult.UNDERFLOW; + } finally { + in.position(pos); + } + } + + + private CoderResult decodeHasArray(ByteBuffer in, CharBuffer out) { + int outRemaining = out.remaining(); + int pos = in.position(); + int limit = in.limit(); + final byte[] bArr = in.array(); + final char[] cArr = out.array(); + final int inIndexLimit = limit + in.arrayOffset(); + int inIndex = pos + in.arrayOffset(); + int outIndex = out.position() + out.arrayOffset(); + // if someone would change the limit in process, + // he would face consequences + for (; inIndex < inIndexLimit && outRemaining > 0; inIndex++) { + int jchar = bArr[inIndex]; + if (jchar < 0) { + jchar = jchar & 0x7F; + // If first byte is invalid, tail will be set to -1 + int tail = remainingBytes[jchar]; + if (tail == -1) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // Additional checks to detect invalid sequences ASAP + // Checks derived from Unicode 6.2, Chapter 3, Table 3-7 + // Check 2nd byte + int tailAvailable = inIndexLimit - inIndex - 1; + if (tailAvailable > 0) { + // First byte C2..DF, second byte 80..BF + if (jchar > 0x41 && jchar < 0x60 && + (bArr[inIndex + 1] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte E0, second byte A0..BF + if (jchar == 0x60 && (bArr[inIndex + 1] & 0xE0) != 0xA0) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte E1..EC, second byte 80..BF + if (jchar > 0x60 && jchar < 0x6D && + (bArr[inIndex + 1] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte ED, second byte 80..9F + if (jchar == 0x6D && (bArr[inIndex + 1] & 0xE0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte EE..EF, second byte 80..BF + if (jchar > 0x6D && jchar < 0x70 && + (bArr[inIndex + 1] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte F0, second byte 90..BF + if (jchar == 0x70 && + ((bArr[inIndex + 1] & 0xFF) < 0x90 || + (bArr[inIndex + 1] & 0xFF) > 0xBF)) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte F1..F3, second byte 80..BF + if (jchar > 0x70 && jchar < 0x74 && + (bArr[inIndex + 1] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + // First byte F4, second byte 80..8F + if (jchar == 0x74 && + (bArr[inIndex + 1] & 0xF0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + } + // Check third byte if present and expected + if (tailAvailable > 1 && tail > 1) { + if ((bArr[inIndex + 2] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(2); + } + } + // Check fourth byte if present and expected + if (tailAvailable > 2 && tail > 2) { + if ((bArr[inIndex + 3] & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(3); + } + } + if (tailAvailable < tail) { + break; + } + for (int i = 0; i < tail; i++) { + int nextByte = bArr[inIndex + i + 1] & 0xFF; + if ((nextByte & 0xC0) != 0x80) { + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1 + i); + } + jchar = (jchar << 6) + nextByte; + } + jchar -= remainingNumbers[tail]; + if (jchar < lowerEncodingLimit[tail]) { + // Should have been encoded in fewer octets + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return CoderResult.malformedForLength(1); + } + inIndex += tail; + } + // Apache Tomcat added test + if (jchar >= 0xD800 && jchar <= 0xDFFF) { + return CoderResult.unmappableForLength(3); + } + // Apache Tomcat added test + if (jchar > 0x10FFFF) { + return CoderResult.unmappableForLength(4); + } + if (jchar <= 0xffff) { + cArr[outIndex++] = (char) jchar; + outRemaining--; + } else { + if (outRemaining < 2) { + return CoderResult.OVERFLOW; + } + cArr[outIndex++] = (char) ((jchar >> 0xA) + 0xD7C0); + cArr[outIndex++] = (char) ((jchar & 0x3FF) + 0xDC00); + outRemaining -= 2; + } + } + in.position(inIndex - in.arrayOffset()); + out.position(outIndex - out.arrayOffset()); + return (outRemaining == 0 && inIndex < inIndexLimit) ? + CoderResult.OVERFLOW : + CoderResult.UNDERFLOW; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java index 4d20ee293..66a9b7c65 100644 --- a/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java +++ b/core/src/main/java/org/apache/struts2/views/util/DefaultUrlHelper.java @@ -27,11 +27,11 @@ import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.struts2.StrutsConstants; +import org.apache.struts2.util.URLDecoderUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; @@ -276,15 +276,15 @@ public class DefaultUrlHelper implements UrlHelper { } /** - * Decodes the URL using {@link java.net.URLDecoder#decode(String, String)} with the encoding specified in the configuration. + * Decodes the URL using {@link URLDecoderUtil#decode(String, String)} with the encoding specified in the configuration. * * @param input the input to decode * @return the encoded string */ public String decode( String input ) { try { - return URLDecoder.decode(input, encoding); - } catch (UnsupportedEncodingException e) { + return URLDecoderUtil.decode(input, encoding); + } catch (Exception e) { if (LOG.isWarnEnabled()) { LOG.warn("Could not decode URL parameter '#0', returning value un-decoded", input); } diff --git a/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java b/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java new file mode 100644 index 000000000..f21c08fc8 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/util/URLDecoderUtilTest.java @@ -0,0 +1,71 @@ +package org.apache.struts2.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class URLDecoderUtilTest { + + @Test + public void testURLDecodeStringInvalid() { + // %n rather than %nn should throw an IAE according to the Javadoc + Exception exception = null; + try { + URLDecoderUtil.decode("%5xxxxx", "ISO-8859-1"); + } catch (Exception e) { + exception = e; + } + assertTrue(exception instanceof IllegalArgumentException); + + // Edge case trying to trigger ArrayIndexOutOfBoundsException + exception = null; + try { + URLDecoderUtil.decode("%5", "ISO-8859-1"); + } catch (Exception e) { + exception = e; + } + assertTrue(exception instanceof IllegalArgumentException); + } + + @Test + public void testURLDecodeStringValidIso88591Start() { + + String result = URLDecoderUtil.decode("%41xxxx", "ISO-8859-1"); + assertEquals("Axxxx", result); + } + + @Test + public void testURLDecodeStringValidIso88591Middle() { + + String result = URLDecoderUtil.decode("xx%41xx", "ISO-8859-1"); + assertEquals("xxAxx", result); + } + + @Test + public void testURLDecodeStringValidIso88591End() { + + String result = URLDecoderUtil.decode("xxxx%41", "ISO-8859-1"); + assertEquals("xxxxA", result); + } + + @Test + public void testURLDecodeStringValidUtf8Start() { + String result = URLDecoderUtil.decode("%c3%aaxxxx", "UTF-8"); + assertEquals("\u00eaxxxx", result); + } + + @Test + public void testURLDecodeStringValidUtf8Middle() { + + String result = URLDecoderUtil.decode("xx%c3%aaxx", "UTF-8"); + assertEquals("xx\u00eaxx", result); + } + + @Test + public void testURLDecodeStringValidUtf8End() { + + String result = URLDecoderUtil.decode("xxxx%c3%aa", "UTF-8"); + assertEquals("xxxx\u00ea", result); + } + +} \ No newline at end of file diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java b/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java index 07957de80..00d58f557 100644 --- a/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java +++ b/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java @@ -300,9 +300,20 @@ public class RestActionMapper extends DefaultActionMapper { private void handleDynamicMethodInvocation(ActionMapping mapping, String name) { int exclamation = name.lastIndexOf("!"); if (exclamation != -1) { - mapping.setName(name.substring(0, exclamation)); + String actionName = name.substring(0, exclamation); + String actionMethod = name.substring(exclamation + 1); + + // WW-4585 + // add any ; appendix to name, it will be handled later in getMapping method + int scPos = actionMethod.indexOf(';'); + if (scPos != -1) { + actionName = actionName + actionMethod.substring(scPos); + actionMethod = actionMethod.substring(0, scPos); + } + + mapping.setName(actionName); if (allowDynamicMethodCalls) { - mapping.setMethod(name.substring(exclamation + 1)); + mapping.setMethod(actionMethod); } else { mapping.setMethod(null); } diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java index f46a20376..d80c33bda 100644 --- a/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java @@ -27,6 +27,8 @@ import org.springframework.mock.web.MockHttpServletResponse; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_CREATED; @@ -133,7 +135,7 @@ public class DefaultHttpHeadersTest extends TestCase { Date now = new Date(); DefaultHttpHeaders headers = new DefaultHttpHeaders() .lastModified(now); - mockRequest.addHeader("If-Modified-Since", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(now)); + mockRequest.addHeader("If-Modified-Since", getGMTDateFormat().format(now)); headers.apply(mockRequest, mockResponse, new Object()); assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); @@ -152,7 +154,7 @@ public class DefaultHttpHeadersTest extends TestCase { public void testLastModifiedSince() { Date now = new Date(); DefaultHttpHeaders headers = new DefaultHttpHeaders().lastModified(now); - mockRequest.addHeader("If-Modified-Since", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(now)); + mockRequest.addHeader("If-Modified-Since", getGMTDateFormat().format(now)); headers.apply(mockRequest, mockResponse, new Object()); assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); @@ -161,7 +163,7 @@ public class DefaultHttpHeadersTest extends TestCase { public void testLastModifiedSinceIsOlder() { Date now = new Date(); DefaultHttpHeaders headers = new DefaultHttpHeaders().lastModified(now); - mockRequest.addHeader("If-Modified-Since", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(new Date(now.getTime() - 1000 * 60 * 60))); + mockRequest.addHeader("If-Modified-Since", getGMTDateFormat().format(new Date(now.getTime() - 1000 * 60 * 60))); headers.apply(mockRequest, mockResponse, new Object()); assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); @@ -173,7 +175,7 @@ public class DefaultHttpHeadersTest extends TestCase { .lastModified(now) .withETag("asdf"); mockRequest.addHeader("If-None-Match", "asdf"); - mockRequest.addHeader("If-Modified-Since", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(now)); + mockRequest.addHeader("If-Modified-Since", getGMTDateFormat().format(now)); headers.apply(mockRequest, mockResponse, new Object()); assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus()); @@ -219,4 +221,10 @@ public class DefaultHttpHeadersTest extends TestCase { assertEquals(SC_OK, mockResponse.getStatus()); } + + private SimpleDateFormat getGMTDateFormat() { + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + return format; + } } diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java index d2e77779d..3de37d454 100644 --- a/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java @@ -183,7 +183,7 @@ public class RestActionMapperTest extends TestCase { req.setRequestURI("/myapp/animals/dog/fido!update;jsessionid=29fefpv23do1g"); req.setServletPath("/animals/dog/fido"); req.setMethod("GET"); - + ActionMapping mapping = mapper.getMapping(req, configManager); assertEquals("/animals", mapping.getNamespace()); @@ -196,10 +196,10 @@ public class RestActionMapperTest extends TestCase { req.setRequestURI("/myapp/animals/dog/fido!update;jsessionid=29fefpv23do1g"); req.setServletPath("/animals/dog/fido"); req.setMethod("GET"); - + // allow DMI mapper.setAllowDynamicMethodCalls("true"); - + ActionMapping mapping = mapper.getMapping(req, configManager); assertEquals("/animals", mapping.getNamespace()); diff --git a/pom.xml b/pom.xml index afa95f2f7..e40681972 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,25 @@ ../maven/struts-annotations + + jdk8 + + 1.8 + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + -Xdoclint:none + + + + + +