Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ public value class ETag private constructor(private val raw: String) {
trimmed.length >= WEAK_FORM_MIN_LEN &&
trimmed.startsWith("W/\"") &&
trimmed.endsWith("\"")
// A strong-form value must open with `"`, which a `W/`-prefixed weak form never
// does, so no explicit weak-prefix exclusion is needed here.
val isStrongForm =
trimmed.length >= 2 &&
!trimmed.startsWith("W/") && trimmed.startsWith("\"") && trimmed.endsWith("\"")
trimmed.startsWith("\"") && trimmed.endsWith("\"")
require(isWeakForm || isStrongForm) { "malformed ETag: $raw" }
return ETag(trimmed)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,10 @@ public data class Headers private constructor(
*/
public fun addAll(headers: Headers): Builder =
apply {
headers.entries().forEach { (key, values) ->
// Read the backing map directly (as the copy constructor above does) rather
// than entries(), whose defensively-copied snapshot would be discarded after
// this single iteration. Keys are already canonical, so merging is unchanged.
headers.headersMap.forEach { (key, values) ->
headersMap.computeIfAbsent(key) { mutableListOf() }.addAll(values)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public data class MediaType private constructor(
parameters.entries.joinToString(separator = ";") { (key, value) ->
"$key=${formatParameterValue(value)}"
}
return if (formattedParams.isEmpty()) "$type/$subtype" else "$type/$subtype;$formattedParams"
return if (formattedParams.isEmpty()) fullType else "$fullType;$formattedParams"
}

/**
Expand Down Expand Up @@ -142,7 +142,7 @@ public data class MediaType private constructor(
): MediaType {
require(type.isNotBlank()) { "Type must not be blank" }
require(subtype.isNotBlank()) { "Subtype must not be blank" }
require(!(type == "*" && subtype != "*")) {
require(type != "*" || subtype == "*") {
"Invalid media type format: type=$type, subtype=$subtype"
}

Expand Down Expand Up @@ -179,7 +179,7 @@ public data class MediaType private constructor(
val type = mimeString.substring(0, slashIndex).trim().lowercase(Locale.US)
val subtype = mimeString.substring(slashIndex + 1).trim().lowercase(Locale.US)

require(!(type == "*" && subtype != "*")) {
require(type != "*" || subtype == "*") {
"Invalid media type format: $mediaType"
}

Expand All @@ -201,27 +201,9 @@ public data class MediaType private constructor(
}
// RFC 7230 §3.2.6: strip surrounding double-quotes and unescape
// quoted-pair sequences (`\"` → `"`, `\\` → `\`).
val value =
if (rawValue.startsWith("\"") && rawValue.endsWith("\"") && rawValue.length >= 2) {
val inner = rawValue.substring(1, rawValue.length - 1)
val sb = StringBuilder(inner.length)
var i = 0
while (i < inner.length) {
if (inner[i] == '\\' && i + 1 < inner.length) {
sb.append(inner[i + 1])
i += 2
} else {
sb.append(inner[i])
i++
}
}
sb.toString()
} else {
rawValue
}
// Keys are lower-cased for case-insensitive lookup; values are preserved
// because boundaries, base64 tokens, etc. are case-sensitive.
key.lowercase(Locale.US) to value
key.lowercase(Locale.US) to unescapeQuotedValue(rawValue)
}

return MediaType(type, subtype, parametersMap)
Expand Down Expand Up @@ -265,5 +247,29 @@ public data class MediaType private constructor(
result.add(current.toString())
return result
}

/**
* Strips the surrounding double-quotes from a parameter [rawValue] and unescapes its
* quoted-pair sequences (`\"` → `"`, `\\` → `\`), per RFC 7230 §3.2.6. A value that is
* not a `quoted-string` is returned unchanged. Inverse of [formatParameterValue].
*/
private fun unescapeQuotedValue(rawValue: String): String {
if (!(rawValue.startsWith("\"") && rawValue.endsWith("\"") && rawValue.length >= 2)) {
return rawValue
}
val inner = rawValue.substring(1, rawValue.length - 1)
val sb = StringBuilder(inner.length)
var i = 0
while (i < inner.length) {
if (inner[i] == '\\' && i + 1 < inner.length) {
sb.append(inner[i + 1])
i += 2
} else {
sb.append(inner[i])
i++
}
}
return sb.toString()
}
}
}
Loading