From: Markus Koschany <apo@debian.org>
Date: Sat, 31 Jul 2021 17:21:57 +0200
Subject: CVE-2020-27223

---
 .../java/org/eclipse/jetty/http/QuotedCSV.java     | 280 ++-----------------
 .../org/eclipse/jetty/http/QuotedCSVParser.java    | 303 +++++++++++++++++++++
 .../org/eclipse/jetty/http/QuotedQualityCSV.java   | 140 ++++++----
 .../eclipse/jetty/http/QuotedQualityCSVTest.java   | 143 +++++-----
 4 files changed, 479 insertions(+), 387 deletions(-)
 create mode 100644 jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java

diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java
index 9ca7dbe..a356213 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java
@@ -1,6 +1,6 @@
 //
 //  ========================================================================
-//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
 //  ------------------------------------------------------------------------
 //  All rights reserved. This program and the accompanying materials
 //  are made available under the terms of the Eclipse Public License v1.0
@@ -22,236 +22,36 @@ import java.util.ArrayList;
 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.
  * 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 class QuotedCSV implements Iterable<String>
-{    
-    private enum State { VALUE, PARAM_NAME, PARAM_VALUE};
-    
+public class QuotedCSV extends QuotedCSVParser implements Iterable<String>
+{
     protected final List<String> _values = new ArrayList<>();
-    protected final boolean _keepQuotes;
-    
-    /* ------------------------------------------------------------ */
+
     public QuotedCSV(String... values)
     {
-        this(true,values);
+        this(true, values);
     }
-    
-    /* ------------------------------------------------------------ */
-    public QuotedCSV(boolean keepQuotes,String... values)
-    {
-        _keepQuotes=keepQuotes;
-        for (String v:values)
-            addValue(v);
-    }
-    
-    /* ------------------------------------------------------------ */
-    /** Add and parse a value string(s) 
-     * @param value A value that may contain one or more Quoted CSV items.
-     */
-    public void addValue(String value)
+
+    public QuotedCSV(boolean keepQuotes, String... values)
     {
-        if (value == null)
-            return;
-        
-        StringBuffer buffer = new StringBuffer();
-        
-        int l=value.length();
-        State state=State.VALUE;
-        boolean quoted=false;
-        boolean sloshed=false;
-        int nws_length=0;
-        int last_length=0;
-        int value_length=-1;
-        int param_name=-1;
-        int param_value=-1;
-        
-        for (int i=0;i<=l;i++)
+        super(keepQuotes);
+        for (String v : values)
         {
-            char c=i==l?0:value.charAt(i);
-            
-            // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6
-            if (quoted && c!=0)
-            {
-                if (sloshed)
-                    sloshed=false;
-                else
-                {
-                    switch(c)
-                    {
-                        case '\\':
-                            sloshed=true;
-                            if (!_keepQuotes)
-                                continue;
-                            break;
-                        case '"':
-                            quoted=false;
-                            if (!_keepQuotes)
-                                continue;
-                            break;
-                    }
-                }
-
-                buffer.append(c);
-                nws_length=buffer.length();
-                continue;
-            }
-            
-            // Handle common cases
-            switch(c)
-            {
-                case ' ':
-                case '\t':
-                    if (buffer.length()>last_length) // not leading OWS
-                        buffer.append(c);
-                    continue;
-
-                case '"':
-                    quoted=true;
-                    if (_keepQuotes)
-                    {
-                        if (state==State.PARAM_VALUE && param_value<0)
-                            param_value=nws_length;
-                        buffer.append(c);
-                    }
-                    else if (state==State.PARAM_VALUE && param_value<0)
-                        param_value=nws_length;
-                    nws_length=buffer.length();
-                    continue;
-                    
-                case ';':                    
-                    buffer.setLength(nws_length); // trim following OWS
-                    if (state==State.VALUE)
-                    {
-                        parsedValue(buffer);
-                        value_length=buffer.length();
-                    }
-                    else
-                        parsedParam(buffer,value_length,param_name,param_value);
-                    nws_length=buffer.length();
-                    param_name=param_value=-1;
-                    buffer.append(c);
-                    last_length=++nws_length;
-                    state=State.PARAM_NAME;
-                    continue;
-                    
-                case ',':
-                case 0:
-                    if (nws_length>0)
-                    {
-                        buffer.setLength(nws_length); // trim following OWS
-                        switch(state)
-                        {
-                            case VALUE:
-                                parsedValue(buffer);
-                                value_length=buffer.length();
-                                break;
-                            case PARAM_NAME:
-                            case PARAM_VALUE:
-                                parsedParam(buffer,value_length,param_name,param_value);
-                                break;
-                        }
-                        _values.add(buffer.toString());
-                    }
-                    buffer.setLength(0);
-                    last_length=0;
-                    nws_length=0;
-                    value_length=param_name=param_value=-1;
-                    state=State.VALUE;
-                    continue;
-
-                case '=':
-                    switch (state)
-                    {
-                        case VALUE:
-                            // It wasn't really a value, it was a param name
-                            value_length=param_name=0;
-                            buffer.setLength(nws_length); // trim following OWS
-                            String param = buffer.toString();
-                            buffer.setLength(0);
-                            parsedValue(buffer);
-                            value_length=buffer.length();
-                            buffer.append(param);
-                            buffer.append(c);
-                            last_length=++nws_length;
-                            state=State.PARAM_VALUE;
-                            continue;
-
-                        case PARAM_NAME:
-                            buffer.setLength(nws_length); // trim following OWS
-                            buffer.append(c);
-                            last_length=++nws_length;
-                            state=State.PARAM_VALUE;
-                            continue;
-
-                        case PARAM_VALUE:
-                            if (param_value<0)
-                                param_value=nws_length;
-                            buffer.append(c);
-                            nws_length=buffer.length();
-                            continue;
-                    }
-                    continue;
-                    
-                default:
-                {
-                    switch (state)
-                    {
-                        case VALUE:
-                        {
-                            buffer.append(c);
-                            nws_length=buffer.length();
-                            continue;
-                        }
-
-                        case PARAM_NAME:
-                        {
-                            if (param_name<0)
-                                param_name=nws_length;
-                            buffer.append(c);
-                            nws_length=buffer.length();
-                            continue;
-                        }
-
-                        case PARAM_VALUE:
-                        {
-                            if (param_value<0)
-                                param_value=nws_length;
-                            buffer.append(c);
-                            nws_length=buffer.length();
-                            continue;
-                        }
-                    }
-                }
-            }  
+            addValue(v);
         }
     }
 
-    /**
-     * Called when a value has been parsed
-     * @param buffer Containing the trimmed value, which may be mutated
-     */
-    protected void parsedValue(StringBuffer buffer)
-    {
-    }
-
-    /**
-     * Called when a parameter has been parsed
-     * @param buffer Containing the trimmed value and all parameters, which may be mutated
-     * @param valueLength The length of the value
-     * @param paramName The index of the start of the parameter just parsed
-     * @param paramValue The index of the start of the parameter value just parsed, or -1
-     */
-    protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+    @Override
+    protected void parsedValueAndParams(StringBuffer buffer)
     {
+        _values.add(buffer.toString());
     }
 
     public int size()
@@ -268,67 +68,21 @@ public class QuotedCSV implements Iterable<String>
     {
         return _values;
     }
-    
+
     @Override
     public Iterator<String> iterator()
     {
         return _values.iterator();
     }
-    
-    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();
-    }
-    
     @Override
     public String toString()
     {
         List<String> list = new ArrayList<>();
         for (String s : this)
+        {
             list.add(s);
+        }
         return list.toString();
     }
 }
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java
new file mode 100644
index 0000000..7aefcf7
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSVParser.java
@@ -0,0 +1,303 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+/**
+ * Implements a quoted comma separated list parser
+ * in accordance with RFC7230.
+ * 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
+{
+    private enum State
+    {
+        VALUE, PARAM_NAME, PARAM_VALUE
+    }
+
+    protected final boolean _keepQuotes;
+
+    public QuotedCSVParser(boolean keepQuotes)
+    {
+        _keepQuotes = keepQuotes;
+    }
+
+    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();
+    }
+
+    /**
+     * Add and parse a value string(s)
+     *
+     * @param value A value that may contain one or more Quoted CSV items.
+     */
+    public void addValue(String value)
+    {
+        if (value == null)
+            return;
+
+        StringBuffer buffer = new StringBuffer();
+
+        int l = value.length();
+        State state = State.VALUE;
+        boolean quoted = false;
+        boolean sloshed = false;
+        int nwsLength = 0;
+        int lastLength = 0;
+        int valueLength = -1;
+        int paramName = -1;
+        int paramValue = -1;
+
+        for (int i = 0; i <= l; i++)
+        {
+            char c = i == l ? 0 : value.charAt(i);
+
+            // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6
+            if (quoted && c != 0)
+            {
+                if (sloshed)
+                    sloshed = false;
+                else
+                {
+                    switch (c)
+                    {
+                        case '\\':
+                            sloshed = true;
+                            if (!_keepQuotes)
+                                continue;
+                            break;
+                        case '"':
+                            quoted = false;
+                            if (!_keepQuotes)
+                                continue;
+                            break;
+                    }
+                }
+
+                buffer.append(c);
+                nwsLength = buffer.length();
+                continue;
+            }
+
+            // Handle common cases
+            switch (c)
+            {
+                case ' ':
+                case '\t':
+                    if (buffer.length() > lastLength) // not leading OWS
+                        buffer.append(c);
+                    continue;
+
+                case '"':
+                    quoted = true;
+                    if (_keepQuotes)
+                    {
+                        if (state == State.PARAM_VALUE && paramValue < 0)
+                            paramValue = nwsLength;
+                        buffer.append(c);
+                    }
+                    else if (state == State.PARAM_VALUE && paramValue < 0)
+                        paramValue = nwsLength;
+                    nwsLength = buffer.length();
+                    continue;
+
+                case ';':
+                    buffer.setLength(nwsLength); // trim following OWS
+                    if (state == State.VALUE)
+                    {
+                        parsedValue(buffer);
+                        valueLength = buffer.length();
+                    }
+                    else
+                        parsedParam(buffer, valueLength, paramName, paramValue);
+                    nwsLength = buffer.length();
+                    paramName = paramValue = -1;
+                    buffer.append(c);
+                    lastLength = ++nwsLength;
+                    state = State.PARAM_NAME;
+                    continue;
+
+                case ',':
+                case 0:
+                    if (nwsLength > 0)
+                    {
+                        buffer.setLength(nwsLength); // trim following OWS
+                        switch (state)
+                        {
+                            case VALUE:
+                                parsedValue(buffer);
+                                valueLength = buffer.length();
+                                break;
+                            case PARAM_NAME:
+                            case PARAM_VALUE:
+                                parsedParam(buffer, valueLength, paramName, paramValue);
+                                break;
+                        }
+                        parsedValueAndParams(buffer);
+                    }
+                    buffer.setLength(0);
+                    lastLength = 0;
+                    nwsLength = 0;
+                    valueLength = paramName = paramValue = -1;
+                    state = State.VALUE;
+                    continue;
+
+                case '=':
+                    switch (state)
+                    {
+                        case VALUE:
+                            // It wasn't really a value, it was a param name
+                            valueLength = paramName = 0;
+                            buffer.setLength(nwsLength); // trim following OWS
+                            String param = buffer.toString();
+                            buffer.setLength(0);
+                            parsedValue(buffer);
+                            valueLength = buffer.length();
+                            buffer.append(param);
+                            buffer.append(c);
+                            lastLength = ++nwsLength;
+                            state = State.PARAM_VALUE;
+                            continue;
+
+                        case PARAM_NAME:
+                            buffer.setLength(nwsLength); // trim following OWS
+                            buffer.append(c);
+                            lastLength = ++nwsLength;
+                            state = State.PARAM_VALUE;
+                            continue;
+
+                        case PARAM_VALUE:
+                            if (paramValue < 0)
+                                paramValue = nwsLength;
+                            buffer.append(c);
+                            nwsLength = buffer.length();
+                            continue;
+                    }
+                    continue;
+
+                default:
+                {
+                    switch (state)
+                    {
+                        case VALUE:
+                        {
+                            buffer.append(c);
+                            nwsLength = buffer.length();
+                            continue;
+                        }
+
+                        case PARAM_NAME:
+                        {
+                            if (paramName < 0)
+                                paramName = nwsLength;
+                            buffer.append(c);
+                            nwsLength = buffer.length();
+                            continue;
+                        }
+
+                        case PARAM_VALUE:
+                        {
+                            if (paramValue < 0)
+                                paramValue = nwsLength;
+                            buffer.append(c);
+                            nwsLength = buffer.length();
+                            continue;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Called when a value and it's parameters has been parsed
+     *
+     * @param buffer Containing the trimmed value and parameters
+     */
+    protected void parsedValueAndParams(StringBuffer buffer)
+    {
+    }
+
+    /**
+     * Called when a value has been parsed (prior to any parameters)
+     *
+     * @param buffer Containing the trimmed value, which may be mutated
+     */
+    protected void parsedValue(StringBuffer buffer)
+    {
+    }
+
+    /**
+     * Called when a parameter has been parsed
+     *
+     * @param buffer Containing the trimmed value and all parameters, which may be mutated
+     * @param valueLength The length of the value
+     * @param paramName The index of the start of the parameter just parsed
+     * @param paramValue The index of the start of the parameter value just parsed, or -1
+     */
+    protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
+    {
+    }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
index d148d9e..5bc9985 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedQualityCSV.java
@@ -1,6 +1,6 @@
 //
 //  ========================================================================
-//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
 //  ------------------------------------------------------------------------
 //  All rights reserved. This program and the accompanying materials
 //  are made available under the terms of the Eclipse Public License v1.0
@@ -21,14 +21,12 @@ package org.eclipse.jetty.http;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.ToIntFunction;
+import java.util.stream.Collectors;
 
 import org.eclipse.jetty.util.log.Log;
 
-import static java.lang.Integer.MIN_VALUE;
-
-/* ------------------------------------------------------------ */
-
 /**
  * Implements a quoted comma separated list of quality values
  * in accordance with RFC7230 and RFC7231.
@@ -57,22 +55,19 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
         return 3;
     };
 
-    private final List<Double> _quality = new ArrayList<>();
+    private final List<QualityValue> _qualities = new ArrayList<>();
+    private QualityValue _lastQuality;
     private boolean _sorted = false;
     private final ToIntFunction<String> _secondaryOrdering;
 
-    /* ------------------------------------------------------------ */
-
     /**
      * Sorts values with equal quality according to the length of the value String.
      */
     public QuotedQualityCSV()
     {
-        this((ToIntFunction)null);
+        this((ToIntFunction<String>)null);
     }
 
-    /* ------------------------------------------------------------ */
-
     /**
      * Sorts values with equal quality according to given order.
      *
@@ -83,57 +78,71 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
         this((s) ->
         {
             for (int i = 0; i < preferredOrder.length; ++i)
+            {
                 if (preferredOrder[i].equals(s))
                     return preferredOrder.length - i;
+            }
 
             if ("*".equals(s))
                 return preferredOrder.length;
 
-            return MIN_VALUE;
+            return 0;
         });
     }
 
-    /* ------------------------------------------------------------ */
-
     /**
      * Orders values with equal quality with the given function.
      *
-     * @param secondaryOrdering Function to apply an ordering other than specified by quality
+     * @param secondaryOrdering Function to apply an ordering other than specified by quality, highest values are sorted first.
      */
     public QuotedQualityCSV(ToIntFunction<String> secondaryOrdering)
     {
         this._secondaryOrdering = secondaryOrdering == null ? s -> 0 : secondaryOrdering;
     }
 
-    /* ------------------------------------------------------------ */
+    @Override
+    protected void parsedValueAndParams(StringBuffer buffer)
+    {
+        super.parsedValueAndParams(buffer);
+
+        // Collect full value with parameters
+        _lastQuality = new QualityValue(_lastQuality._quality, buffer.toString(), _lastQuality._index);
+        _qualities.set(_lastQuality._index, _lastQuality);
+    }
+
     @Override
     protected void parsedValue(StringBuffer buffer)
     {
         super.parsedValue(buffer);
 
+        _sorted = false;
+
+        // This is the just the value, without parameters.
         // Assume a quality of ONE
-        _quality.add(1.0D);
+        _lastQuality = new QualityValue(1.0D, buffer.toString(), _qualities.size());
+        _qualities.add(_lastQuality);
     }
 
-    /* ------------------------------------------------------------ */
     @Override
     protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
     {
+        _sorted = false;
+
         if (paramName < 0)
         {
             if (buffer.charAt(buffer.length() - 1) == ';')
                 buffer.setLength(buffer.length() - 1);
         }
         else if (paramValue >= 0 &&
-                buffer.charAt(paramName) == 'q' && paramValue > paramName &&
-                buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=')
+            buffer.charAt(paramName) == 'q' && paramValue > paramName &&
+            buffer.length() >= paramName && buffer.charAt(paramName + 1) == '=')
         {
-            Double q;
+            double q;
             try
             {
                 q = (_keepQuotes && buffer.charAt(paramValue) == '"')
-                        ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1))
-                        : Double.valueOf(buffer.substring(paramValue));
+                    ? Double.valueOf(buffer.substring(paramValue + 1, buffer.length() - 1))
+                    : Double.valueOf(buffer.substring(paramValue));
             }
             catch (Exception e)
             {
@@ -143,8 +152,10 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
             buffer.setLength(Math.max(0, paramName - 1));
 
             if (q != 1.0D)
-                // replace assumed quality
-                _quality.set(_quality.size() - 1, q);
+            {
+                _lastQuality = new QualityValue(q, buffer.toString(), _lastQuality._index);
+                _qualities.set(_lastQuality._index, _lastQuality);
+            }
         }
     }
 
@@ -166,38 +177,73 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
 
     protected void sort()
     {
+        _values.clear();
+        _qualities.stream()
+            .filter((qv) -> qv._quality != 0.0D)
+            .sorted()
+            .map(QualityValue::getValue)
+            .collect(Collectors.toCollection(() -> _values));
         _sorted = true;
+    }
+
+    private class QualityValue implements Comparable<QualityValue>
+    {
+        private final double _quality;
+        private final String _value;
+        private final int _index;
 
-        Double last = 0.0D;
-        int lastSecondaryOrder = Integer.MIN_VALUE;
+        private QualityValue(double quality, String value, int index)
+        {
+            _quality = quality;
+            _value = value;
+            _index = index;
+        }
 
-        for (int i = _values.size(); i-- > 0; )
+        @Override
+        public int hashCode()
         {
-            String v = _values.get(i);
-            Double q = _quality.get(i);
+            return Double.hashCode(_quality) ^ Objects.hash(_value, _index);
+        }
 
-            int compare = last.compareTo(q);
-            if (compare > 0 || (compare == 0 && _secondaryOrdering.applyAsInt(v) < lastSecondaryOrder))
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (!(obj instanceof QualityValue))
+                return false;
+            QualityValue qv = (QualityValue)obj;
+            return _quality == qv._quality && Objects.equals(_value, qv._value) && Objects.equals(_index, qv._index);
+        }
+
+        private String getValue()
+        {
+            return _value;
+        }
+
+        @Override
+        public int compareTo(QualityValue o)
+        {
+            // sort highest quality first
+            int compare = Double.compare(o._quality, _quality);
+            if (compare == 0)
             {
-                _values.set(i, _values.get(i + 1));
-                _values.set(i + 1, v);
-                _quality.set(i, _quality.get(i + 1));
-                _quality.set(i + 1, q);
-                last = 0.0D;
-                lastSecondaryOrder = 0;
-                i = _values.size();
-                continue;
+                // then sort secondary order highest first
+                compare = Integer.compare(_secondaryOrdering.applyAsInt(o._value), _secondaryOrdering.applyAsInt(_value));
+                if (compare == 0)
+                    // then sort index lowest first
+                    compare = -Integer.compare(o._index, _index);
             }
-
-            last = q;
-            lastSecondaryOrder = _secondaryOrdering.applyAsInt(v);
+            return compare;
         }
 
-        int last_element = _quality.size();
-        while (last_element > 0 && _quality.get(--last_element).equals(0.0D))
+        @Override
+        public String toString()
         {
-            _quality.remove(last_element);
-            _values.remove(last_element);
+            return String.format("%s@%x[%s,q=%f,i=%d]",
+                getClass().getSimpleName(),
+                hashCode(),
+                _value,
+                _quality,
+                _index);
         }
     }
 }
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java
index f03657b..b941e95 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedQualityCSVTest.java
@@ -1,6 +1,6 @@
 //
 //  ========================================================================
-//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
 //  ------------------------------------------------------------------------
 //  All rights reserved. This program and the accompanying materials
 //  are made available under the terms of the Eclipse Public License v1.0
@@ -18,13 +18,12 @@
 
 package org.eclipse.jetty.http;
 
+import java.util.ArrayList;
+import java.util.List;
 
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Test;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 
@@ -32,58 +31,58 @@ public class QuotedQualityCSVTest
 {
 
     @Test
-    public void test7231_5_3_2_example1()
+    public void test7231Sec532Example1()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue(" audio/*; q=0.2, audio/basic");
-        assertThat(values,Matchers.contains("audio/basic","audio/*"));
+        assertThat(values, Matchers.contains("audio/basic", "audio/*"));
     }
 
     @Test
-    public void test7231_5_3_2_example2()
+    public void test7231Sec532Example2()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("text/plain; q=0.5, text/html,");
         values.addValue("text/x-dvi; q=0.8, text/x-c");
-        assertThat(values,Matchers.contains("text/html","text/x-c","text/x-dvi","text/plain"));
+        assertThat(values, Matchers.contains("text/html", "text/x-c", "text/x-dvi", "text/plain"));
     }
-    
+
     @Test
-    public void test7231_5_3_2_example3()
+    public void test7231Sec532Example3()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
-        
+
         // Note this sort is only on quality and not the most specific type as per 5.3.2
-        assertThat(values,Matchers.contains("text/*","text/plain","text/plain;format=flowed","*/*"));
+        assertThat(values, Matchers.contains("text/*", "text/plain", "text/plain;format=flowed", "*/*"));
     }
-    
+
     @Test
-    public void test7231_5_3_2_example3_most_specific()
+    public void test7231532Example3MostSpecific()
     {
         QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING);
         values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
-        
-        assertThat(values,Matchers.contains("text/plain;format=flowed","text/plain","text/*","*/*"));
+
+        assertThat(values, Matchers.contains("text/plain;format=flowed", "text/plain", "text/*", "*/*"));
     }
