Skip to content
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ brew install flatbuffers # only for .fbs edits (any flatc version;
is in-process via `proto-gen` (no `protoc`/`protobuf-java`): one record per message with static
`decode(MemorySegment, long, long)` + `encode()` operating directly on a segment.

### Mutation testing

Opt-in [PIT](https://pitest.org) profile in `core` and `reader` (`-P pitest`), bound to the
`verify` phase and scoped to the bounds/parse classes via `<targetClasses>` in each module POM.
Used to harden the security-critical bounds guards (ADR 0003 Phase E).

```bash
./mvnw -pl reader -am -P pitest verify -DskipITs # reader run (-am builds core; -DskipITs skips ITs)
./mvnw -pl core -P pitest verify # core run (IoBounds)
```

Report: `<module>/target/pit-reports/index.html` (+ `mutations.xml` for scripting). Widen a run by
adding `<param>` entries under `<targetClasses>` in the module's `pitest` profile.

Do not invoke the goal directly (`org.pitest:pitest-maven:mutationCoverage`) — it resolves the
latest plugin without the JUnit 5 engine and ignores the profile; always go through `-P pitest`.

Read survivors as a **simplify-first** signal, not only a test-gap signal: an equivalent mutant
often marks a clause that can never change the outcome (dead code) — delete it rather than writing
an unkillable test. Only add a test when the mutated bound is a genuine, independent edge.

### Releasing

```bash
Expand Down
21 changes: 21 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,27 @@
</plugins>
</build>
</profile>

<!-- Mutation testing for the bounds primitive (ADR 0003 Phase E). Opt-in only:
./mvnw -pl core -P pitest verify
Common PIT setup is inherited from the parent `pitest` profile; only the scope differs.
Report: core/target/pit-reports/. -->
<profile>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is possible to define this once in the parent?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b23a853. Parent pitest profile now holds the version, junit5 engine dep, verify-phase execution, and output config under <pluginManagement>; core/reader keep only their <targetClasses>. targetTests dropped — PIT defaults to the test packages matching the targets. Scores unchanged (reader 95%, core 100%).

<id>pitest</id>
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<configuration>
<targetClasses>
<param>io.github.dfa1.vortex.core.IoBounds</param>
</targetClasses>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>

</project>
45 changes: 45 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@
<artifactId>maven-dependency-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.1</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
Expand Down Expand Up @@ -531,6 +536,46 @@
</plugins>
</build>
</profile>
<!-- Mutation testing (opt-in: -P pitest). Common PIT setup lives here once; each module's
own `pitest` profile only adds the plugin to <plugins> and names its <targetClasses>.
targetTests is left to PIT's default (test packages matching the target classes). -->
<profile>
<id>pitest</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.20.0</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>pit-report</id>
<phase>verify</phase>
<goals>
<goal>mutationCoverage</goal>
</goals>
</execution>
</executions>
<configuration>
<outputFormats>
<param>HTML</param>
<param>XML</param>
</outputFormats>
<timestampedReports>false</timestampedReports>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>

</project>
29 changes: 29 additions & 0 deletions reader/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,33 @@
</plugin>
</plugins>
</build>

<profiles>
<!-- Mutation testing pilot (ADR: test-suite strength). Opt-in only:
./mvnw -pl reader -am -P pitest verify -DskipITs
Common PIT setup is inherited from the parent `pitest` profile; only the scope differs:
the file-structure bounds-checking parse classes with dedicated security tests.
Report: reader/target/pit-reports/. -->
<profile>
<id>pitest</id>
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<configuration>
<targetClasses>
<param>io.github.dfa1.vortex.reader.Footer</param>
<param>io.github.dfa1.vortex.reader.Trailer</param>
<param>io.github.dfa1.vortex.reader.PostscriptParser</param>
<param>io.github.dfa1.vortex.reader.SegmentSpec</param>
<param>io.github.dfa1.vortex.reader.Layout</param>
<param>io.github.dfa1.vortex.reader.FlatSegmentDecoder</param>
</targetClasses>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ static void validateSegmentSpecs(List<SegmentSpec> specs, long fileSize) {
SegmentSpec s = specs.get(i);
long offset = s.offset();
long length = s.length();
if (offset < 0 || length < 0 || offset > fileSize || length > fileSize - offset) {
// Overflow-safe containment in [0, fileSize], same shape as IoBounds.checkRange. An
// `offset > fileSize` clause would be redundant: with length >= 0 already guaranteed,
// offset > fileSize forces length > fileSize - offset, so the final clause covers it.
if (offset < 0 || length < 0 || length > fileSize - offset) {
throw new VortexException(
"footer segmentSpecs[" + i + "] out of bounds: offset=" + offset
+ " length=" + length + " fileSize=" + fileSize);
Expand All @@ -87,7 +90,10 @@ static void validateSegmentSpecs(List<SegmentSpec> specs, long fileSize) {
}

private static void checkBlobBounds(String name, long offset, long length, long fileSize) {
if (offset < 0 || length < 0 || offset > fileSize || length > fileSize - offset) {
// Same overflow-safe range form as IoBounds.checkRange (no redundant `offset > fileSize`
// clause: length >= 0 makes it implied by the final comparison). Keeps the blob-named
// message that checkRange's generic text would lose.
if (offset < 0 || length < 0 || length > fileSize - offset) {
throw new VortexException(
"postscript " + name + " blob out of bounds: offset=" + offset
+ " length=" + length + " fileSize=" + fileSize);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.github.dfa1.vortex.reader;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

/// Pins the [Layout] encoding-kind predicates (`isFlat`, `isChunked`, `isStruct`, `isZoned`,
/// `isDict`). [ScanIterator] dispatches layout-tree traversal on these, so a predicate that
/// silently returns a constant would route a whole layout family down the wrong decode path.
/// One layout per encoding id, asserting the matching predicate is `true` and every other is
/// `false` — which fixes each method's return to its `encodingId` rather than a constant.
class LayoutKindTest {

private static Layout layout(String encodingId) {
return new Layout(encodingId, 0L, null, List.of(), List.of());
}

static Stream<Arguments> kinds() {
// (encodingId, isFlat, isChunked, isStruct, isZoned, isDict)
return Stream.of(
Arguments.of(Layout.FLAT, true, false, false, false, false),
Arguments.of(Layout.CHUNKED, false, true, false, false, false),
Arguments.of(Layout.STRUCT, false, false, true, false, false),
Arguments.of(Layout.ZONED, false, false, false, true, false),
Arguments.of(Layout.DICT, false, false, false, false, true));
}

@ParameterizedTest(name = "{0}")
@MethodSource("kinds")
void predicates_matchOnlyOwnEncodingId(
String encodingId, boolean flat, boolean chunked, boolean struct, boolean zoned, boolean dict) {
// Given
Layout sut = layout(encodingId);

// When / Then — exactly one predicate is true, the rest false
assertThat(sut.isFlat()).as("isFlat").isEqualTo(flat);
assertThat(sut.isChunked()).as("isChunked").isEqualTo(chunked);
assertThat(sut.isStruct()).as("isStruct").isEqualTo(struct);
assertThat(sut.isZoned()).as("isZoned").isEqualTo(zoned);
assertThat(sut.isDict()).as("isDict").isEqualTo(dict);
}

@Test
void predicates_allFalse_forUnknownEncodingId() {
// Given — an id matching no known layout kind
Layout sut = layout("vortex.bogus");

// When / Then — no predicate claims it
assertThat(sut.isFlat()).isFalse();
assertThat(sut.isChunked()).isFalse();
assertThat(sut.isStruct()).isFalse();
assertThat(sut.isZoned()).isFalse();
assertThat(sut.isDict()).isFalse();
}
}
Loading