Skip to content

Commit bab885b

Browse files
authored
chore: dedupe SSE optional-space strip and simplify retry/grow helpers (#190)
PR: #190
1 parent 3421dc2 commit bab885b

2 files changed

Lines changed: 32 additions & 33 deletions

File tree

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/sse/ServerSentEventReader.kt

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public class ServerSentEventReader(private val source: BufferedSource) {
7777
// Comment line: latest wins. Per WHATWG SSE §9.2.6 the optional single
7878
// leading space after the `:` is stripped (same rule applied to field values).
7979
if (line[0] == ':') {
80-
comment = line.substring(1).removePrefix(" ")
80+
comment = valueFrom(line, 1)
8181
hasField = true
8282
continue
8383
}
@@ -91,14 +91,7 @@ public class ServerSentEventReader(private val source: BufferedSource) {
9191
rawValue = ""
9292
} else {
9393
field = line.substring(0, colon)
94-
// Per spec: if the value starts with U+0020 SPACE, drop one.
95-
val afterColon = colon + 1
96-
rawValue =
97-
if (afterColon < line.length && line[afterColon] == ' ') {
98-
line.substring(afterColon + 1)
99-
} else {
100-
line.substring(afterColon)
101-
}
94+
rawValue = valueFrom(line, colon + 1)
10295
}
10396

10497
when (field) {
@@ -200,18 +193,31 @@ public class ServerSentEventReader(private val source: BufferedSource) {
200193
* representable range. The spec is silent on overflow; ignoring is conservative.
201194
*/
202195
private fun parseRetryMillis(value: String): Long? {
203-
if (value.isEmpty()) return null
204-
var result = 0L
205-
for (c in value) {
206-
if (c !in '0'..'9') return null
207-
val digit = (c - '0').toLong()
208-
// Detect overflow before it happens so we don't wrap around.
209-
if (result > (Long.MAX_VALUE - digit) / DECIMAL_BASE) return null
210-
result = result * DECIMAL_BASE + digit
211-
}
212-
return result
196+
// Spec allows ASCII digits only; this guard rejects signs and any other
197+
// non-digit up front (toLongOrNull would otherwise accept a leading +/-).
198+
// toLongOrNull then parses base-10 and returns null on empty input or on
199+
// values past Long.MAX_VALUE — the same overflow boundary as the former
200+
// hand-rolled loop.
201+
if (value.any { it !in '0'..'9' }) return null
202+
return value.toLongOrNull()
213203
}
214204

205+
/**
206+
* Returns the value portion of [line] starting at [start], dropping a single
207+
* leading U+0020 SPACE if present. WHATWG SSE §9.2.6 applies this optional
208+
* single-space strip to both comment text and field values, so both code
209+
* paths share this one implementation.
210+
*/
211+
private fun valueFrom(
212+
line: String,
213+
start: Int,
214+
): String =
215+
if (start < line.length && line[start] == ' ') {
216+
line.substring(start + 1)
217+
} else {
218+
line.substring(start)
219+
}
220+
215221
private companion object {
216222
private const val LF: Byte = 0x0A
217223
private const val CR: Byte = 0x0D
@@ -221,9 +227,6 @@ public class ServerSentEventReader(private val source: BufferedSource) {
221227
// Initial capacity for the per-event `data` line accumulator. SSE servers tend to
222228
// emit small numbers of data lines per event (one is typical); 4 covers the long tail.
223229
private const val DATA_ACCUMULATOR_INITIAL_CAP = 4
224-
225-
// Numeric base for `retry:` digit parsing — SSE retry values are unsigned decimals.
226-
private const val DECIMAL_BASE = 10L
227230
}
228231

229232
/**
@@ -237,20 +240,16 @@ public class ServerSentEventReader(private val source: BufferedSource) {
237240
private var count: Int = 0
238241

239242
fun append(b: Byte) {
240-
if (count == bytes.size) grow(count + 1)
243+
if (count == bytes.size) grow()
241244
bytes[count++] = b
242245
}
243246

244247
fun toUtf8(): String = if (count == 0) "" else String(bytes, 0, count, Charsets.UTF_8)
245248

246-
private fun grow(minCapacity: Int) {
247-
val oldCap = bytes.size
248-
val newCap =
249-
when {
250-
oldCap == 0 -> INITIAL_CAP
251-
oldCap < minCapacity -> maxOf(oldCap * 2, minCapacity)
252-
else -> oldCap
253-
}
249+
// Called only when the array is full (count == bytes.size), so the first
250+
// allocation is INITIAL_CAP and every later growth simply doubles.
251+
private fun grow() {
252+
val newCap = if (bytes.isEmpty()) INITIAL_CAP else bytes.size * 2
254253
bytes = bytes.copyOf(newCap)
255254
}
256255

sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/sse/ServerSentEventReaderTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,8 @@ class ServerSentEventReaderTest {
489489

490490
@Test
491491
fun `retry value one above Long MAX VALUE is rejected via overflow guard`() {
492-
// 9223372036854775808 — Long.MAX_VALUE + 1, exercises the
493-
// `result > (Long.MAX_VALUE - digit) / 10` branch in parseRetryMillis.
492+
// 9223372036854775808 — Long.MAX_VALUE + 1; one past the representable
493+
// boundary, so parseRetryMillis rejects it rather than wrapping.
494494
val src = source("retry: 9223372036854775808\ndata: x\n\n")
495495
val event = ServerSentEventReader(src).next()
496496
assertNull(event?.retry, "value exceeding Long.MAX_VALUE must be rejected")

0 commit comments

Comments
 (0)