-    
+
     @Test
-    public void test7231_5_3_2_example4()
+    public void test7231Sec532Example4()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("text/*;q=0.3, text/html;q=0.7, text/html;level=1,");
         values.addValue("text/html;level=2;q=0.4, */*;q=0.5");
-        assertThat(values,Matchers.contains(
-                "text/html;level=1",
-                "text/html",
-                "*/*",
-                "text/html;level=2",
-                "text/*"
-                ));
+        assertThat(values, Matchers.contains(
+            "text/html;level=1",
+            "text/html",
+            "*/*",
+            "text/html;level=2",
+            "text/*"
+        ));
     }
-    
+
     @Test
-    public void test7231_5_3_4_example1()
+    public void test7231Sec534Example1()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("compress, gzip");
@@ -91,16 +90,16 @@ public class QuotedQualityCSVTest
         values.addValue("*");
         values.addValue("compress;q=0.5, gzip;q=1.0");
         values.addValue("gzip;q=1.0, identity; q=0.5, *;q=0");
-        
-        assertThat(values,Matchers.contains(
-                "compress",
-                "gzip",
-                "*",
-                "gzip",
-                "gzip",
-                "compress",
-                "identity"
-                ));
+
+        assertThat(values, Matchers.contains(
+            "compress",
+            "gzip",
+            "*",
+            "gzip",
+            "gzip",
+            "compress",
+            "identity"
+        ));
     }
 
     @Test
