diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/ETag.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/ETag.kt index 7f4bccd..d4920c2 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/ETag.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/ETag.kt @@ -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) } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/Headers.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/Headers.kt index c0f05fe..92df1e0 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/Headers.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/Headers.kt @@ -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) } } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt index 3f5e9ba..7b75572 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt @@ -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" } /** @@ -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" } @@ -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" } @@ -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) @@ -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() + } } }