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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ Entries land here as they merge.

### Bug fixes

- **SVG path reader no longer hangs on malformed `d` data.** A `Z`/`z`
close command (which consumes no operands) followed by a stray
non-command token — e.g. `"M0 0 Z5"` — made the scanner loop forever,
appending a close op every pass until the heap was exhausted. A single
malformed or hostile path string could therefore DoS the `@Beta`
`SvgPath.parse` / `SvgIcon` reader. The scanner now fails fast with the
usual position-carrying `IllegalArgumentException` when an iteration
consumes neither a command nor an operand.
- **`BEHIND_CONTENT` watermarks no longer wash out the page.** The PDF
watermark renderer set its low-opacity graphics state in a *prepended*
content stream without a save/restore pair; PDFBox's `resetContext` only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ List<double[]> parse() {
break;
}
char c = d.charAt(pos);
int loopStart = pos;
if (isCommand(c)) {
cmd = c;
pos++;
Expand All @@ -68,6 +69,15 @@ List<double[]> parse() {
}
}
apply(cmd);
if (pos == loopStart) {
// No command letter and no operand was consumed this iteration.
// This only happens when an operand-less command (Z/z) is
// implicitly repeated by a stray non-command token, e.g.
// "M0 0 Z5": apply(Z) consumes nothing, so without this guard
// the loop would spin forever, appending a close op each pass
// until the heap is exhausted. Fail loudly instead of hanging.
throw fail("a command letter");
}
}
return ops;
}
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/com/demcha/compose/document/svg/SvgPathTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.demcha.compose.document.style.DocumentPathSegment.CubicTo;
import com.demcha.compose.document.style.DocumentPathSegment.LineTo;
import com.demcha.compose.document.style.DocumentPathSegment.MoveTo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -197,4 +199,22 @@ void syntaxErrorsCarryThePosition() {
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("viewBox");
}

@Test
void strayTokenAfterCloseIsRejectedAndDoesNotHang() {
// Regression: an operand-less Z/z followed by a non-command token used
// to spin forever (the close op consumes no characters, so the scanner
// never advanced), appending a close op per iteration until OOM. A
// single malformed/hostile 'd' string would DoS the @Beta reader.
// Each call must fail fast; the assertTimeout pins that it cannot hang.
Assertions.assertTimeoutPreemptively(
Duration.ofSeconds(2), () -> {
assertThatThrownBy(() -> SvgPath.parse("M0 0 Z5"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("position");
assertThatThrownBy(() -> SvgPath.parse("M0 0 z9 9"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("position");
});
}
}