@@ -108,66 +107,65 @@ public class QuotedQualityCSVTest
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("  value 0.5  ;  p = v  ;  q =0.5  ,  value 1.0 ");
-        assertThat(values,Matchers.contains(
-                "value 1.0",
-                "value 0.5;p=v"));
+        assertThat(values, Matchers.contains(
+            "value 1.0",
+            "value 0.5;p=v"));
     }
-    
+
     @Test
     public void testEmpty()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue(",aaaa,  , bbbb ,,cccc,");
-        assertThat(values,Matchers.contains(
-                "aaaa",
-                "bbbb",
-                "cccc"));
+        assertThat(values, Matchers.contains(
+            "aaaa",
+            "bbbb",
+            "cccc"));
     }
-        
+
     @Test
     public void testQuoted()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("  value 0.5  ;  p = \"v  ;  q = \\\"0.5\\\"  ,  value 1.0 \"  ");
-        assertThat(values,Matchers.contains(
-                "value 0.5;p=\"v  ;  q = \\\"0.5\\\"  ,  value 1.0 \""));
+        assertThat(values, Matchers.contains(
+            "value 0.5;p=\"v  ;  q = \\\"0.5\\\"  ,  value 1.0 \""));
     }
-    
+
     @Test
     public void testOpenQuote()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("value;p=\"v");
