Simplified QuotedStringTokenizer (#9729)
Simplified QuotedStringTokenizer #9729 * Now implements a simple subset of `quoted-string` from RFC9110 * introduced builder * Extracted QuotedStringTokenizer interface and re-introduced the legacy implementation * Re-introduced the ability to have unescaped \ in filenames * Whitespace is Character.isWhiteSpace * Disable test pending RFC8187 * No OWS around =
This commit is contained in:
parent
3e5d479f39
commit
068a60a868
|
@ -20,7 +20,6 @@ import java.time.Instant;
|
|||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
|
||||
|
@ -306,8 +305,8 @@ public final class EtagUtils
|
|||
return false;
|
||||
|
||||
// compare unquoted strong etags
|
||||
etag = etagQuoted ? QuotedStringTokenizer.unquote(etag) : etag;
|
||||
etagWithOptionalSuffix = etagSuffixQuoted ? QuotedStringTokenizer.unquote(etagWithOptionalSuffix) : etagWithOptionalSuffix;
|
||||
etag = etagQuoted ? QuotedCSV.unquote(etag) : etag;
|
||||
etagWithOptionalSuffix = etagSuffixQuoted ? QuotedCSV.unquote(etagWithOptionalSuffix) : etagWithOptionalSuffix;
|
||||
separator = etagWithOptionalSuffix.lastIndexOf(ETAG_SEPARATOR);
|
||||
if (separator > 0)
|
||||
return etag.regionMatches(0, etagWithOptionalSuffix, 0, separator);
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
|
@ -26,6 +26,18 @@ import org.eclipse.jetty.util.StringUtil;
|
|||
*/
|
||||
public class HttpField
|
||||
{
|
||||
/**
|
||||
* A constant {@link QuotedStringTokenizer} configured for quoting/tokenizing {@code parameters} lists as defined by
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc9110#name-parameters">RFC9110</a>
|
||||
*/
|
||||
public static final QuotedStringTokenizer PARAMETER_TOKENIZER = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().allowEmbeddedQuotes().returnQuotes().build();
|
||||
|
||||
/**
|
||||
* A constant {@link QuotedStringTokenizer} configured for quoting/tokenizing a single {@code parameter} as defined by
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc9110#name-parameters">RFC9110</a>
|
||||
*/
|
||||
public static final QuotedStringTokenizer NAME_VALUE_TOKENIZER = QuotedStringTokenizer.builder().delimiters("=").build();
|
||||
|
||||
private static final String __zeroQuality = "q=0";
|
||||
private final HttpHeader _header;
|
||||
private final String _name;
|
||||
|
@ -67,37 +79,38 @@ public class HttpField
|
|||
*
|
||||
* </PRE>
|
||||
*
|
||||
* @param value The Field value, possibly with parameters.
|
||||
* @param valueParams The Field value, possibly with parameters.
|
||||
* @param parameters A map to populate with the parameters, or null
|
||||
* @return The value.
|
||||
*/
|
||||
public static String getValueParameters(String value, Map<String, String> parameters)
|
||||
public static String getValueParameters(String valueParams, Map<String, String> parameters)
|
||||
{
|
||||
if (value == null)
|
||||
if (valueParams == null)
|
||||
return null;
|
||||
|
||||
int i = value.indexOf(';');
|
||||
if (i < 0)
|
||||
return value;
|
||||
if (parameters == null)
|
||||
return value.substring(0, i).trim();
|
||||
|
||||
StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true);
|
||||
while (tok1.hasMoreTokens())
|
||||
Iterator<String> tokens = PARAMETER_TOKENIZER.tokenize(valueParams);
|
||||
if (!tokens.hasNext())
|
||||
return null;
|
||||
String value = tokens.next();
|
||||
if (parameters != null)
|
||||
{
|
||||
String token = tok1.nextToken();
|
||||
StringTokenizer tok2 = new QuotedStringTokenizer(token, "= ");
|
||||
if (tok2.hasMoreTokens())
|
||||
while (tokens.hasNext())
|
||||
{
|
||||
String paramName = tok2.nextToken();
|
||||
String paramVal = null;
|
||||
if (tok2.hasMoreTokens())
|
||||
paramVal = tok2.nextToken();
|
||||
parameters.put(paramName, paramVal);
|
||||
String token = tokens.next();
|
||||
|
||||
Iterator<String> nameValue = NAME_VALUE_TOKENIZER.tokenize(token);
|
||||
if (nameValue.hasNext())
|
||||
{
|
||||
String paramName = nameValue.next();
|
||||
String paramVal = null;
|
||||
if (nameValue.hasNext())
|
||||
paramVal = nameValue.next();
|
||||
parameters.put(paramName, paramVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value.substring(0, i).trim();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.nio.file.StandardCopyOption;
|
|||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -67,6 +68,12 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||
public class MultiPart
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MultiPart.class);
|
||||
private static final QuotedStringTokenizer CONTENT_DISPOSITION_TOKENIZER = QuotedStringTokenizer.builder()
|
||||
.delimiters(";")
|
||||
.ignoreOptionalWhiteSpace()
|
||||
.allowEmbeddedQuotes()
|
||||
.allowEscapeOnlyForQuotes()
|
||||
.build();
|
||||
private static final int MAX_BOUNDARY_LENGTH = 70;
|
||||
|
||||
private MultiPart()
|
||||
|
@ -86,7 +93,7 @@ public class MultiPart
|
|||
{
|
||||
Map<String, String> parameters = new HashMap<>();
|
||||
HttpField.valueParameters(contentType, parameters);
|
||||
return QuotedStringTokenizer.unquote(parameters.get("boundary"));
|
||||
return CONTENT_DISPOSITION_TOKENIZER.unquote(parameters.get("boundary"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1609,16 +1616,15 @@ public class MultiPart
|
|||
{
|
||||
String namePrefix = "name=";
|
||||
String fileNamePrefix = "filename=";
|
||||
QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(headerValue, ";", false, true);
|
||||
while (tokenizer.hasMoreTokens())
|
||||
for (Iterator<String> tokens = CONTENT_DISPOSITION_TOKENIZER.tokenize(headerValue); tokens.hasNext();)
|
||||
{
|
||||
String token = tokenizer.nextToken().trim();
|
||||
String token = tokens.next();
|
||||
String lowerToken = StringUtil.asciiToLowerCase(token);
|
||||
if (lowerToken.startsWith(namePrefix))
|
||||
{
|
||||
int index = lowerToken.indexOf(namePrefix);
|
||||
String value = token.substring(index + namePrefix.length()).trim();
|
||||
name = QuotedStringTokenizer.unquoteOnly(value);
|
||||
name = CONTENT_DISPOSITION_TOKENIZER.unquote(value); // TODO should the tokenizer be returnQuotes == false ?
|
||||
}
|
||||
else if (lowerToken.startsWith(fileNamePrefix))
|
||||
{
|
||||
|
@ -1648,11 +1654,7 @@ public class MultiPart
|
|||
}
|
||||
else
|
||||
{
|
||||
// unquote the string, but allow any backslashes that don't
|
||||
// form a valid escape sequence to remain as many browsers
|
||||
// even on *nix systems will not escape a filename containing
|
||||
// backslashes
|
||||
return QuotedStringTokenizer.unquoteOnly(value, true);
|
||||
return CONTENT_DISPOSITION_TOKENIZER.unquote(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import java.util.concurrent.CompletableFuture;
|
|||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -402,10 +401,10 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
|
|||
String value = "form-data";
|
||||
String name = part.getName();
|
||||
if (name != null)
|
||||
value += "; name=" + QuotedStringTokenizer.quote(name);
|
||||
value += "; name=" + QuotedCSV.quote(name);
|
||||
String fileName = part.getFileName();
|
||||
if (fileName != null)
|
||||
value += "; filename=" + QuotedStringTokenizer.quote(fileName);
|
||||
value += "; filename=" + QuotedCSV.quote(fileName);
|
||||
return HttpFields.build(headers).put(HttpHeader.CONTENT_DISPOSITION, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,6 @@ import java.util.Arrays;
|
|||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
|
||||
/**
|
||||
* Implements a quoted comma separated list of values
|
||||
* in accordance with RFC7230.
|
||||
|
@ -30,11 +28,6 @@ import org.eclipse.jetty.util.QuotedStringTokenizer;
|
|||
*/
|
||||
public class QuotedCSV extends QuotedCSVParser implements Iterable<String>
|
||||
{
|
||||
/**
|
||||
* ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting.
|
||||
*/
|
||||
public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;=,";
|
||||
|
||||
/**
|
||||
* Join a list into Quoted CSV string
|
||||
*
|
||||
|
@ -104,7 +97,7 @@ public class QuotedCSV extends QuotedCSVParser implements Iterable<String>
|
|||
builder.append(", ");
|
||||
else
|
||||
needsDelim = true;
|
||||
QuotedStringTokenizer.quoteIfNeeded(builder, value, ABNF_REQUIRED_QUOTING);
|
||||
LIST_TOKENIZER.quoteIfNeeded(builder, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,13 +13,12 @@
|
|||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
|
||||
/**
|
||||
* Implements a quoted comma separated list parser
|
||||
* in accordance with RFC7230.
|
||||
* in accordance with <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-5.6">RFC9110 section 5.6</a>.
|
||||
* OWS is removed and quoted characters ignored for parsing.
|
||||
*
|
||||
* @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
|
||||
* @see "https://tools.ietf.org/html/rfc7230#section-7"
|
||||
*/
|
||||
public abstract class QuotedCSVParser
|
||||
{
|
||||
|
@ -28,6 +27,15 @@ public abstract class QuotedCSVParser
|
|||
VALUE, PARAM_NAME, PARAM_VALUE
|
||||
}
|
||||
|
||||
public static final String DELIMITERS = ",;=";
|
||||
public static final QuotedStringTokenizer LIST_TOKENIZER = QuotedStringTokenizer.builder()
|
||||
.delimiters(DELIMITERS)
|
||||
.ignoreOptionalWhiteSpace()
|
||||
.allowEmbeddedQuotes()
|
||||
.returnDelimiters()
|
||||
.returnQuotes()
|
||||
.build();
|
||||
|
||||
protected final boolean _keepQuotes;
|
||||
|
||||
public QuotedCSVParser(boolean keepQuotes)
|
||||
|
@ -35,52 +43,19 @@ public abstract class QuotedCSVParser
|
|||
_keepQuotes = keepQuotes;
|
||||
}
|
||||
|
||||
public static String quote(String s)
|
||||
{
|
||||
return LIST_TOKENIZER.quote(s);
|
||||
}
|
||||
|
||||
public static String quoteIfNeeded(String s)
|
||||
{
|
||||
return LIST_TOKENIZER.quoteIfNeeded(s);
|
||||
}
|
||||
|
||||
public static String unquote(String s)
|
||||
{
|
||||
// handle trivial cases
|
||||
int l = s.length();
|
||||
if (s == null || l == 0)
|
||||
return s;
|
||||
|
||||
// Look for any quotes
|
||||
int i = 0;
|
||||
for (; i < l; i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (c == '"')
|
||||
break;
|
||||
}
|
||||
if (i == l)
|
||||
return s;
|
||||
|
||||
boolean quoted = true;
|
||||
boolean sloshed = false;
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.append(s, 0, i);
|
||||
i++;
|
||||
for (; i < l; i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (quoted)
|
||||
{
|
||||
if (sloshed)
|
||||
{
|
||||
buffer.append(c);
|
||||
sloshed = false;
|
||||
}
|
||||
else if (c == '"')
|
||||
quoted = false;
|
||||
else if (c == '\\')
|
||||
sloshed = true;
|
||||
else
|
||||
buffer.append(c);
|
||||
}
|
||||
else if (c == '"')
|
||||
quoted = true;
|
||||
else
|
||||
buffer.append(c);
|
||||
}
|
||||
return buffer.toString();
|
||||
return LIST_TOKENIZER.unquote(s);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,6 +68,11 @@ public abstract class QuotedCSVParser
|
|||
if (value == null)
|
||||
return;
|
||||
|
||||
// The parser does not actually use LIST_TOKENIZER as we wish to keep the tokens in the StringBuffer
|
||||
// and allow them to be mutated by the callbacks.
|
||||
|
||||
// TODO update to RFC9110, specifically no OWS around '='
|
||||
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
int l = value.length();
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -189,4 +191,16 @@ public class HttpFieldTest
|
|||
assertEquals("X-My-Custom-Header", field.getName());
|
||||
assertEquals("something", field.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetValueParameters()
|
||||
{
|
||||
Map<String, String> map = new HashMap<>();
|
||||
String value = HttpField.getValueParameters("Value ; p1=v1;p2=v2 ; p3=\" v ; 3=three\"", map);
|
||||
assertThat(value, is("Value"));
|
||||
assertThat(map.size(), is(3));
|
||||
assertThat(map.get("p1"), is("v1"));
|
||||
assertThat(map.get("p2"), is("v2"));
|
||||
assertThat(map.get("p3"), is(" v ; 3=three"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.nio.file.Path;
|
|||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -154,7 +155,7 @@ public class MultiPartCaptureTest
|
|||
String boundaryAttribute = "boundary=";
|
||||
int boundaryIndex = expectations.contentType.indexOf(boundaryAttribute);
|
||||
assertThat(boundaryIndex, greaterThan(0));
|
||||
String boundary = QuotedStringTokenizer.unquoteOnly(expectations.contentType.substring(boundaryIndex + boundaryAttribute.length()));
|
||||
String boundary = HttpField.PARAMETER_TOKENIZER.unquote(expectations.contentType.substring(boundaryIndex + boundaryAttribute.length()));
|
||||
|
||||
TestPartsListener listener = new TestPartsListener(expectations);
|
||||
MultiPart.Parser parser = new MultiPart.Parser(boundary, listener);
|
||||
|
@ -289,10 +290,10 @@ public class MultiPartCaptureTest
|
|||
if (StringUtil.isBlank(contentType))
|
||||
return defaultCharset;
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentType, ";", false, false);
|
||||
while (tok.hasMoreTokens())
|
||||
QuotedStringTokenizer tok = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().build();
|
||||
for (Iterator<String> i = tok.tokenize(contentType); i.hasNext();)
|
||||
{
|
||||
String str = tok.nextToken().trim();
|
||||
String str = i.next().trim();
|
||||
if (str.startsWith("charset="))
|
||||
{
|
||||
return str.substring("charset=".length());
|
||||
|
|
|
@ -25,15 +25,13 @@ import java.util.concurrent.ExecutionException;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.RetainableByteBuffer;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.ISO_8859_1;
|
||||
|
@ -677,7 +675,7 @@ public class MultiPartFormDataTest
|
|||
|
||||
String contents = """
|
||||
--AaB03x\r
|
||||
content-disposition: form-data; name="stuff"; filename="Taken on Aug 22 \\ 2012.jpg"\r
|
||||
content-disposition: form-data; name="stuff"; filename="C:\\Pictures\\4th May 2012.jpg"\r
|
||||
Content-Type: text/plain\r
|
||||
\r
|
||||
stuffaaa\r
|
||||
|
@ -689,7 +687,7 @@ public class MultiPartFormDataTest
|
|||
{
|
||||
assertThat(parts.size(), is(1));
|
||||
MultiPart.Part part = parts.get(0);
|
||||
assertThat(part.getFileName(), is("Taken on Aug 22 \\ 2012.jpg"));
|
||||
assertThat(part.getFileName(), is("C:\\Pictures\\4th May 2012.jpg"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -718,7 +716,10 @@ public class MultiPartFormDataTest
|
|||
}
|
||||
}
|
||||
|
||||
// TODO We need to implement RFC8187 to lookfor the filename*= attribute. Meanwhile, it appears
|
||||
// that escaping is only done for quote in these filenames.
|
||||
@Test
|
||||
@Disabled
|
||||
public void testCorrectlyEncodedMSFilename() throws Exception
|
||||
{
|
||||
MultiPartFormData formData = new MultiPartFormData("AaB03x");
|
||||
|
|
|
@ -144,7 +144,7 @@ public class QuotedCSVTest
|
|||
assertThat(QuotedCSV.unquote("\"\""), is(""));
|
||||
assertThat(QuotedCSV.unquote("foo"), is("foo"));
|
||||
assertThat(QuotedCSV.unquote("\"foo\""), is("foo"));
|
||||
assertThat(QuotedCSV.unquote("f\"o\"o"), is("foo"));
|
||||
assertThat(QuotedCSV.unquote("f\"o\"o"), is("f\"o\"o"));
|
||||
assertThat(QuotedCSV.unquote("\"\\\"foo\""), is("\"foo"));
|
||||
assertThat(QuotedCSV.unquote("\\foo"), is("\\foo"));
|
||||
}
|
||||
|
@ -157,6 +157,6 @@ public class QuotedCSVTest
|
|||
assertThat(QuotedCSV.join(Collections.singletonList("hi")), is("hi"));
|
||||
assertThat(QuotedCSV.join("hi", "ho"), is("hi, ho"));
|
||||
assertThat(QuotedCSV.join("h i", "h,o"), is("\"h i\", \"h,o\""));
|
||||
assertThat(QuotedCSV.join("h\"i", "h\to"), is("\"h\\\"i\", \"h\\to\""));
|
||||
assertThat(QuotedCSV.join("h\"i", "h\to"), is("\"h\\\"i\", \"h\to\""));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.eclipse.jetty.server.Request;
|
|||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
|
@ -364,9 +363,9 @@ public abstract class ProxyHandler extends Handler.Abstract
|
|||
// server (so the scheme is http), but securely with the forward proxy (so isSecure() is true).
|
||||
String protoAttr = scheme == null ? (clientToProxyRequest.isSecure() ? "https" : "http") : scheme;
|
||||
String forwardedValue = "by=%s;for=%s;host=%s;proto=%s".formatted(
|
||||
QuotedStringTokenizer.quote(byAttr),
|
||||
QuotedStringTokenizer.quote(forAttr),
|
||||
QuotedStringTokenizer.quote(hostAttr),
|
||||
HttpField.PARAMETER_TOKENIZER.quote(byAttr),
|
||||
HttpField.PARAMETER_TOKENIZER.quote(forAttr),
|
||||
HttpField.PARAMETER_TOKENIZER.quote(hostAttr),
|
||||
protoAttr
|
||||
);
|
||||
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
|
||||
package org.eclipse.jetty.security.authentication;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.BitSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
@ -49,12 +51,13 @@ import org.slf4j.LoggerFactory;
|
|||
public class DigestAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DigestAuthenticator.class);
|
||||
private static final QuotedStringTokenizer TOKENIZER = QuotedStringTokenizer.builder().delimiters("=, ").returnDelimiters().allowEmbeddedQuotes().build();
|
||||
|
||||
private final SecureRandom _random = new SecureRandom();
|
||||
private final Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
|
||||
private long _maxNonceAgeMs = 60 * 1000;
|
||||
private int _maxNC = 1024;
|
||||
private ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
|
||||
private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
@Override
|
||||
public void setConfiguration(Configuration configuration)
|
||||
|
@ -105,14 +108,13 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Credentials: {}", credentials);
|
||||
QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
|
||||
final Digest digest = new Digest(req.getMethod());
|
||||
String last = null;
|
||||
String name = null;
|
||||
|
||||
while (tokenizer.hasMoreTokens())
|
||||
for (Iterator<String> i = TOKENIZER.tokenize(credentials); i.hasNext();)
|
||||
{
|
||||
String tok = tokenizer.nextToken();
|
||||
String tok = i.next();
|
||||
char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
|
||||
|
||||
switch (c)
|
||||
|
@ -268,7 +270,7 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
|
||||
public boolean seen(int count)
|
||||
{
|
||||
try (AutoLock l = _lock.lock())
|
||||
try (AutoLock ignored = _lock.lock())
|
||||
{
|
||||
if (count >= _seen.size())
|
||||
return true;
|
||||
|
@ -281,6 +283,7 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
|
||||
private static class Digest extends Credential
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2484639019549527724L;
|
||||
final String method;
|
||||
String username = "";
|
||||
|
|
|
@ -182,7 +182,7 @@ public final class HttpCookieUtils
|
|||
{
|
||||
builder.append(";Domain=");
|
||||
if (quoteDomain)
|
||||
QuotedStringTokenizer.quoteOnly(builder, domain);
|
||||
HttpField.PARAMETER_TOKENIZER.quote(builder, domain);
|
||||
else
|
||||
builder.append(domain);
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ public final class HttpCookieUtils
|
|||
{
|
||||
builder.append(";Path=");
|
||||
if (quotePath)
|
||||
QuotedStringTokenizer.quoteOnly(builder, path);
|
||||
HttpField.PARAMETER_TOKENIZER.quote(builder, path);
|
||||
else
|
||||
builder.append(path);
|
||||
}
|
||||
|
@ -407,7 +407,7 @@ public final class HttpCookieUtils
|
|||
private static void quoteIfNeededAndAppend(String text, StringBuilder builder)
|
||||
{
|
||||
if (isQuoteNeeded(text))
|
||||
QuotedStringTokenizer.quoteOnly(builder, text);
|
||||
HttpField.PARAMETER_TOKENIZER.quote(builder, text);
|
||||
else
|
||||
builder.append(text);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import org.eclipse.jetty.server.Response;
|
|||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -384,9 +383,7 @@ public class ErrorHandler implements Request.Handler
|
|||
}
|
||||
|
||||
writer.append(json.entrySet().stream()
|
||||
.map(e -> QuotedStringTokenizer.quote(e.getKey()) +
|
||||
":" +
|
||||
QuotedStringTokenizer.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.map(e -> HttpField.NAME_VALUE_TOKENIZER.quote(e.getKey()) + ":" + HttpField.NAME_VALUE_TOKENIZER.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.collect(Collectors.joining(",\n", "{\n", "\n}")));
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.List;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.eclipse.jetty.http.ByteRange;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
@ -35,7 +36,6 @@ import org.eclipse.jetty.io.content.ByteBufferContentSource;
|
|||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
@ -96,7 +96,7 @@ public class MultiPartByteRangesTest
|
|||
content.close();
|
||||
|
||||
response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/byteranges; boundary=" + QuotedStringTokenizer.quote(boundary));
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/byteranges; boundary=" + HttpField.NAME_VALUE_TOKENIZER.quote(boundary));
|
||||
Content.copy(content, response, callback);
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -61,7 +61,6 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
|||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.resource.FileSystemPool;
|
||||
import org.eclipse.jetty.util.resource.ResourceFactory;
|
||||
|
@ -1444,7 +1443,7 @@ public class ResourceHandlerTest
|
|||
String eTag1 = response1.get(ETAG);
|
||||
assertThat(eTag1, endsWith("--gzip\""));
|
||||
assertThat(eTag1, startsWith("W/"));
|
||||
String nakedEtag1 = QuotedStringTokenizer.unquote(eTag1.substring(2));
|
||||
String nakedEtag1 = HttpField.PARAMETER_TOKENIZER.unquote(eTag1.substring(2));
|
||||
// Load big.txt.gz into a byte array and assert its contents byte per byte.
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
|
||||
{
|
||||
|
@ -1464,7 +1463,7 @@ public class ResourceHandlerTest
|
|||
assertThat(response2.get(CONTENT_ENCODING), is(nullValue()));
|
||||
String eTag2 = response2.get(ETAG);
|
||||
assertThat(eTag2, startsWith("W/"));
|
||||
String nakedEtag2 = QuotedStringTokenizer.unquote(eTag2.substring(2));
|
||||
String nakedEtag2 = HttpField.PARAMETER_TOKENIZER.unquote(eTag2.substring(2));
|
||||
assertThat(nakedEtag1, startsWith(nakedEtag2));
|
||||
assertThat(response2.getContent(), startsWith(" 1\tThis is a big file"));
|
||||
assertThat(response2.getContent(), endsWith(" 400\tThis is a big file\n"));
|
||||
|
|
|
@ -0,0 +1,530 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class LegacyQuotedStringTokenizer implements QuotedStringTokenizer
|
||||
{
|
||||
private final String _delim;
|
||||
private final boolean _returnQuotes;
|
||||
private final boolean _returnDelimiters;
|
||||
private final boolean _singleQuotes;
|
||||
|
||||
LegacyQuotedStringTokenizer(String delim,
|
||||
boolean returnDelimiters,
|
||||
boolean returnQuotes,
|
||||
boolean singleQuotes)
|
||||
{
|
||||
_delim = delim == null ? "\t\n\r" : delim;
|
||||
_returnDelimiters = returnDelimiters;
|
||||
_returnQuotes = returnQuotes;
|
||||
_singleQuotes = singleQuotes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> tokenize(String string)
|
||||
{
|
||||
LegacyTokenizer tokenizer = new LegacyTokenizer(string);
|
||||
return new Iterator<>()
|
||||
{
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return tokenizer.hasMoreTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next()
|
||||
{
|
||||
return tokenizer.nextToken();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsQuoting(char c)
|
||||
{
|
||||
return LegacyTokenizer.needsQuoting(c, _delim);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String quoteIfNeeded(String s)
|
||||
{
|
||||
return LegacyTokenizer.quoteIfNeeded(s, _delim);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quoteIfNeeded(StringBuilder buf, String str)
|
||||
{
|
||||
LegacyTokenizer.quoteIfNeeded(buf, str, _delim);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quote(Appendable buffer, String input)
|
||||
{
|
||||
LegacyTokenizer.quote(buffer, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String unquote(String s)
|
||||
{
|
||||
return LegacyTokenizer.unquote(s);
|
||||
}
|
||||
|
||||
private class LegacyTokenizer extends StringTokenizer
|
||||
{
|
||||
private final String _string;
|
||||
private final StringBuffer _token;
|
||||
private boolean _hasToken = false;
|
||||
private int _i = 0;
|
||||
|
||||
public LegacyTokenizer(String str)
|
||||
{
|
||||
super("");
|
||||
_string = str;
|
||||
|
||||
if (_delim.indexOf('\'') >= 0 ||
|
||||
_delim.indexOf('"') >= 0)
|
||||
throw new Error("Can't use quotes as delimiters: " + _delim);
|
||||
|
||||
_token = new StringBuffer(_string.length() > 1024 ? 512 : _string.length() / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreTokens()
|
||||
{
|
||||
// Already found a token
|
||||
if (_hasToken)
|
||||
return true;
|
||||
|
||||
int state = 0;
|
||||
boolean escape = false;
|
||||
while (_i < _string.length())
|
||||
{
|
||||
char c = _string.charAt(_i++);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0: // Start
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
{
|
||||
_token.append(c);
|
||||
return _hasToken = true;
|
||||
}
|
||||
}
|
||||
else if (c == '\'' && _singleQuotes)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 2;
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
_hasToken = true;
|
||||
state = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // Token
|
||||
_hasToken = true;
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
_i--;
|
||||
return _hasToken;
|
||||
}
|
||||
else if (c == '\'' && _singleQuotes)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 2;
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Single Quote
|
||||
_hasToken = true;
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
_token.append(c);
|
||||
}
|
||||
else if (c == '\'')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 1;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // Double Quote
|
||||
_hasToken = true;
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
_token.append(c);
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 1;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
return _hasToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextToken()
|
||||
throws NoSuchElementException
|
||||
{
|
||||
if (!hasMoreTokens() || _token == null)
|
||||
throw new NoSuchElementException();
|
||||
String t = _token.toString();
|
||||
_token.setLength(0);
|
||||
_hasToken = false;
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextToken(String delim)
|
||||
throws NoSuchElementException
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreElements()
|
||||
{
|
||||
return hasMoreTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nextElement()
|
||||
throws NoSuchElementException
|
||||
{
|
||||
return nextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
*/
|
||||
@Override
|
||||
public int countTokens()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static boolean needsQuoting(char c, String delim)
|
||||
{
|
||||
return c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string.
|
||||
* The string is quoted only if quoting is required due to
|
||||
* embedded delimiters, quote characters or the
|
||||
* empty string.
|
||||
*
|
||||
* @param s The string to quote.
|
||||
* @param delim the delimiter to use to quote the string
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String quoteIfNeeded(String s, String delim)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() == 0)
|
||||
return "\"\"";
|
||||
|
||||
for (int i = 0; i < s.length(); i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (needsQuoting(c, delim))
|
||||
{
|
||||
StringBuffer b = new StringBuffer(s.length() + 8);
|
||||
quote(b, s);
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append into buf the provided string, adding quotes if needed.
|
||||
* <p>
|
||||
* Quoting is determined if any of the characters in the {@code delim} are found in the input {@code str}.
|
||||
*
|
||||
* @param buf the buffer to append to
|
||||
* @param str the string to possibly quote
|
||||
* @param delim the delimiter characters that will trigger automatic quoting
|
||||
*/
|
||||
public static void quoteIfNeeded(StringBuilder buf, String str, String delim)
|
||||
{
|
||||
if (str == null)
|
||||
return;
|
||||
// check for delimiters in input string
|
||||
int len = str.length();
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
int ch;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ch = str.codePointAt(i);
|
||||
if (delim.indexOf(ch) >= 0)
|
||||
{
|
||||
// found a delimiter codepoint. we need to quote it.
|
||||
quote(buf, str);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no special delimiters used, no quote needed.
|
||||
buf.append(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string.
|
||||
* The string is quoted only if quoting is required due to
|
||||
* embedded delimiters, quote characters or the
|
||||
* empty string.
|
||||
*
|
||||
* @param s The string to quote.
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String quote(String s)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() == 0)
|
||||
return "\"\"";
|
||||
|
||||
StringBuffer b = new StringBuffer(s.length() + 8);
|
||||
quote(b, s);
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private static final char[] escapes = new char[32];
|
||||
|
||||
static
|
||||
{
|
||||
Arrays.fill(escapes, (char)0xFFFF);
|
||||
escapes['\b'] = 'b';
|
||||
escapes['\t'] = 't';
|
||||
escapes['\n'] = 'n';
|
||||
escapes['\f'] = 'f';
|
||||
escapes['\r'] = 'r';
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string into an Appendable.
|
||||
* The characters ", \, \n, \r, \t, \f and \b are escaped
|
||||
*
|
||||
* @param buffer The Appendable
|
||||
* @param input The String to quote.
|
||||
*/
|
||||
public static void quote(Appendable buffer, String input)
|
||||
{
|
||||
if (input == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
buffer.append('"');
|
||||
for (int i = 0; i < input.length(); ++i)
|
||||
{
|
||||
char c = input.charAt(i);
|
||||
if (c >= 32)
|
||||
{
|
||||
if (c == '"' || c == '\\')
|
||||
buffer.append('\\');
|
||||
buffer.append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
char escape = escapes[c];
|
||||
if (escape == 0xFFFF)
|
||||
{
|
||||
// Unicode escape
|
||||
buffer.append('\\').append('u').append('0').append('0');
|
||||
if (c < 0x10)
|
||||
buffer.append('0');
|
||||
buffer.append(Integer.toString(c, 16));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.append('\\').append(escape);
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.append('"');
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
|
||||
public static String unquote(String s)
|
||||
{
|
||||
return unquote(s, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquote a string.
|
||||
*
|
||||
* @param s The string to unquote.
|
||||
* @param lenient true if unquoting should be lenient to escaped content, leaving some alone, false if string unescaping
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String unquote(String s, boolean lenient)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() < 2)
|
||||
return s;
|
||||
|
||||
char first = s.charAt(0);
|
||||
char last = s.charAt(s.length() - 1);
|
||||
if (first != last || (first != '"' && first != '\''))
|
||||
return s;
|
||||
|
||||
StringBuilder b = new StringBuilder(s.length() - 2);
|
||||
boolean escape = false;
|
||||
for (int i = 1; i < s.length() - 1; i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
switch (c)
|
||||
{
|
||||
case 'n':
|
||||
b.append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
b.append('\r');
|
||||
break;
|
||||
case 't':
|
||||
b.append('\t');
|
||||
break;
|
||||
case 'f':
|
||||
b.append('\f');
|
||||
break;
|
||||
case 'b':
|
||||
b.append('\b');
|
||||
break;
|
||||
case '\\':
|
||||
b.append('\\');
|
||||
break;
|
||||
case '/':
|
||||
b.append('/');
|
||||
break;
|
||||
case '"':
|
||||
b.append('"');
|
||||
break;
|
||||
case 'u':
|
||||
b.append((char)(
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 24) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 16) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 8) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)))
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
if (lenient && !isValidEscaping(c))
|
||||
{
|
||||
b.append('\\');
|
||||
}
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that char c (which is preceded by a backslash) is a valid
|
||||
* escape sequence.
|
||||
*/
|
||||
private static boolean isValidEscaping(char c)
|
||||
{
|
||||
return ((c == 'n') || (c == 'r') || (c == 't') ||
|
||||
(c == 'f') || (c == 'b') || (c == '\\') ||
|
||||
(c == '/') || (c == '"') || (c == 'u'));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,312 +13,33 @@
|
|||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* StringTokenizer with Quoting support.
|
||||
*
|
||||
* This class is a copy of the java.util.StringTokenizer API and
|
||||
* the behaviour is the same, except that single and double quoted
|
||||
* string values are recognised.
|
||||
* Delimiters within quotes are not considered delimiters.
|
||||
* Quotes can be escaped with '\'.
|
||||
*
|
||||
* @see java.util.StringTokenizer
|
||||
* A Tokenizer that splits a string into parts, allowing for quotes.
|
||||
*/
|
||||
public class QuotedStringTokenizer
|
||||
extends StringTokenizer
|
||||
public interface QuotedStringTokenizer
|
||||
{
|
||||
private static final String __delim = "\t\n\r";
|
||||
private String _string;
|
||||
private String _delim = __delim;
|
||||
private boolean _returnQuotes = false;
|
||||
private boolean _returnDelimiters = false;
|
||||
private StringBuffer _token;
|
||||
private boolean _hasToken = false;
|
||||
private int _i = 0;
|
||||
private int _lastStart = 0;
|
||||
private boolean _double = true;
|
||||
private boolean _single = true;
|
||||
/**
|
||||
* A QuotedStringTokenizer for comma separated values with optional white space.
|
||||
*/
|
||||
QuotedStringTokenizer CSV = QuotedStringTokenizer.builder().delimiters(",").ignoreOptionalWhiteSpace().build();
|
||||
|
||||
public QuotedStringTokenizer(String str,
|
||||
String delim,
|
||||
boolean returnDelimiters,
|
||||
boolean returnQuotes)
|
||||
/**
|
||||
* @return A Builder for a {@link QuotedStringTokenizer}.
|
||||
*/
|
||||
static Builder builder()
|
||||
{
|
||||
super("");
|
||||
_string = str;
|
||||
if (delim != null)
|
||||
_delim = delim;
|
||||
_returnDelimiters = returnDelimiters;
|
||||
_returnQuotes = returnQuotes;
|
||||
|
||||
if (_delim.indexOf('\'') >= 0 ||
|
||||
_delim.indexOf('"') >= 0)
|
||||
throw new Error("Can't use quotes as delimiters: " + _delim);
|
||||
|
||||
_token = new StringBuffer(_string.length() > 1024 ? 512 : _string.length() / 2);
|
||||
}
|
||||
|
||||
public QuotedStringTokenizer(String str,
|
||||
String delim,
|
||||
boolean returnDelimiters)
|
||||
{
|
||||
this(str, delim, returnDelimiters, false);
|
||||
}
|
||||
|
||||
public QuotedStringTokenizer(String str,
|
||||
String delim)
|
||||
{
|
||||
this(str, delim, false, false);
|
||||
}
|
||||
|
||||
public QuotedStringTokenizer(String str)
|
||||
{
|
||||
this(str, null, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreTokens()
|
||||
{
|
||||
// Already found a token
|
||||
if (_hasToken)
|
||||
return true;
|
||||
|
||||
_lastStart = _i;
|
||||
|
||||
int state = 0;
|
||||
boolean escape = false;
|
||||
while (_i < _string.length())
|
||||
{
|
||||
char c = _string.charAt(_i++);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0: // Start
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
{
|
||||
_token.append(c);
|
||||
return _hasToken = true;
|
||||
}
|
||||
}
|
||||
else if (c == '\'' && _single)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 2;
|
||||
}
|
||||
else if (c == '\"' && _double)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
_hasToken = true;
|
||||
state = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1: // Token
|
||||
_hasToken = true;
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
_i--;
|
||||
return _hasToken;
|
||||
}
|
||||
else if (c == '\'' && _single)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 2;
|
||||
}
|
||||
else if (c == '\"' && _double)
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Single Quote
|
||||
_hasToken = true;
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
_token.append(c);
|
||||
}
|
||||
else if (c == '\'')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 1;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // Double Quote
|
||||
_hasToken = true;
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
_token.append(c);
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
state = 1;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
return _hasToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextToken()
|
||||
throws NoSuchElementException
|
||||
{
|
||||
if (!hasMoreTokens() || _token == null)
|
||||
throw new NoSuchElementException();
|
||||
String t = _token.toString();
|
||||
_token.setLength(0);
|
||||
_hasToken = false;
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextToken(String delim)
|
||||
throws NoSuchElementException
|
||||
{
|
||||
_delim = delim;
|
||||
_i = _lastStart;
|
||||
_token.setLength(0);
|
||||
_hasToken = false;
|
||||
return nextToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreElements()
|
||||
{
|
||||
return hasMoreTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nextElement()
|
||||
throws NoSuchElementException
|
||||
{
|
||||
return nextToken();
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented.
|
||||
* @param s The string to test
|
||||
* @return True if the string is quoted.
|
||||
*/
|
||||
@Override
|
||||
public int countTokens()
|
||||
static boolean isQuoted(String s)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string.
|
||||
* The string is quoted only if quoting is required due to
|
||||
* embedded delimiters, quote characters or the
|
||||
* empty string.
|
||||
*
|
||||
* @param s The string to quote.
|
||||
* @param delim the delimiter to use to quote the string
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String quoteIfNeeded(String s, String delim)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() == 0)
|
||||
return "\"\"";
|
||||
|
||||
for (int i = 0; i < s.length(); i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\' || c == '"' || c == '\'' || Character.isWhitespace(c) || delim.indexOf(c) >= 0)
|
||||
{
|
||||
StringBuffer b = new StringBuffer(s.length() + 8);
|
||||
quote(b, s);
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append into buf the provided string, adding quotes if needed.
|
||||
* <p>
|
||||
* Quoting is determined if any of the characters in the {@code delim} are found in the input {@code str}.
|
||||
*
|
||||
* @param buf the buffer to append to
|
||||
* @param str the string to possibly quote
|
||||
* @param delim the delimiter characters that will trigger automatic quoting
|
||||
*/
|
||||
public static void quoteIfNeeded(StringBuilder buf, String str, String delim)
|
||||
{
|
||||
if (str == null)
|
||||
return;
|
||||
// check for delimiters in input string
|
||||
int len = str.length();
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
int ch;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ch = str.codePointAt(i);
|
||||
if (delim.indexOf(ch) >= 0)
|
||||
{
|
||||
// found a delimiter codepoint. we need to quote it.
|
||||
quote(buf, str);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no special delimiters used, no quote needed.
|
||||
buf.append(str);
|
||||
return s != null && s.length() > 0 && s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -330,7 +51,7 @@ public class QuotedStringTokenizer
|
|||
* @param s The string to quote.
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String quote(String s)
|
||||
default String quote(String s)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
|
@ -342,283 +63,189 @@ public class QuotedStringTokenizer
|
|||
return b.toString();
|
||||
}
|
||||
|
||||
private static final char[] escapes = new char[32];
|
||||
|
||||
static
|
||||
{
|
||||
Arrays.fill(escapes, (char)0xFFFF);
|
||||
escapes['\b'] = 'b';
|
||||
escapes['\t'] = 't';
|
||||
escapes['\n'] = 'n';
|
||||
escapes['\f'] = 'f';
|
||||
escapes['\r'] = 'r';
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a string into an Appendable.
|
||||
* The characters ", \, \n, \r, \t, \f and \b are escaped
|
||||
* Quote a string into an Appendable, escaping any characters that
|
||||
* need to be escaped.
|
||||
*
|
||||
* @param buffer The Appendable
|
||||
* @param buffer The Appendable to append the quoted and escaped string into.
|
||||
* @param input The String to quote.
|
||||
*/
|
||||
public static void quote(Appendable buffer, String input)
|
||||
{
|
||||
if (input == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
buffer.append('"');
|
||||
for (int i = 0; i < input.length(); ++i)
|
||||
{
|
||||
char c = input.charAt(i);
|
||||
if (c >= 32)
|
||||
{
|
||||
if (c == '"' || c == '\\')
|
||||
buffer.append('\\');
|
||||
buffer.append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
char escape = escapes[c];
|
||||
if (escape == 0xFFFF)
|
||||
{
|
||||
// Unicode escape
|
||||
buffer.append('\\').append('u').append('0').append('0');
|
||||
if (c < 0x10)
|
||||
buffer.append('0');
|
||||
buffer.append(Integer.toString(c, 16));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.append('\\').append(escape);
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.append('"');
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
void quote(Appendable buffer, String input);
|
||||
|
||||
/**
|
||||
* Quote a string into an Appendable.
|
||||
* Only quotes and backslash are escaped.
|
||||
*
|
||||
* @param buffer The Appendable
|
||||
* @param input The String to quote.
|
||||
*/
|
||||
public static void quoteOnly(Appendable buffer, String input)
|
||||
{
|
||||
if (input == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
buffer.append('"');
|
||||
for (int i = 0; i < input.length(); ++i)
|
||||
{
|
||||
char c = input.charAt(i);
|
||||
if (c == '"' || c == '\\')
|
||||
buffer.append('\\');
|
||||
buffer.append(c);
|
||||
}
|
||||
buffer.append('"');
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
|
||||
public static String unquoteOnly(String s)
|
||||
{
|
||||
return unquoteOnly(s, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquote a string, NOT converting unicode sequences
|
||||
* Unquote a string and expand any escaped characters
|
||||
*
|
||||
* @param s The string to unquote.
|
||||
* @param lenient if true, will leave in backslashes that aren't valid escapes
|
||||
* @return quoted string
|
||||
* @return unquoted string with escaped characters expanded.
|
||||
*/
|
||||
public static String unquoteOnly(String s, boolean lenient)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() < 2)
|
||||
return s;
|
||||
|
||||
char first = s.charAt(0);
|
||||
char last = s.charAt(s.length() - 1);
|
||||
if (first != last || (first != '"' && first != '\''))
|
||||
return s;
|
||||
|
||||
StringBuilder b = new StringBuilder(s.length() - 2);
|
||||
boolean escape = false;
|
||||
for (int i = 1; i < s.length() - 1; i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
if (lenient && !isValidEscaping(c))
|
||||
{
|
||||
b.append('\\');
|
||||
}
|
||||
b.append(c);
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public static String unquote(String s)
|
||||
{
|
||||
return unquote(s, false);
|
||||
}
|
||||
String unquote(String s);
|
||||
|
||||
/**
|
||||
* Unquote a string.
|
||||
* Tokenize the passed string into an {@link Iterator} of tokens
|
||||
* split from the string by delimiters. Tokenization is done as the
|
||||
* iterator is advanced.
|
||||
* @param string The string to be tokenized
|
||||
* @return An iterator of token strings.
|
||||
*/
|
||||
Iterator<String> tokenize(String string);
|
||||
|
||||
/**
|
||||
* @param c A character
|
||||
* @return True if a string containing the character should be quoted.
|
||||
*/
|
||||
boolean needsQuoting(char c);
|
||||
|
||||
/**
|
||||
* Quote a string.
|
||||
* The string is quoted only if quoting is required due to
|
||||
* embedded delimiters, quote characters or the empty string.
|
||||
*
|
||||
* @param s The string to unquote.
|
||||
* @param lenient true if unquoting should be lenient to escaped content, leaving some alone, false if string unescaping
|
||||
* @param s The string to quote.
|
||||
* @return quoted string
|
||||
*/
|
||||
public static String unquote(String s, boolean lenient)
|
||||
String quoteIfNeeded(String s);
|
||||
|
||||
/**
|
||||
* Append into buf the provided string, adding quotes if needed.
|
||||
* <p>
|
||||
* Quoting is determined if any of the characters in the {@code delim} are found in the input {@code str}.
|
||||
*
|
||||
* @param buf the buffer to append to
|
||||
* @param str the string to possibly quote
|
||||
*/
|
||||
void quoteIfNeeded(StringBuilder buf, String str);
|
||||
|
||||
class Builder
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() < 2)
|
||||
return s;
|
||||
private String _delim;
|
||||
private boolean _returnQuotes;
|
||||
private boolean _returnDelimiters;
|
||||
private boolean _optionalWhiteSpace;
|
||||
private boolean _embeddedQuotes;
|
||||
private boolean _singleQuotes;
|
||||
private boolean _escapeOnlyQuote;
|
||||
private boolean _legacy;
|
||||
|
||||
char first = s.charAt(0);
|
||||
char last = s.charAt(s.length() - 1);
|
||||
if (first != last || (first != '"' && first != '\''))
|
||||
return s;
|
||||
|
||||
StringBuilder b = new StringBuilder(s.length() - 2);
|
||||
boolean escape = false;
|
||||
for (int i = 1; i < s.length() - 1; i++)
|
||||
private Builder()
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
switch (c)
|
||||
{
|
||||
case 'n':
|
||||
b.append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
b.append('\r');
|
||||
break;
|
||||
case 't':
|
||||
b.append('\t');
|
||||
break;
|
||||
case 'f':
|
||||
b.append('\f');
|
||||
break;
|
||||
case 'b':
|
||||
b.append('\b');
|
||||
break;
|
||||
case '\\':
|
||||
b.append('\\');
|
||||
break;
|
||||
case '/':
|
||||
b.append('/');
|
||||
break;
|
||||
case '"':
|
||||
b.append('"');
|
||||
break;
|
||||
case 'u':
|
||||
b.append((char)(
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 24) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 16) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)) << 8) +
|
||||
(TypeUtil.convertHexDigit((byte)s.charAt(i++)))
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
if (lenient && !isValidEscaping(c))
|
||||
{
|
||||
b.append('\\');
|
||||
}
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
/**
|
||||
* @param delim A string containing the set of characters that are considered delimiters.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder delimiters(String delim)
|
||||
{
|
||||
_delim = delim;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that char c (which is preceded by a backslash) is a valid
|
||||
* escape sequence.
|
||||
*/
|
||||
private static boolean isValidEscaping(char c)
|
||||
{
|
||||
return ((c == 'n') || (c == 'r') || (c == 't') ||
|
||||
(c == 'f') || (c == 'b') || (c == '\\') ||
|
||||
(c == '/') || (c == '"') || (c == 'u'));
|
||||
}
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will return tokens with quotes interpreted but not removed.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder returnQuotes()
|
||||
{
|
||||
_returnQuotes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static boolean isQuoted(String s)
|
||||
{
|
||||
return s != null && s.length() > 0 && s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"';
|
||||
}
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will return delimiter characters as individual tokens.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder returnDelimiters()
|
||||
{
|
||||
_returnDelimiters = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return handle double quotes if true
|
||||
*/
|
||||
public boolean getDouble()
|
||||
{
|
||||
return _double;
|
||||
}
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will ignore optional white space characters before
|
||||
* and after delimiters. This is not supported together with {@link #legacy()}. For example, the
|
||||
* string {@code a, b ,c} with delimiter {@code ,} will be tokenized with this option as {@code a},
|
||||
* {@code b} and {@code c}, all trimmed of spaces. Without this option, the second token would be {@code b} with one
|
||||
* space before and after.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder ignoreOptionalWhiteSpace()
|
||||
{
|
||||
_optionalWhiteSpace = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param d handle double quotes if true
|
||||
*/
|
||||
public void setDouble(boolean d)
|
||||
{
|
||||
_double = d;
|
||||
}
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will interpret quote characters within a token as initiating
|
||||
* a sequence of quoted characters, rather than being part of the token value itself.
|
||||
* For example the string {@code name1=value1; name2="value;2"} with {@code ;} delimiter, would result in
|
||||
* two tokens: {@code name1=value1} and {@code name2=value;2}. Without this option
|
||||
* the result would be three tokens: {@code name1=value1}, {@code name2="value} and {@code 2"}.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder allowEmbeddedQuotes()
|
||||
{
|
||||
_embeddedQuotes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return handle single quotes if true
|
||||
*/
|
||||
public boolean getSingle()
|
||||
{
|
||||
return _single;
|
||||
}
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will allow quoting with the single quote character {@code '}.
|
||||
* This can only be used with {@link #legacy()}.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder allowSingleQuote()
|
||||
{
|
||||
_singleQuotes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param single handle single quotes if true
|
||||
*/
|
||||
public void setSingle(boolean single)
|
||||
{
|
||||
_single = single;
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will only allow escapes to be used with
|
||||
* the quote character. Specifically the escape character itself cannot be escaped.
|
||||
* Any usage of the escape character, other than for quotes, is considered as a literal escape character.
|
||||
* For example the string {@code "test\"tokenizer\test"} will be unquoted as
|
||||
* {@code test"tokenizer\test}.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder allowEscapeOnlyForQuotes()
|
||||
{
|
||||
_escapeOnlyQuote = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If called, the built {@link QuotedStringTokenizer} will use the legacy implementation from prior to
|
||||
* jetty-12. The legacy implementation does not comply with any current RFC. Using {@code legacy} also
|
||||
* implies {@link #allowEmbeddedQuotes()}.
|
||||
* @return this {@code Builder}
|
||||
*/
|
||||
public Builder legacy()
|
||||
{
|
||||
_legacy = true;
|
||||
_embeddedQuotes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The built immutable {@link QuotedStringTokenizer}.
|
||||
*/
|
||||
public QuotedStringTokenizer build()
|
||||
{
|
||||
if (_legacy)
|
||||
{
|
||||
if (_optionalWhiteSpace)
|
||||
throw new IllegalArgumentException("OWS not supported by legacy");
|
||||
if (_escapeOnlyQuote)
|
||||
throw new IllegalArgumentException("EscapeOnlyQuote not supported by legacy");
|
||||
if (!_embeddedQuotes)
|
||||
throw new IllegalArgumentException("EmbeddedQuotes must be used with legacy");
|
||||
return new LegacyQuotedStringTokenizer(_delim, _returnDelimiters, _returnQuotes, _singleQuotes);
|
||||
}
|
||||
if (StringUtil.isEmpty(_delim))
|
||||
throw new IllegalArgumentException("Delimiters must be provided");
|
||||
if (_singleQuotes)
|
||||
throw new IllegalArgumentException("Single quotes not supported by RFC9110");
|
||||
return new RFC9110QuotedStringTokenizer(_delim, _optionalWhiteSpace, _returnDelimiters, _returnQuotes, _embeddedQuotes, _escapeOnlyQuote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An implementation of {@link QuotedStringTokenizer} with partial handling of
|
||||
* <a href="https://www.rfc-editor.org/rfc/rfc9110#name-quoted-strings">RFC9110 quoted-string</a>s.
|
||||
* The deviation from the RFC is that characters are not enforced to be
|
||||
* {@code qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text} and it is expected
|
||||
* that the caller will enforce any character restrictions.
|
||||
*/
|
||||
public class RFC9110QuotedStringTokenizer implements QuotedStringTokenizer
|
||||
{
|
||||
private final String _delim;
|
||||
private final boolean _optionalWhiteSpace;
|
||||
private final boolean _returnDelimiters;
|
||||
private final boolean _returnQuotes;
|
||||
private final boolean _embeddedQuotes;
|
||||
private final boolean _escapeOnlyQuote;
|
||||
|
||||
RFC9110QuotedStringTokenizer(String delim,
|
||||
boolean optionalWhiteSpace,
|
||||
boolean returnDelimiters,
|
||||
boolean returnQuotes,
|
||||
boolean embeddedQuotes,
|
||||
boolean escapeOnlyQuote)
|
||||
{
|
||||
_delim = Objects.requireNonNull(delim);
|
||||
_optionalWhiteSpace = optionalWhiteSpace;
|
||||
_returnDelimiters = returnDelimiters;
|
||||
_returnQuotes = returnQuotes;
|
||||
_embeddedQuotes = embeddedQuotes;
|
||||
_escapeOnlyQuote = escapeOnlyQuote;
|
||||
|
||||
if (_delim.indexOf('"') >= 0)
|
||||
throw new IllegalArgumentException("Can't use quote as delimiters: " + _delim);
|
||||
if (_optionalWhiteSpace && _delim.indexOf(' ') >= 0)
|
||||
throw new IllegalArgumentException("Can't delimit with space with optional white space");
|
||||
}
|
||||
|
||||
protected boolean isOptionalWhiteSpace(char c)
|
||||
{
|
||||
return Character.isWhitespace(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> tokenize(String string)
|
||||
{
|
||||
Objects.requireNonNull(string);
|
||||
|
||||
return new Iterator<>()
|
||||
{
|
||||
private enum State
|
||||
{
|
||||
START,
|
||||
TOKEN,
|
||||
QUOTE,
|
||||
END,
|
||||
}
|
||||
|
||||
private final StringBuilder _token = new StringBuilder();
|
||||
State _state = State.START;
|
||||
private boolean _hasToken;
|
||||
private int _ows = -1;
|
||||
private int _i = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
if (_hasToken)
|
||||
return true;
|
||||
|
||||
boolean escape = false;
|
||||
while (_i < string.length())
|
||||
{
|
||||
char c = string.charAt(_i++);
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case START ->
|
||||
{
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
{
|
||||
_token.append(c);
|
||||
return _hasToken = true;
|
||||
}
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
_ows = -1;
|
||||
_state = State.QUOTE;
|
||||
}
|
||||
else if (!_optionalWhiteSpace || !isOptionalWhiteSpace(c))
|
||||
{
|
||||
_token.append(c);
|
||||
_hasToken = true;
|
||||
_ows = -1;
|
||||
_state = State.TOKEN;
|
||||
}
|
||||
}
|
||||
case TOKEN ->
|
||||
{
|
||||
_hasToken = true;
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
if (_returnDelimiters)
|
||||
_i--;
|
||||
_state = State.START;
|
||||
if (_ows >= 0)
|
||||
_token.setLength(_ows);
|
||||
return _hasToken;
|
||||
}
|
||||
else if (_embeddedQuotes && c == '"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
_ows = -1;
|
||||
_state = State.QUOTE;
|
||||
}
|
||||
else if (_optionalWhiteSpace && isOptionalWhiteSpace(c))
|
||||
{
|
||||
if (_ows < 0)
|
||||
_ows = _token.length();
|
||||
_token.append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ows = -1;
|
||||
_token.append(c);
|
||||
}
|
||||
}
|
||||
case QUOTE ->
|
||||
{
|
||||
_hasToken = true;
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
_token.append(c);
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
if (_embeddedQuotes)
|
||||
{
|
||||
_ows = -1;
|
||||
_state = State.TOKEN;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.END;
|
||||
return _hasToken;
|
||||
}
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
if (_escapeOnlyQuote && (_i >= string.length() || string.charAt(_i) != '"'))
|
||||
_token.append(c);
|
||||
else
|
||||
{
|
||||
if (_returnQuotes)
|
||||
_token.append(c);
|
||||
escape = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_token.append(c);
|
||||
}
|
||||
}
|
||||
case END ->
|
||||
{
|
||||
if (_delim.indexOf(c) >= 0)
|
||||
{
|
||||
_state = State.START;
|
||||
if (_returnDelimiters)
|
||||
{
|
||||
_token.append(c);
|
||||
return _hasToken = true;
|
||||
}
|
||||
}
|
||||
else if (!_optionalWhiteSpace || !isOptionalWhiteSpace(c))
|
||||
throw new IllegalArgumentException("characters after end quote");
|
||||
}
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == State.QUOTE)
|
||||
throw new IllegalArgumentException("unterminated quote");
|
||||
|
||||
if (_ows >= 0 && _hasToken)
|
||||
_token.setLength(_ows);
|
||||
|
||||
return _hasToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next()
|
||||
{
|
||||
if (!hasNext())
|
||||
throw new NoSuchElementException();
|
||||
String t = _token.toString();
|
||||
_token.setLength(0);
|
||||
_hasToken = false;
|
||||
return t;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quote(Appendable buffer, String input)
|
||||
{
|
||||
if (input == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
buffer.append('"');
|
||||
for (int i = 0; i < input.length(); ++i)
|
||||
{
|
||||
char c = input.charAt(i);
|
||||
if (c == '"' || c == '\\')
|
||||
buffer.append('\\').append(c);
|
||||
else
|
||||
buffer.append(c);
|
||||
}
|
||||
buffer.append('"');
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String quoteIfNeeded(String s)
|
||||
{
|
||||
return quoteIfNeededImpl(null, s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quoteIfNeeded(StringBuilder buf, String str)
|
||||
{
|
||||
quoteIfNeededImpl(buf, str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsQuoting(char c)
|
||||
{
|
||||
return c == '\\' || c == '"' || _optionalWhiteSpace && Character.isWhitespace(c) || _delim.indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
private String quoteIfNeededImpl(StringBuilder buf, String str)
|
||||
{
|
||||
if (str == null)
|
||||
return null;
|
||||
if (str.length() == 0)
|
||||
{
|
||||
if (buf == null)
|
||||
return "\"\"";
|
||||
|
||||
buf.append("\"\"");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < str.length(); i++)
|
||||
{
|
||||
char c = str.charAt(i);
|
||||
if (needsQuoting(c))
|
||||
{
|
||||
if (buf == null)
|
||||
return quote(str);
|
||||
quote(buf, str);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// no special delimiters used, no quote needed.
|
||||
if (buf == null)
|
||||
return str;
|
||||
buf.append(str);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String unquote(String s)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
if (s.length() < 2)
|
||||
return s;
|
||||
|
||||
char first = s.charAt(0);
|
||||
char last = s.charAt(s.length() - 1);
|
||||
if (first != '"' || last != '"')
|
||||
return s;
|
||||
|
||||
StringBuilder b = new StringBuilder(s.length() - 2);
|
||||
boolean escape = false;
|
||||
for (int i = 1; i < s.length() - 1; i++)
|
||||
{
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (escape)
|
||||
{
|
||||
escape = false;
|
||||
b.append(c);
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escape = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append(getClass().getSimpleName()).append('@').append(Long.toHexString(hashCode()))
|
||||
.append("{'").append(_delim).append('\'');
|
||||
|
||||
if (_optionalWhiteSpace)
|
||||
out.append(",optionalWhiteSpace");
|
||||
if (_returnDelimiters)
|
||||
out.append(",returnDelimiters");
|
||||
if (_returnQuotes)
|
||||
out.append(",returnQuotes");
|
||||
if (_embeddedQuotes)
|
||||
out.append(",embeddedQuotes");
|
||||
if (_escapeOnlyQuote)
|
||||
out.append(",escapeOnlyQuote");
|
||||
out.append('}');
|
||||
return out.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
public class LegacyQuotedStringTokenizerTest
|
||||
{
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer0()
|
||||
{
|
||||
TestTokenizer tok =
|
||||
new TestTokenizer("abc\n\"d\\\"'\"\n'p\\',y'\nz", null, false, false, true);
|
||||
checkTok(tok, false, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer1()
|
||||
{
|
||||
TestTokenizer tok =
|
||||
new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", false, false, true);
|
||||
checkTok(tok, false, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer2()
|
||||
{
|
||||
TestTokenizer tok =
|
||||
new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", false, false, true);
|
||||
checkTok(tok, false, false);
|
||||
|
||||
tok = new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,", true, false, true);
|
||||
checkTok(tok, true, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer3()
|
||||
{
|
||||
TestTokenizer tok;
|
||||
|
||||
tok = new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
false, false, true);
|
||||
checkTok(tok, false, false);
|
||||
|
||||
tok = new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
false, true, true);
|
||||
checkTok(tok, false, true);
|
||||
|
||||
tok = new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
true, false, true);
|
||||
checkTok(tok, true, false);
|
||||
|
||||
tok = new TestTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
true, true, true);
|
||||
checkTok(tok, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuote()
|
||||
{
|
||||
QuotedStringTokenizer tokenizer = QuotedStringTokenizer.builder().legacy().build();
|
||||
StringBuffer buf = new StringBuffer();
|
||||
|
||||
buf.setLength(0);
|
||||
tokenizer.quote(buf, "abc \n efg");
|
||||
assertEquals("\"abc \\n efg\"", buf.toString());
|
||||
|
||||
buf.setLength(0);
|
||||
tokenizer.quote(buf, "abcefg");
|
||||
assertEquals("\"abcefg\"", buf.toString());
|
||||
|
||||
buf.setLength(0);
|
||||
tokenizer.quote(buf, "abcefg\"");
|
||||
assertEquals("\"abcefg\\\"\"", buf.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer4()
|
||||
{
|
||||
TestTokenizer tok = new TestTokenizer("abc'def,ghi'jkl", ",", false, false, false);
|
||||
Iterator<String> iter = tok.test();
|
||||
assertEquals("abc'def", iter.next());
|
||||
assertEquals("ghi'jkl", iter.next());
|
||||
tok = new TestTokenizer("abc'def,ghi'jkl", ",", false, false, true);
|
||||
iter = tok.test();
|
||||
assertEquals("abcdef,ghijkl", iter.next());
|
||||
}
|
||||
|
||||
private void checkTok(TestTokenizer tok, boolean delim, boolean quotes)
|
||||
{
|
||||
Iterator<String> trial = tok.test();
|
||||
assertTrue(trial.hasNext());
|
||||
assertEquals("abc", trial.next());
|
||||
if (delim)
|
||||
assertEquals(",", trial.next());
|
||||
if (delim)
|
||||
assertEquals(" ", trial.next());
|
||||
|
||||
assertEquals(quotes ? "\"d\\\"'\"" : "d\"'", trial.next());
|
||||
if (delim)
|
||||
assertEquals(",", trial.next());
|
||||
assertEquals(quotes ? "'p\\',y'" : "p',y", trial.next());
|
||||
if (delim)
|
||||
assertEquals(" ", trial.next());
|
||||
assertEquals("z", trial.next());
|
||||
assertFalse(trial.hasNext());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String quote(String, String)
|
||||
*/
|
||||
@Test
|
||||
public void testQuoteIfNeeded()
|
||||
{
|
||||
QuotedStringTokenizer tokenizer = QuotedStringTokenizer.builder().legacy().delimiters(" ,").build();
|
||||
assertEquals("abc", tokenizer.quoteIfNeeded("abc"));
|
||||
assertEquals("\"a c\"", tokenizer.quoteIfNeeded("a c"));
|
||||
assertEquals("\"a'c\"", tokenizer.quoteIfNeeded("a'c"));
|
||||
assertEquals("\"a\\n\\r\\t\"", tokenizer.quote("a\n\r\t"));
|
||||
assertEquals("\"\\u0000\\u001f\"", tokenizer.quote("\u0000\u001f"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnquote()
|
||||
{
|
||||
QuotedStringTokenizer tokenizer = QuotedStringTokenizer.builder().legacy().delimiters(" ,").build();
|
||||
assertEquals("abc", tokenizer.unquote("abc"));
|
||||
assertEquals("a\"c", tokenizer.unquote("\"a\\\"c\""));
|
||||
assertEquals("a'c", tokenizer.unquote("\"a'c\""));
|
||||
assertEquals("a\n\r\t", tokenizer.unquote("\"a\\n\\r\\t\""));
|
||||
assertEquals("\u0000\u001f ", tokenizer.unquote("\"\u0000\u001f\u0020\""));
|
||||
assertEquals("\u0000\u001f ", tokenizer.unquote("\"\u0000\u001f\u0020\""));
|
||||
assertEquals("ab\u001ec", tokenizer.unquote("ab\u001ec"));
|
||||
assertEquals("ab\u001ec", tokenizer.unquote("\"ab\u001ec\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* When encountering a Content-Disposition line during a multi-part mime file
|
||||
* upload, the filename="..." field can contain '\' characters that do not
|
||||
* belong to a proper escaping sequence, this tests QuotedStringTokenizer to
|
||||
* ensure that it preserves those slashes for where they cannot be escaped.
|
||||
*/
|
||||
@Test
|
||||
public void testNextTokenOnContentDisposition()
|
||||
{
|
||||
String contentDisposition = "form-data; name=\"fileup\"; filename=\"Taken on Aug 22 \\ 2012.jpg\"";
|
||||
|
||||
TestTokenizer tok = new TestTokenizer(contentDisposition, ";", false, true, true);
|
||||
Iterator<String> trial = tok.test();
|
||||
|
||||
assertEquals("form-data", trial.next().trim());
|
||||
assertEquals("name=\"fileup\"", trial.next().trim());
|
||||
assertEquals("filename=\"Taken on Aug 22 \\ 2012.jpg\"", trial.next().trim());
|
||||
}
|
||||
|
||||
static class TestTokenizer
|
||||
{
|
||||
private final String _string;
|
||||
private final QuotedStringTokenizer _tokenizer;
|
||||
|
||||
public TestTokenizer(String string, String delimiters, boolean returnDelimiters, boolean returnQuotes, boolean singleQuotes)
|
||||
{
|
||||
_string = string;
|
||||
QuotedStringTokenizer.Builder builder = QuotedStringTokenizer.builder().legacy();
|
||||
if (delimiters != null)
|
||||
builder.delimiters(delimiters);
|
||||
if (returnDelimiters)
|
||||
builder.returnDelimiters();
|
||||
if (returnQuotes)
|
||||
builder.returnQuotes();
|
||||
if (singleQuotes)
|
||||
builder.allowSingleQuote();
|
||||
_tokenizer = builder.build();
|
||||
}
|
||||
|
||||
Iterator<String> test()
|
||||
{
|
||||
return _tokenizer.tokenize(_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,77 +13,116 @@
|
|||
|
||||
package org.eclipse.jetty.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Iterator;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
public class QuotedStringTokenizerTest
|
||||
{
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer0()
|
||||
public static Stream<Arguments> tokenizerTests()
|
||||
{
|
||||
QuotedStringTokenizer tok =
|
||||
new QuotedStringTokenizer("abc\n\"d\\\"'\"\n'p\\',y'\nz");
|
||||
checkTok(tok, false, false);
|
||||
QuotedStringTokenizer commaList = QuotedStringTokenizer.builder().delimiters(",").build();
|
||||
QuotedStringTokenizer commaListOws = QuotedStringTokenizer.builder().delimiters(",").ignoreOptionalWhiteSpace().build();
|
||||
QuotedStringTokenizer commaListOwsEmbedded = QuotedStringTokenizer.builder().delimiters(",").ignoreOptionalWhiteSpace().allowEmbeddedQuotes().build();
|
||||
QuotedStringTokenizer commaListDelimiters = QuotedStringTokenizer.builder().delimiters(",").returnDelimiters().build();
|
||||
QuotedStringTokenizer commaListOwsDelimiters = QuotedStringTokenizer.builder().delimiters(",").ignoreOptionalWhiteSpace().returnDelimiters().build();
|
||||
QuotedStringTokenizer commaListOwsEmbeddedQuotes = QuotedStringTokenizer.builder().delimiters(",").ignoreOptionalWhiteSpace().returnQuotes().allowEmbeddedQuotes().build();
|
||||
QuotedStringTokenizer commaListEscapeOQ = QuotedStringTokenizer.builder().delimiters(",").allowEscapeOnlyForQuotes().build();
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(commaList, "", new String[] {}),
|
||||
Arguments.of(commaList, "a,b,c", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaList, " a , b , c ", new String[] {" a ", " b ", " c "}),
|
||||
Arguments.of(commaList, "a a,b b, c c ", new String[] {"a a", "b b", " c c "}),
|
||||
Arguments.of(commaList, "\"a,a\",\"b,b\",c", new String[] {"a,a", "b,b", "c"}),
|
||||
Arguments.of(commaList, "\"a,a\", b\",\"b ,c", new String[] {"a,a", " b\"", null}),
|
||||
Arguments.of(commaList, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", "b\\b", "c,c"}),
|
||||
|
||||
Arguments.of(commaListOws, "", new String[] {}),
|
||||
Arguments.of(commaListOws, "a,b,c", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOws, " a , b , c ", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOws, "a a,b b, c c ", new String[] {"a a", "b b", "c c"}),
|
||||
Arguments.of(commaListOws, "\"a,a\",\"b,b\",c", new String[] {"a,a", "b,b", "c"}),
|
||||
Arguments.of(commaListOws, "\"a,a\", b\",\"b ,c", new String[] {"a,a", "b\"", null}),
|
||||
Arguments.of(commaListOws, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", "b\\b", "c,c"}),
|
||||
|
||||
Arguments.of(commaListOwsEmbedded, "", new String[] {}),
|
||||
Arguments.of(commaListOwsEmbedded, "a,b,c", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOwsEmbedded, " a , b , c ", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOwsEmbedded, "a a,b b, c c ", new String[] {"a a", "b b", "c c"}),
|
||||
Arguments.of(commaListOwsEmbedded, "\"a,a\",\"b,b\",c", new String[] {"a,a", "b,b", "c"}),
|
||||
Arguments.of(commaListOwsEmbedded, "\"a,a\", b\",\"b ,c", new String[] {"a,a", "b,b", "c"}),
|
||||
Arguments.of(commaListOwsEmbedded, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", "b\\b", "c,c"}),
|
||||
|
||||
Arguments.of(commaListDelimiters, "", new String[] {}),
|
||||
Arguments.of(commaListDelimiters, "a,b,c", new String[] {"a", ",", "b", ",", "c"}),
|
||||
Arguments.of(commaListDelimiters, " a , b , c ", new String[] {" a ", ",", " b ", ",", " c "}),
|
||||
Arguments.of(commaListDelimiters, "a a,b b, c c ", new String[] {"a a", ",", "b b", ",", " c c "}),
|
||||
Arguments.of(commaListDelimiters, "\"a,a\",\"b,b\",c", new String[] {"a,a", ",", "b,b", ",", "c"}),
|
||||
Arguments.of(commaListDelimiters, "\"a,a\", b\",\"b ,c", new String[] {"a,a", ",", " b\"", ",", null}),
|
||||
Arguments.of(commaListDelimiters, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", ",", "b\\b", ",", "c,c"}),
|
||||
|
||||
Arguments.of(commaListOwsDelimiters, "", new String[] {}),
|
||||
Arguments.of(commaListOwsDelimiters, "a,b,c", new String[] {"a", ",", "b", ",", "c"}),
|
||||
Arguments.of(commaListOwsDelimiters, " a , b , c ", new String[] {"a", ",", "b", ",", "c"}),
|
||||
Arguments.of(commaListOwsDelimiters, "a a,b b, c c ", new String[] {"a a", ",", "b b", ",", "c c"}),
|
||||
Arguments.of(commaListOwsDelimiters, "\"a,a\",\"b,b\",c", new String[] {"a,a", ",", "b,b", ",", "c"}),
|
||||
Arguments.of(commaListOwsDelimiters, "\"a,a\", b\",\"b ,c", new String[] {"a,a", ",", "b\"", ",", null}),
|
||||
Arguments.of(commaListOwsDelimiters, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", ",", "b\\b", ",", "c,c"}),
|
||||
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "", new String[] {}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "a,b,c", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, " a , b , c ", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "a a,b b, c c ", new String[] {"a a", "b b", "c c"}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "\"a,a\",\"b,b\",c", new String[] {"\"a,a\"", "\"b,b\"", "c"}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "\"a,a\", b\",\"b ,c", new String[] {"\"a,a\"", "b\",\"b", "c"}),
|
||||
Arguments.of(commaListOwsEmbeddedQuotes, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"\"a\\\"a\"", "\"b\\\\b\"", "\"c\\,c\""}),
|
||||
|
||||
Arguments.of(commaListEscapeOQ, "", new String[] {}),
|
||||
Arguments.of(commaListEscapeOQ, "a,b,c", new String[] {"a", "b", "c"}),
|
||||
Arguments.of(commaListEscapeOQ, " a , b , c ", new String[] {" a ", " b ", " c "}),
|
||||
Arguments.of(commaListEscapeOQ, "a a,b b, c c ", new String[] {"a a", "b b", " c c "}),
|
||||
Arguments.of(commaListEscapeOQ, "\"a,a\",\"b,b\",c", new String[] {"a,a", "b,b", "c"}),
|
||||
Arguments.of(commaListEscapeOQ, "\"a,a\", b\",\"b ,c", new String[] {"a,a", " b\"", null}),
|
||||
Arguments.of(commaListEscapeOQ, "\"a\\\"a\",\"b\\\\b\",\"c\\,c\"", new String[] {"a\"a", "b\\\\b", "c\\,c"}),
|
||||
|
||||
Arguments.of(commaList, null, null)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer1()
|
||||
@ParameterizedTest
|
||||
@MethodSource("tokenizerTests")
|
||||
public void testTokenizer(QuotedStringTokenizer tokenizer, String string, String[] expected)
|
||||
{
|
||||
QuotedStringTokenizer tok =
|
||||
new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z",
|
||||
" ,");
|
||||
checkTok(tok, false, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer2()
|
||||
{
|
||||
QuotedStringTokenizer tok =
|
||||
new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
false);
|
||||
checkTok(tok, false, false);
|
||||
|
||||
tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
true);
|
||||
checkTok(tok, true, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer3()
|
||||
{
|
||||
QuotedStringTokenizer tok;
|
||||
|
||||
tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
false, false);
|
||||
checkTok(tok, false, false);
|
||||
|
||||
tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
false, true);
|
||||
checkTok(tok, false, true);
|
||||
|
||||
tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
true, false);
|
||||
checkTok(tok, true, false);
|
||||
|
||||
tok = new QuotedStringTokenizer("abc, \"d\\\"'\",'p\\',y' z", " ,",
|
||||
true, true);
|
||||
checkTok(tok, true, true);
|
||||
if (expected == null)
|
||||
{
|
||||
assertThrows(NullPointerException.class, () -> tokenizer.tokenize(string));
|
||||
return;
|
||||
}
|
||||
Iterator<String> iterator = tokenizer.tokenize(string);
|
||||
int i = 0;
|
||||
while (i < expected.length)
|
||||
{
|
||||
String token = expected[i++];
|
||||
if (token == null)
|
||||
assertThrows(IllegalArgumentException.class, iterator::hasNext);
|
||||
else
|
||||
{
|
||||
assertTrue(iterator.hasNext());
|
||||
assertThat(iterator.next(), Matchers.equalTo(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -92,87 +131,46 @@ public class QuotedStringTokenizerTest
|
|||
StringBuffer buf = new StringBuffer();
|
||||
|
||||
buf.setLength(0);
|
||||
QuotedStringTokenizer.quote(buf, "abc \n efg");
|
||||
assertEquals("\"abc \\n efg\"", buf.toString());
|
||||
QuotedStringTokenizer.CSV.quote(buf, "abc \n efg");
|
||||
assertEquals("\"abc \n efg\"", buf.toString());
|
||||
|
||||
buf.setLength(0);
|
||||
QuotedStringTokenizer.quote(buf, "abcefg");
|
||||
QuotedStringTokenizer.CSV.quote(buf, "abcefg");
|
||||
assertEquals("\"abcefg\"", buf.toString());
|
||||
|
||||
buf.setLength(0);
|
||||
QuotedStringTokenizer.quote(buf, "abcefg\"");
|
||||
QuotedStringTokenizer.CSV.quote(buf, "abcefg\"");
|
||||
assertEquals("\"abcefg\\\"\"", buf.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String nextToken()
|
||||
*/
|
||||
@Test
|
||||
public void testTokenizer4()
|
||||
{
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ",");
|
||||
tok.setSingle(false);
|
||||
assertEquals("abc'def", tok.nextToken());
|
||||
assertEquals("ghi'jkl", tok.nextToken());
|
||||
tok = new QuotedStringTokenizer("abc'def,ghi'jkl", ",");
|
||||
tok.setSingle(true);
|
||||
assertEquals("abcdef,ghijkl", tok.nextToken());
|
||||
}
|
||||
|
||||
private void checkTok(QuotedStringTokenizer tok, boolean delim, boolean quotes)
|
||||
{
|
||||
assertTrue(tok.hasMoreElements());
|
||||
assertTrue(tok.hasMoreTokens());
|
||||
assertEquals("abc", tok.nextToken());
|
||||
if (delim)
|
||||
assertEquals(",", tok.nextToken());
|
||||
if (delim)
|
||||
assertEquals(" ", tok.nextToken());
|
||||
|
||||
assertEquals(quotes ? "\"d\\\"'\"" : "d\"'", tok.nextElement());
|
||||
if (delim)
|
||||
assertEquals(",", tok.nextToken());
|
||||
assertEquals(quotes ? "'p\\',y'" : "p',y", tok.nextToken());
|
||||
if (delim)
|
||||
assertEquals(" ", tok.nextToken());
|
||||
assertEquals("z", tok.nextToken());
|
||||
assertFalse(tok.hasMoreTokens());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test for String quote(String, String)
|
||||
*/
|
||||
@Test
|
||||
public void testQuoteIfNeeded()
|
||||
{
|
||||
assertEquals("abc", QuotedStringTokenizer.quoteIfNeeded("abc", " ,"));
|
||||
assertEquals("\"a c\"", QuotedStringTokenizer.quoteIfNeeded("a c", " ,"));
|
||||
assertEquals("\"a'c\"", QuotedStringTokenizer.quoteIfNeeded("a'c", " ,"));
|
||||
assertEquals("\"a\\n\\r\\t\"", QuotedStringTokenizer.quote("a\n\r\t"));
|
||||
assertEquals("\"\\u0000\\u001f\"", QuotedStringTokenizer.quote("\u0000\u001f"));
|
||||
QuotedStringTokenizer tokenizer = QuotedStringTokenizer.CSV; // OWS
|
||||
assertEquals("abc", tokenizer.quoteIfNeeded("abc"));
|
||||
assertEquals("\"a c\"", tokenizer.quoteIfNeeded("a c"));
|
||||
assertEquals("a c", QuotedStringTokenizer.builder().delimiters(",").build().quoteIfNeeded("a c")); // No OWS
|
||||
assertEquals("a'c", tokenizer.quoteIfNeeded("a'c"));
|
||||
assertEquals("\"a\\\"c\"", tokenizer.quoteIfNeeded("a\"c"));
|
||||
assertEquals("\"a\n\r\t\"", tokenizer.quoteIfNeeded("a\n\r\t"));
|
||||
assertEquals("\"\u0000\u001f\"", tokenizer.quoteIfNeeded("\u0000\u001f"));
|
||||
assertEquals("\"a\\\"c\"", tokenizer.quoteIfNeeded("a\"c"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnquote()
|
||||
{
|
||||
assertEquals("abc", QuotedStringTokenizer.unquote("abc"));
|
||||
assertEquals("a\"c", QuotedStringTokenizer.unquote("\"a\\\"c\""));
|
||||
assertEquals("a'c", QuotedStringTokenizer.unquote("\"a'c\""));
|
||||
assertEquals("a\n\r\t", QuotedStringTokenizer.unquote("\"a\\n\\r\\t\""));
|
||||
assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\""));
|
||||
assertEquals("\u0000\u001f ", QuotedStringTokenizer.unquote("\"\u0000\u001f\u0020\""));
|
||||
assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("ab\u001ec"));
|
||||
assertEquals("ab\u001ec", QuotedStringTokenizer.unquote("\"ab\u001ec\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnquoteOnly()
|
||||
{
|
||||
assertEquals("abc", QuotedStringTokenizer.unquoteOnly("abc"));
|
||||
assertEquals("a\"c", QuotedStringTokenizer.unquoteOnly("\"a\\\"c\""));
|
||||
assertEquals("a'c", QuotedStringTokenizer.unquoteOnly("\"a'c\""));
|
||||
assertEquals("a\\n\\r\\t", QuotedStringTokenizer.unquoteOnly("\"a\\\\n\\\\r\\\\t\""));
|
||||
assertEquals("ba\\uXXXXaaa", QuotedStringTokenizer.unquoteOnly("\"ba\\\\uXXXXaaa\""));
|
||||
assertEquals("abc", QuotedStringTokenizer.CSV.unquote("abc"));
|
||||
assertEquals("a\"c", QuotedStringTokenizer.CSV.unquote("\"a\\\"c\""));
|
||||
assertEquals("a'c", QuotedStringTokenizer.CSV.unquote("\"a'c\""));
|
||||
assertEquals("anrt", QuotedStringTokenizer.CSV.unquote("\"a\\n\\r\\t\""));
|
||||
assertEquals("\u0000\u001f ", QuotedStringTokenizer.CSV.unquote("\"\u0000\u001f \""));
|
||||
assertEquals("\u0000\u001f ", QuotedStringTokenizer.CSV.unquote("\"\u0000\u001f \""));
|
||||
assertEquals("ab\u001ec", QuotedStringTokenizer.CSV.unquote("ab\u001ec"));
|
||||
assertEquals("ab\u001ec", QuotedStringTokenizer.CSV.unquote("\"ab\u001ec\""));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,12 +182,13 @@ public class QuotedStringTokenizerTest
|
|||
@Test
|
||||
public void testNextTokenOnContentDisposition()
|
||||
{
|
||||
String contentDisposition = "form-data; name=\"fileup\"; filename=\"Taken on Aug 22 \\ 2012.jpg\"";
|
||||
String contentDisposition = "form-data; name=\"fileup\"; filename=\"C:\\Pictures\\20120504.jpg\"";
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
|
||||
QuotedStringTokenizer tok = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().returnQuotes().allowEmbeddedQuotes().allowEscapeOnlyForQuotes().build();
|
||||
Iterator<String> iter = tok.tokenize(contentDisposition);
|
||||
|
||||
assertEquals("form-data", tok.nextToken().trim());
|
||||
assertEquals("name=\"fileup\"", tok.nextToken().trim());
|
||||
assertEquals("filename=\"Taken on Aug 22 \\ 2012.jpg\"", tok.nextToken().trim());
|
||||
assertEquals("form-data", iter.next());
|
||||
assertEquals("name=\"fileup\"", iter.next());
|
||||
assertEquals("filename=\"C:\\Pictures\\20120504.jpg\"", iter.next());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
public abstract class CoreClientUpgradeRequest implements Response.CompleteListener, HttpUpgrader.Factory
|
||||
{
|
||||
|
||||
public static CoreClientUpgradeRequest from(WebSocketCoreClient webSocketClient, URI requestURI, FrameHandler frameHandler)
|
||||
{
|
||||
return new CoreClientUpgradeRequest(webSocketClient, requestURI)
|
||||
|
@ -377,10 +378,9 @@ public abstract class CoreClientUpgradeRequest implements Response.CompleteListe
|
|||
{
|
||||
for (String extVal : extValues)
|
||||
{
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(extVal, ",");
|
||||
while (tok.hasMoreTokens())
|
||||
for (Iterator<String> i = QuotedStringTokenizer.CSV.tokenize(extVal); i.hasNext();)
|
||||
{
|
||||
negotiatedExtensions.add(ExtensionConfig.parse(tok.nextToken()));
|
||||
negotiatedExtensions.add(ExtensionConfig.parse(i.next()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import org.eclipse.jetty.websocket.core.WebSocketComponents;
|
|||
@ManagedObject("Identity Extension")
|
||||
public class IdentityExtension extends AbstractExtension
|
||||
{
|
||||
private static final QuotedStringTokenizer PARAM_VALUE_QUOTING = QuotedStringTokenizer.builder().delimiters(";=").ignoreOptionalWhiteSpace().build();
|
||||
|
||||
private String id;
|
||||
|
||||
public String getParam(String key)
|
||||
|
@ -67,7 +69,8 @@ public class IdentityExtension extends AbstractExtension
|
|||
{
|
||||
s.append(';');
|
||||
}
|
||||
s.append(param).append('=').append(QuotedStringTokenizer.quoteIfNeeded(config.getParameter(param, ""), ";="));
|
||||
|
||||
s.append(param).append('=').append(PARAM_VALUE_QUOTING.quoteIfNeeded(config.getParameter(param, "")));
|
||||
delim = true;
|
||||
}
|
||||
s.append("]");
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.net.URI;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
@ -144,10 +145,10 @@ public class QuickStartDescriptorProcessor extends IterativeDescriptorProcessor
|
|||
{
|
||||
context.removeAttribute(name);
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(value, ",");
|
||||
while (tok.hasMoreElements())
|
||||
for (Iterator<String> i = QuotedStringTokenizer.CSV.tokenize(value); i.hasNext();)
|
||||
{
|
||||
values.add(tok.nextToken().trim());
|
||||
String token = i.next();
|
||||
values.add(token);
|
||||
}
|
||||
}
|
||||
default -> values.add(value);
|
||||
|
|
|
@ -631,7 +631,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
|
|||
v.append(",\n ");
|
||||
else
|
||||
v.append("\n ");
|
||||
QuotedStringTokenizer.quote(v, i.toString());
|
||||
QuotedStringTokenizer.CSV.quote(v, i.toString());
|
||||
}
|
||||
}
|
||||
out.openTag("context-param")
|
||||
|
@ -672,7 +672,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
|
|||
v.append(",\n ");
|
||||
else
|
||||
v.append("\n ");
|
||||
QuotedStringTokenizer.quote(v, normalizer.normalize(i));
|
||||
QuotedStringTokenizer.CSV.quote(v, normalizer.normalize(i));
|
||||
}
|
||||
}
|
||||
out.openTag("context-param")
|
||||
|
|
|
@ -31,6 +31,7 @@ import jakarta.servlet.RequestDispatcher;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
@ -43,7 +44,6 @@ import org.eclipse.jetty.server.Server;
|
|||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -486,9 +486,7 @@ public class ErrorHandler implements Request.Handler
|
|||
}
|
||||
|
||||
writer.append(json.entrySet().stream()
|
||||
.map(e -> QuotedStringTokenizer.quote(e.getKey()) +
|
||||
":" +
|
||||
QuotedStringTokenizer.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.map(e -> HttpField.NAME_VALUE_TOKENIZER.quote(e.getKey()) + ":" + HttpField.NAME_VALUE_TOKENIZER.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.collect(Collectors.joining(",\n", "{\n", "\n}")));
|
||||
}
|
||||
|
||||
|
|
|
@ -253,7 +253,15 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
public long getDateHeader(String name)
|
||||
{
|
||||
HttpFields fields = getFields();
|
||||
return fields == null ? -1 : fields.getDateField(name);
|
||||
if (fields == null)
|
||||
return -1;
|
||||
HttpField field = fields.getField(name);
|
||||
if (field == null)
|
||||
return -1;
|
||||
long date = fields.getDateField(name);
|
||||
if (date == -1)
|
||||
throw new IllegalArgumentException("Cannot parse date");
|
||||
return date;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -41,7 +41,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
|
||||
public class QuickStartTest
|
||||
{
|
||||
|
||||
@Test
|
||||
public void testStandardTestWar() throws Exception
|
||||
{
|
||||
|
|
|
@ -31,6 +31,7 @@ import jakarta.servlet.RequestDispatcher;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
@ -39,7 +40,6 @@ import org.eclipse.jetty.http.QuotedQualityCSV;
|
|||
import org.eclipse.jetty.io.ByteBufferOutputStream;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -478,9 +478,7 @@ public class ErrorHandler extends AbstractHandler
|
|||
}
|
||||
|
||||
writer.append(json.entrySet().stream()
|
||||
.map(e -> QuotedStringTokenizer.quote(e.getKey()) +
|
||||
":" +
|
||||
QuotedStringTokenizer.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.map(e -> HttpField.NAME_VALUE_TOKENIZER.quote(e.getKey()) + ":" + HttpField.NAME_VALUE_TOKENIZER.quote(StringUtil.sanitizeXmlString((e.getValue()))))
|
||||
.collect(Collectors.joining(",\n", "{\n", "\n}")));
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,14 @@ import java.nio.file.StandardOpenOption;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.Part;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
import org.eclipse.jetty.util.ExceptionUtil;
|
||||
|
@ -91,6 +93,7 @@ public class MultiPartFormInputStream
|
|||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormInputStream.class);
|
||||
private static final QuotedStringTokenizer QUOTED_STRING_TOKENIZER = QuotedStringTokenizer.builder().delimiters(";").ignoreOptionalWhiteSpace().allowEmbeddedQuotes().build();
|
||||
|
||||
private final AutoLock _lock = new AutoLock();
|
||||
private final MultiMap<Part> _parts = new MultiMap<>();
|
||||
|
@ -103,7 +106,6 @@ public class MultiPartFormInputStream
|
|||
private int _numParts = 0;
|
||||
private volatile Throwable _err;
|
||||
private volatile Path _tmpDir;
|
||||
private volatile boolean _deleteOnExit;
|
||||
private volatile boolean _writeFilesWithFilenames;
|
||||
private volatile int _bufferSize = 16 * 1024;
|
||||
private State state = State.UNPARSED;
|
||||
|
@ -586,7 +588,7 @@ public class MultiPartFormInputStream
|
|||
{
|
||||
int bend = _contentType.indexOf(";", bstart);
|
||||
bend = (bend < 0 ? _contentType.length() : bend);
|
||||
contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart, bend)).trim());
|
||||
contentTypeBoundary = HttpField.PARAMETER_TOKENIZER.unquote(value(_contentType.substring(bstart, bend)).trim());
|
||||
}
|
||||
|
||||
parser = new MultiPartParser(new Handler(), contentTypeBoundary);
|
||||
|
@ -734,12 +736,13 @@ public class MultiPartFormInputStream
|
|||
throw new IOException("Missing content-disposition");
|
||||
}
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
|
||||
QUOTED_STRING_TOKENIZER.tokenize(contentDisposition);
|
||||
|
||||
String name = null;
|
||||
String filename = null;
|
||||
while (tok.hasMoreTokens())
|
||||
for (Iterator<String> i = QUOTED_STRING_TOKENIZER.tokenize(contentDisposition); i.hasNext();)
|
||||
{
|
||||
String t = tok.nextToken().trim();
|
||||
String t = i.next();
|
||||
String tl = StringUtil.asciiToLowerCase(t);
|
||||
if (tl.startsWith("form-data"))
|
||||
formData = true;
|
||||
|
@ -888,7 +891,7 @@ public class MultiPartFormInputStream
|
|||
{
|
||||
int idx = nameEqualsValue.indexOf('=');
|
||||
String value = nameEqualsValue.substring(idx + 1).trim();
|
||||
return QuotedStringTokenizer.unquoteOnly(value);
|
||||
return HttpField.PARAMETER_TOKENIZER.unquote(value);
|
||||
}
|
||||
|
||||
private static String filenameValue(String nameEqualsValue)
|
||||
|
@ -910,11 +913,7 @@ public class MultiPartFormInputStream
|
|||
return value;
|
||||
}
|
||||
else
|
||||
// unquote the string, but allow any backslashes that don't
|
||||
// form a valid escape sequence to remain as many browsers
|
||||
// even on *nix systems will not escape a filename containing
|
||||
// backslashes
|
||||
return QuotedStringTokenizer.unquoteOnly(value, true);
|
||||
return HttpField.PARAMETER_TOKENIZER.unquote(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -767,7 +767,15 @@ public class Request implements HttpServletRequest
|
|||
public long getDateHeader(String name)
|
||||
{
|
||||
HttpFields fields = _httpFields;
|
||||
return fields == null ? -1 : fields.getDateField(name);
|
||||
if (fields == null)
|
||||
return -1;
|
||||
HttpField field = fields.getField(name);
|
||||
if (field == null)
|
||||
return -1;
|
||||
long date = fields.getDateField(name);
|
||||
if (date == -1)
|
||||
throw new IllegalArgumentException("Cannot parse date");
|
||||
return date;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.net.URI;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
@ -144,10 +145,9 @@ public class QuickStartDescriptorProcessor extends IterativeDescriptorProcessor
|
|||
{
|
||||
context.removeAttribute(name);
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(value, ",");
|
||||
while (tok.hasMoreElements())
|
||||
for (Iterator<String> i = QuotedStringTokenizer.CSV.tokenize(value); i.hasNext();)
|
||||
{
|
||||
values.add(tok.nextToken().trim());
|
||||
values.add(i.next());
|
||||
}
|
||||
}
|
||||
default -> values.add(value);
|
||||
|
|
|
@ -627,7 +627,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
|
|||
v.append(",\n ");
|
||||
else
|
||||
v.append("\n ");
|
||||
QuotedStringTokenizer.quote(v, i.toString());
|
||||
QuotedStringTokenizer.CSV.quote(v, i.toString());
|
||||
}
|
||||
}
|
||||
out.openTag("context-param")
|
||||
|
@ -668,7 +668,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
|
|||
v.append(",\n ");
|
||||
else
|
||||
v.append("\n ");
|
||||
QuotedStringTokenizer.quote(v, normalizer.normalize(i));
|
||||
QuotedStringTokenizer.CSV.quote(v, normalizer.normalize(i));
|
||||
}
|
||||
}
|
||||
out.openTag("context-param")
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
package org.eclipse.jetty.ee9.security.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serial;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.BitSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
@ -52,12 +54,13 @@ import org.slf4j.LoggerFactory;
|
|||
public class DigestAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DigestAuthenticator.class);
|
||||
private static final QuotedStringTokenizer TOKENIZER = QuotedStringTokenizer.builder().delimiters("=, ").returnDelimiters().allowEmbeddedQuotes().build();
|
||||
|
||||
private final SecureRandom _random = new SecureRandom();
|
||||
private final ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
|
||||
private final Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
|
||||
private long _maxNonceAgeMs = 60 * 1000;
|
||||
private int _maxNC = 1024;
|
||||
private ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<>();
|
||||
private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration)
|
||||
|
@ -117,20 +120,21 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
try
|
||||
{
|
||||
Request baseRequest = Request.getBaseRequest(request);
|
||||
if (baseRequest == null)
|
||||
return Authentication.UNAUTHENTICATED;
|
||||
|
||||
boolean stale = false;
|
||||
if (credentials != null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Credentials: {}", credentials);
|
||||
QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
|
||||
final Digest digest = new Digest(request.getMethod());
|
||||
String last = null;
|
||||
String name = null;
|
||||
|
||||
while (tokenizer.hasMoreTokens())
|
||||
for (Iterator<String> i = TOKENIZER.tokenize(credentials); i.hasNext();)
|
||||
{
|
||||
String tok = tokenizer.nextToken();
|
||||
String tok = i.next();
|
||||
char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
|
||||
|
||||
switch (c)
|
||||
|
@ -291,7 +295,7 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
|
||||
public boolean seen(int count)
|
||||
{
|
||||
try (AutoLock l = _lock.lock())
|
||||
try (AutoLock ignored = _lock.lock())
|
||||
{
|
||||
if (count >= _seen.size())
|
||||
return true;
|
||||
|
@ -304,6 +308,7 @@ public class DigestAuthenticator extends LoginAuthenticator
|
|||
|
||||
private static class Digest extends Credential
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2484639019549527724L;
|
||||
final String method;
|
||||
String username = "";
|
||||
|
|
Loading…
Reference in New Issue