mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Handle MIME type parameter names case-insensitively
MIME type parameter names are case-insensitive, and MimeType already stores them in a LinkedCaseInsensitiveMap. Several code paths, however, still compared them with case-sensitive String.equals(). As a result, MimeType.hashCode() disagreed with MimeType.equals() for parameter names that differ only in case, breaking the equals/hashCode contract: text/plain;FOO=bar and text/plain;foo=bar are equal but hash differently, so one is not found in a hash-based collection holding the other. MimeType.compareTo() had the same blind spot for the charset parameter. MediaType was affected in two further ways: an out-of-range quality value escaped validation when spelled Q=, and removeQualityValue() left a Q= parameter in place. Signed-off-by: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com>
This commit is contained in:
committed by
Brian Clozel
parent
ee19b96ee9
commit
2028eb3694
@@ -486,9 +486,9 @@ public class MimeType implements Comparable<MimeType>, Serializable {
|
||||
/**
|
||||
* Determine if the parameters in this {@code MimeType} and the supplied
|
||||
* {@code MimeType} are equal, performing case-insensitive comparisons
|
||||
* for {@link Charset Charsets} and disregarding quoting of parameter
|
||||
* values, so that, for example, {@code spring="framework"} and
|
||||
* {@code spring=framework} are considered equal.
|
||||
* for parameter names and {@link Charset Charsets}, and disregarding
|
||||
* quoting of parameter values, so that, for example, {@code spring="framework"}
|
||||
* and {@code spring=framework} are considered equal.
|
||||
* @since 4.2
|
||||
*/
|
||||
private boolean parametersAreEqual(MimeType other) {
|
||||
@@ -501,7 +501,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
|
||||
if (!other.parameters.containsKey(key)) {
|
||||
return false;
|
||||
}
|
||||
if (PARAM_CHARSET.equals(key)) {
|
||||
if (PARAM_CHARSET.equalsIgnoreCase(key)) {
|
||||
if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) {
|
||||
return false;
|
||||
}
|
||||
@@ -528,15 +528,15 @@ public class MimeType implements Comparable<MimeType>, Serializable {
|
||||
|
||||
/**
|
||||
* Compute a hash code for the parameters map, consistent with
|
||||
* {@link #parametersAreEqual}: normalizing {@link Charset Charsets} and
|
||||
* disregarding quoting of parameter values.
|
||||
* {@link #parametersAreEqual}: normalizing parameter names and
|
||||
* {@link Charset Charsets}, and disregarding quoting of parameter values.
|
||||
*/
|
||||
private int parametersHashCode() {
|
||||
int result = 0;
|
||||
for (Map.Entry<String, String> entry : this.parameters.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = (PARAM_CHARSET.equals(key) ? getCharset() : unquote(entry.getValue()));
|
||||
result += key.hashCode() ^ ObjectUtils.nullSafeHashCode(value);
|
||||
Object value = (PARAM_CHARSET.equalsIgnoreCase(key) ? getCharset() : unquote(entry.getValue()));
|
||||
result += key.toLowerCase(Locale.ROOT).hashCode() ^ ObjectUtils.nullSafeHashCode(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -602,7 +602,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
|
||||
if (comp != 0) {
|
||||
return comp;
|
||||
}
|
||||
if (PARAM_CHARSET.equals(thisAttribute)) {
|
||||
if (PARAM_CHARSET.equalsIgnoreCase(thisAttribute)) {
|
||||
Charset thisCharset = getCharset();
|
||||
Charset otherCharset = other.getCharset();
|
||||
if (thisCharset != otherCharset) {
|
||||
|
||||
@@ -545,6 +545,28 @@ class MimeTypeTests {
|
||||
assertThat(m2.compareTo(m1)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsIsCaseInsensitiveForParameterNames() {
|
||||
MimeType m1 = new MimeType("text", "plain", singletonMap("Spring", "framework"));
|
||||
MimeType m2 = new MimeType("text", "plain", singletonMap("spring", "framework"));
|
||||
assertThat(m1).isEqualTo(m2);
|
||||
assertThat(m2).isEqualTo(m1);
|
||||
assertThat(m1).hasSameHashCodeAs(m2);
|
||||
assertThat(m1.compareTo(m2)).isEqualTo(0);
|
||||
assertThat(m2.compareTo(m1)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsIsCaseInsensitiveForCharsetParameterName() {
|
||||
MimeType m1 = new MimeType("text", "plain", singletonMap("Charset", "UTF-8"));
|
||||
MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8"));
|
||||
assertThat(m1).isEqualTo(m2);
|
||||
assertThat(m2).isEqualTo(m1);
|
||||
assertThat(m1).hasSameHashCodeAs(m2);
|
||||
assertThat(m1.compareTo(m2)).isEqualTo(0);
|
||||
assertThat(m2.compareTo(m1)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test // gh-36729
|
||||
void equalsIgnoresParameterValueQuoting() {
|
||||
MimeType m1 = MimeTypeUtils.parseMimeType("text/plain; spring=\"framework\"");
|
||||
|
||||
@@ -503,7 +503,7 @@ public class MediaType extends MimeType implements Serializable {
|
||||
@Override
|
||||
protected void checkParameters(String parameter, String value) {
|
||||
super.checkParameters(parameter, value);
|
||||
if (PARAM_QUALITY_FACTOR.equals(parameter)) {
|
||||
if (PARAM_QUALITY_FACTOR.equalsIgnoreCase(parameter)) {
|
||||
String unquotedValue = unquote(value);
|
||||
double d = Double.parseDouble(unquotedValue);
|
||||
Assert.isTrue(d >= 0D && d <= 1D,
|
||||
@@ -651,7 +651,7 @@ public class MediaType extends MimeType implements Serializable {
|
||||
return this;
|
||||
}
|
||||
Map<String, String> params = new LinkedHashMap<>(getParameters());
|
||||
params.remove(PARAM_QUALITY_FACTOR);
|
||||
params.keySet().removeIf(PARAM_QUALITY_FACTOR::equalsIgnoreCase);
|
||||
return new MediaType(this, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,12 @@ class MediaTypeTests {
|
||||
MediaType.parseMediaType("audio/basic;q=1.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseMediaTypeIllegalQualityFactorWithUpperCaseParameterName() {
|
||||
assertThatExceptionOfType(InvalidMediaTypeException.class).isThrownBy(() ->
|
||||
MediaType.parseMediaType("audio/basic;Q=1.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseMediaTypeIllegalCharset() {
|
||||
assertThatExceptionOfType(InvalidMediaTypeException.class).isThrownBy(() ->
|
||||
@@ -293,6 +299,18 @@ class MediaTypeTests {
|
||||
assertThat(new MediaType("text", "*").isConcrete()).as("text/* concrete").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeQualityValue() {
|
||||
assertThat(MediaType.parseMediaType("audio/basic;q=0.8").removeQualityValue())
|
||||
.isEqualTo(MediaType.parseMediaType("audio/basic"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeQualityValueWithUpperCaseParameterName() {
|
||||
assertThat(MediaType.parseMediaType("audio/basic;Q=0.8").removeQualityValue())
|
||||
.isEqualTo(MediaType.parseMediaType("audio/basic"));
|
||||
}
|
||||
|
||||
@Test // gh-26127
|
||||
void serialize() throws Exception {
|
||||
MediaType original = new MediaType("text", "plain", StandardCharsets.UTF_8);
|
||||
|
||||
Reference in New Issue
Block a user