-        assertThat(values,Matchers.contains(
-                "value;p=\"v"));
+        assertThat(values, Matchers.contains(
+            "value;p=\"v"));
     }
-    
+
     @Test
     public void testQuotedQuality()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("  value 0.5  ;  p = v  ;  q = \"0.5\"  ,  value 1.0 ");
-        assertThat(values,Matchers.contains(
-                "value 1.0",
-                "value 0.5;p=v"));
+        assertThat(values, Matchers.contains(
+            "value 1.0",
+            "value 0.5;p=v"));
     }
-    
+
     @Test
     public void testBadQuality()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("value0.5;p=v;q=0.5,value1.0,valueBad;q=X");
-        assertThat(values,Matchers.contains(
-                "value1.0",
-                "value0.5;p=v"));
+        assertThat(values, Matchers.contains(
+            "value1.0",
+            "value0.5;p=v"));
     }
-    
+
     @Test
     public void testBad()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
 
-        
         // None of these should throw exceptions
         values.addValue(null);
         values.addValue("");
@@ -223,13 +221,10 @@ public class QuotedQualityCSVTest
         values.addValue("q=");
         values.addValue("q=,");
         values.addValue("q=;");
-        
     }
 
-    /* ------------------------------------------------------------ */
-
-    private static final String[] preferBrotli = {"br","gzip"};
-    private static final String[] preferGzip = {"gzip","br"};
+    private static final String[] preferBrotli = {"br", "gzip"};
+    private static final String[] preferGzip = {"gzip", "br"};
     private static final String[] noFormats = {};
 
     @Test
