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:
Greg Wilkins 2023-05-18 08:25:53 +02:00 committed by GitHub
parent 3e5d479f39
commit 068a60a868
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1597 additions and 844 deletions

View File

@ -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);

View File

@ -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;
}
/**

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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"));
}
}

View File

@ -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());

View File

@ -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");

View File

@ -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\""));
}
}

View File

@ -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
);

View File

@ -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 = "";

View File

@ -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);
}

View File

@ -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}")));
}

View File

@ -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;

View File

@ -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"));

View File

@ -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'));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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()));
}
}
}

View File

@ -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("]");

View File

@ -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);

View File

@ -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")

View File

@ -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}")));
}

View File

@ -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

View File

@ -41,7 +41,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class QuickStartTest
{
@Test
public void testStandardTestWar() throws Exception
{

View File

@ -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}")));
}

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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);

View File

@ -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")

View File

@ -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 = "";