@@ -295,14 +290,13 @@ public class QuotedQualityCSVTest
         values.addValue("gzip, *");
         assertThat(values, contains("*", "gzip"));
     }
-    
 
     @Test
     public void testSameQuality()
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("one;q=0.5,two;q=0.5,three;q=0.5");
-        assertThat(values.getValues(),Matchers.contains("one","two","three"));
+        assertThat(values.getValues(), Matchers.contains("one", "two", "three"));
     }
 
     @Test
@@ -310,10 +304,9 @@ public class QuotedQualityCSVTest
     {
         QuotedQualityCSV values = new QuotedQualityCSV();
         values.addValue("one,two;,three;x=y");
-        assertThat(values.getValues(),Matchers.contains("one","two","three;x=y"));
+        assertThat(values.getValues(), Matchers.contains("one", "two", "three;x=y"));
     }
 
-
     @Test
     public void testQuality()
     {
@@ -339,19 +332,15 @@ public class QuotedQualityCSVTest
             }
         };
 
-
         // The provided string is not legal according to some RFCs ( not a token because of = and not a parameter because not preceded by ; )
         // The string is legal according to RFC7239 which allows for just parameters (called forwarded-pairs)
         values.addValue("p=0.5,q=0.5");
 
-
         // The QuotedCSV implementation is lenient and adopts the later interpretation and thus sees q=0.5 and p=0.5 both as parameters
-        assertThat(results,contains("parsedValue: ", "parsedParam: p=0.5",
-                                    "parsedValue: ", "parsedParam: q=0.5"));
-
+        assertThat(results, contains("parsedValue: ", "parsedParam: p=0.5",
+            "parsedValue: ", "parsedParam: q=0.5"));
 
         // However the QuotedQualityCSV only handles the q parameter and that is consumed from the parameter string.
-        assertThat(values,contains("p=0.5", ""));
-
+        assertThat(values, contains("p=0.5", ""));
     }
 }
