This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/3.7-dev by this push:
     new 1f0ded919d CTR: Guard against non-terminating loops in TinkerGraph 
GraphSON deserialization
1f0ded919d is described below

commit 1f0ded919d0236123110d02e23f63b4bcb356e1e
Author: Cole Greer <[email protected]>
AuthorDate: Thu Aug 20 11:23:48 2026 -0700

    CTR: Guard against non-terminating loops in TinkerGraph GraphSON 
deserialization
    
    Assisted-by: Kiro:claude-opus-5
---
 .beads/issues.jsonl                                |  11 +++
 CHANGELOG.asciidoc                                 |   1 +
 .../tinkergraph/structure/TinkerIoRegistryV2.java  |  27 +++++-
 .../tinkergraph/structure/TinkerIoRegistryV3.java  |  27 +++++-
 .../TinkerGraphGraphSONSerializerV2Test.java       |  36 +++++++
 .../TinkerGraphGraphSONSerializerV3Test.java       | 107 +++++++++++++++++++++
 6 files changed, 203 insertions(+), 6 deletions(-)

diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
new file mode 100644
index 0000000000..2fda10a578
--- /dev/null
+++ b/.beads/issues.jsonl
@@ -0,0 +1,11 @@
+{"_type":"issue","id":"tinkerpop-usp.2","title":"Phase 1: Scaffold 
workflow_dispatch skeleton + core Java build","description":"Create 
.github/workflows/release-preflight.yml — the minimal \"Run workflow\" button 
that builds the Java core and produces a summary.\n\n**Files:** 
.github/workflows/release-preflight.yml (create)\n\n**Context:**\n- Model on 
build-test.yml triggers/setup. workflow_dispatch with inputs appears as a \"Run 
workflow\" button.\n- Core build mirrors doc step 5a: mvn  [...]
+{"_type":"issue","id":"tinkerpop-usp.1","title":"Phase 0: Prerequisite — 
remove mono from .NET packaging (TINKERPOP-2085)","description":"Remove the 
mono dependency from the .NET template packaging step so the pre-flight 
dotnet-template job runs cleanly on GitHub-hosted runners without 
mono.\n\n**Files:** gremlin-dotnet/src/pom.xml (edit), docker/Dockerfile 
(edit), docs/src/dev/developer/release.asciidoc (edit if commands 
change)\n\n**Context:**\n- The only mono usage is the Ant exec at  [...]
+{"_type":"issue","id":"tinkerpop-usp.6","title":"Phase 5: License/NOTICE (RAT) 
+ javadoc generation + upgrade-prompt existence (doc steps 5f, 5g, 
6)","description":"Add a static-checks job for license-header validation (RAT), 
javadoc build verification, and upgrade-prompt existence check.\n\n**Files:** 
.github/workflows/release-preflight.yml (edit)\n\n**Context:**\n- 
apache-rat-plugin configured at pom.xml:463 — mvn apache-rat:check flags 
missing/incorrect license headers (partial covera [...]
+{"_type":"issue","id":"tinkerpop-usp","title":"Release Pre-Flight Check 
Automation","description":"Encode the entire pre-flight check from 
docs/src/dev/developer/release.asciidoc into a single workflow_dispatch GitHub 
Actions button at .github/workflows/release-preflight.yml. Heavy 
non-side-effecting verification (integration tests, docs generation, dependency 
reports) runs on every press. Only genuinely side-effecting steps (SNAPSHOT 
deploy to Apache Nexus, docs publish to SVN) are gate [...]
+{"_type":"issue","id":"tinkerpop-usp.10","title":"Phase 9: Aggregate 
pre-flight report + manual checklist","description":"Complete the summary job 
(stubbed in Phase 1) so a single run produces a clear pass/fail dashboard, 
links the Phase 8 human-assist artifacts, and lists the residual 
non-automatable checklist.\n\n**Files:** 
.github/workflows/release-preflight.yml (edit)\n\n**Context:**\n- The summary 
job (needs: every job including Phase 8 jobs, if: always()) reads each job 
result via  [...]
+{"_type":"issue","id":"tinkerpop-usp.4","title":"Phase 3: Docker-image build + 
Gremlin.Net.Template packaging (doc steps 5c, 5d)","description":"Add jobs to 
build Docker images (gremlin-server, gremlin-console) and validate 
Gremlin.Net.Template .nupkg packaging — both non-side-effecting (no 
push/publish at install phase).\n\n**Files:** 
.github/workflows/release-preflight.yml (edit)\n\n**Context:**\n- Step 5c: mvn 
clean install -pl gremlin-server,gremlin-console -DdockerImages. The docker 
[...]
+{"_type":"issue","id":"tinkerpop-usp.3","title":"Phase 2: Dependency-update 
report (doc step 3)","description":"Add a dependency-report job that runs on 
every press (non-gated, non-failing) and surfaces available minor/patch updates 
across all ecosystem toolchains.\n\n**Files:** 
.github/workflows/release-preflight.yml (edit)\n\n**Context:**\n- Doc step 3 
lists four tools: mvn versions:display-dependency-updates 
-DallowMajorUpdates=false, go list -m -u all, npm-check-updates, dotnet-check 
[...]
+{"_type":"issue","id":"tinkerpop-usp.9","title":"Phase 8: Human-assist — 
decision-support data + pre-populated email drafts (doc steps 2, 4, 5f, 5h, 
6)","description":"Add jobs that gather decision-support data and pre-populate 
email drafts for the human/comms steps that cannot be fully 
automated.\n\n**Files:** .github/workflows/release-preflight.yml (edit); 
optionally bin/preflight-emails.sh (new helper) and email-draft 
templates\n\n**Context:**\nTwo kinds of assist:\n1. Decision-suppor [...]
+{"_type":"issue","id":"tinkerpop-usp.7","title":"Phase 6: Documentation 
generation validation (doc step 7, non-publishing) — heavy, runs every 
press","description":"Add a docs-validation job that runs bin/process-docs.sh 
(generation only, no SVN publish). Non-side-effecting, runs on every press per 
the exhaustive pre-flight rationale.\n\n**Files:** 
.github/workflows/release-preflight.yml (edit)\n\n**Context:**\n- Step 7 
ultimately runs bin/publish-docs.sh (publishes to SVN). The validati [...]
+{"_type":"issue","id":"tinkerpop-usp.5","title":"Phase 4: Full integration 
test suite (doc step 5b) — heavy, runs every press","description":"Add the full 
dockerized integration test suite job. Runs on every press (not gated) per the 
exhaustive pre-flight rationale — pre-flight runs rarely and every verification 
step is necessary.\n\n**Files:** .github/workflows/release-preflight.yml 
(edit)\n\n**Context:**\n- Step 5b: docker/build.sh -t -i. Builds base images 
and runs the full integratio [...]
+{"_type":"issue","id":"tinkerpop-usp.8","title":"Phase 7: Side-effecting 
stages — SNAPSHOT deploy + docs publish (gated + secrets) 
[HIGH-RISK]","description":"⚠️ HIGH-RISK / SIDE-EFFECTING. These push to shared 
Apache infrastructure. Default OFF. Require secrets and explicit input 
toggles.\n\nAdd gated jobs for SNAPSHOT deploy (5e) and docs publish (7). 
Follow Log4j secret-injection pattern (setup-java 
server-id/server-username/server-password, gpg-private-key; svn 
non-interactive).\n\n* [...]
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 1505c29d7f..b6865fa065 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -29,6 +29,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Disabled unsafe Java deserialization on the Gryo IO paths and added 
`GryoMapper.Builder.javaSerializationAllowed(boolean)` to control it.
 * Added verification to `io()` ensuring a class named by `IO.reader`, 
`IO.writer` or `IO.registry` implements the expected interface before loading 
and initializing it.
 * Fixed `io().write()` to resolve its `GraphWriter` before opening the output 
file, so that a writer which cannot be constructed no longer truncates the 
target.
+* Fixed `TinkerGraph` GraphSON deserialization to fail fast with 
`JsonParseException` on malformed `tinker:graph` input.
 * Fixed `subgraph()` to throw a descriptive error identifying the required 
`Edge` input instead of an internal `ClassCastException` when the traversal 
produces a non-edge value.
 * Fixed `where(P)` to throw a descriptive error identifying the required 
String scope key (and suggesting `is(P)` for value comparisons) instead of an 
internal `ClassCastException` when given a non-String predicate value.
 * Fixed `PeerPressure.property_name` in `gremlin-python` incorrectly mapping 
to the `pageRank` property name token.
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV2.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV2.java
index 9b97e7aa75..dbe1874abb 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV2.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV2.java
@@ -36,6 +36,7 @@ import org.apache.tinkerpop.gremlin.structure.util.Attachable;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
 import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator;
+import org.apache.tinkerpop.shaded.jackson.core.JsonParseException;
 import org.apache.tinkerpop.shaded.jackson.core.JsonParser;
 import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException;
 import org.apache.tinkerpop.shaded.jackson.core.JsonToken;
@@ -192,16 +193,20 @@ public final class TinkerIoRegistryV2 extends 
AbstractIoRegistry {
             
conf.setProperty("gremlin.tinkergraph.defaultVertexPropertyCardinality", 
"list");
             final TinkerGraph graph = TinkerGraph.open(conf);
 
-            while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+            while (nextTokenOrThrow(jsonParser) != JsonToken.END_OBJECT) {
                 if (jsonParser.getCurrentName().equals("vertices")) {
-                    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
+                    if (nextTokenOrThrow(jsonParser) != JsonToken.START_ARRAY)
+                        throw new JsonParseException(jsonParser, "Expected an 
array value for the \"vertices\" field");
+                    while (nextTokenOrThrow(jsonParser) != 
JsonToken.END_ARRAY) {
                         if (jsonParser.currentToken() == 
JsonToken.START_OBJECT) {
                             final DetachedVertex v = (DetachedVertex) 
deserializationContext.readValue(jsonParser, Vertex.class);
                             v.attach(Attachable.Method.getOrCreate(graph));
                         }
                     }
                 } else if (jsonParser.getCurrentName().equals("edges")) {
-                    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
+                    if (nextTokenOrThrow(jsonParser) != JsonToken.START_ARRAY)
+                        throw new JsonParseException(jsonParser, "Expected an 
array value for the \"edges\" field");
+                    while (nextTokenOrThrow(jsonParser) != 
JsonToken.END_ARRAY) {
                         if (jsonParser.currentToken() == 
JsonToken.START_OBJECT) {
                             final DetachedEdge e = (DetachedEdge) 
deserializationContext.readValue(jsonParser, Edge.class);
                             e.attach(Attachable.Method.getOrCreate(graph));
@@ -213,6 +218,22 @@ public final class TinkerIoRegistryV2 extends 
AbstractIoRegistry {
             return graph;
         }
 
+        /**
+         * Advances the parser one token, treating end-of-input as a parse 
error.  Once the
+         * underlying input is exhausted, {@code JsonParser.nextToken()} 
returns {@code null} on
+         * every subsequent call rather than throwing.  A loop whose only exit 
condition compares
+         * the result against a structural close token (e.g. {@code 
END_ARRAY}) would therefore
+         * never terminate on truncated or malformed input.  This wrapper 
converts the {@code null}
+         * return into a {@code JsonParseException} so that callers can use a 
simple while-loop
+         * idiom without risk of non-termination.
+         */
+        private static JsonToken nextTokenOrThrow(final JsonParser jsonParser) 
throws IOException {
+            final JsonToken token = jsonParser.nextToken();
+            if (null == token)
+                throw new JsonParseException(jsonParser, "Unexpected 
end-of-input while reading a TinkerGraph");
+            return token;
+        }
+
         @Override
         public boolean isCachable() {
             return true;
diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV3.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV3.java
index 7783a5c0fc..6252f1c867 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV3.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerIoRegistryV3.java
@@ -36,6 +36,7 @@ import org.apache.tinkerpop.gremlin.structure.util.Attachable;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
 import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator;
+import org.apache.tinkerpop.shaded.jackson.core.JsonParseException;
 import org.apache.tinkerpop.shaded.jackson.core.JsonParser;
 import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException;
 import org.apache.tinkerpop.shaded.jackson.core.JsonToken;
@@ -192,16 +193,20 @@ public final class TinkerIoRegistryV3 extends 
AbstractIoRegistry {
             
conf.setProperty("gremlin.tinkergraph.defaultVertexPropertyCardinality", 
"list");
             final TinkerGraph graph = TinkerGraph.open(conf);
 
-            while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+            while (nextTokenOrThrow(jsonParser) != JsonToken.END_OBJECT) {
                 if (jsonParser.getCurrentName().equals("vertices")) {
-                    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
+                    if (nextTokenOrThrow(jsonParser) != JsonToken.START_ARRAY)
+                        throw new JsonParseException(jsonParser, "Expected an 
array value for the \"vertices\" field");
+                    while (nextTokenOrThrow(jsonParser) != 
JsonToken.END_ARRAY) {
                         if (jsonParser.currentToken() == 
JsonToken.START_OBJECT) {
                             final DetachedVertex v = (DetachedVertex) 
deserializationContext.readValue(jsonParser, Vertex.class);
                             v.attach(Attachable.Method.getOrCreate(graph));
                         }
                     }
                 } else if (jsonParser.getCurrentName().equals("edges")) {
-                    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
+                    if (nextTokenOrThrow(jsonParser) != JsonToken.START_ARRAY)
+                        throw new JsonParseException(jsonParser, "Expected an 
array value for the \"edges\" field");
+                    while (nextTokenOrThrow(jsonParser) != 
JsonToken.END_ARRAY) {
                         if (jsonParser.currentToken() == 
JsonToken.START_OBJECT) {
                             final DetachedEdge e = (DetachedEdge) 
deserializationContext.readValue(jsonParser, Edge.class);
                             e.attach(Attachable.Method.getOrCreate(graph));
@@ -212,5 +217,21 @@ public final class TinkerIoRegistryV3 extends 
AbstractIoRegistry {
 
             return graph;
         }
+
+        /**
+         * Advances the parser one token, treating end-of-input as a parse 
error.  Once the
+         * underlying input is exhausted, {@code JsonParser.nextToken()} 
returns {@code null} on
+         * every subsequent call rather than throwing.  A loop whose only exit 
condition compares
+         * the result against a structural close token (e.g. {@code 
END_ARRAY}) would therefore
+         * never terminate on truncated or malformed input.  This wrapper 
converts the {@code null}
+         * return into a {@code JsonParseException} so that callers can use a 
simple while-loop
+         * idiom without risk of non-termination.
+         */
+        private static JsonToken nextTokenOrThrow(final JsonParser jsonParser) 
throws IOException {
+            final JsonToken token = jsonParser.nextToken();
+            if (null == token)
+                throw new JsonParseException(jsonParser, "Unexpected 
end-of-input while reading a TinkerGraph");
+            return token;
+        }
     }
 }
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV2Test.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV2Test.java
index f65e8b70ee..b795764f20 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV2Test.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV2Test.java
@@ -338,6 +338,42 @@ public class TinkerGraphGraphSONSerializerV2Test {
         }
     }
 
+    @Test(timeout = 5000)
+    public void shouldFailFastOnScalarVerticesField() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":0}}";
+        final GraphReader reader = getReader(defaultMapperV2);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for malformed tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — the START_ARRAY check threw as intended
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void shouldFailFastOnScalarEdgesField() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":[],\"edges\":0}}";
+        final GraphReader reader = getReader(defaultMapperV2);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for malformed tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — the START_ARRAY check threw as intended
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void shouldFailFastOnTruncatedInput() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":[";
+        final GraphReader reader = getReader(defaultMapperV2);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for truncated tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — nextTokenOrThrow detected end-of-input
+        }
+    }
+
     @Test
     public void deserializersTestsProperty() {
         final TinkerGraph tg = TinkerGraph.open();
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV3Test.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV3Test.java
new file mode 100644
index 0000000000..727985ee8f
--- /dev/null
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/TinkerGraphGraphSONSerializerV3Test.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.tinkergraph.structure;
+
+import org.apache.tinkerpop.gremlin.structure.io.GraphReader;
+import org.apache.tinkerpop.gremlin.structure.io.GraphWriter;
+import org.apache.tinkerpop.gremlin.structure.io.Mapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONReader;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONXModuleV3;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Serialization and deserialization tests for GraphSON V3 with the 
TinkerGraph custom type.
+ */
+public class TinkerGraphGraphSONSerializerV3Test {
+
+    private final Mapper defaultMapperV3 = GraphSONMapper.build()
+            .version(GraphSONVersion.V3_0)
+            .addCustomModule(GraphSONXModuleV3.build())
+            .addRegistry(TinkerIoRegistryV3.instance())
+            .create();
+
+    @Test
+    public void shouldDeserializeWellFormedGraph() throws IOException {
+        final TinkerGraph original = TinkerFactory.createModern();
+        final GraphWriter writer = getWriter(defaultMapperV3);
+        final GraphReader reader = getReader(defaultMapperV3);
+
+        try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            writer.writeObject(out, original);
+            final String json = out.toString();
+            final TinkerGraph read = reader.readObject(new 
ByteArrayInputStream(json.getBytes()), TinkerGraph.class);
+            assertEquals(6L, read.traversal().V().count().next().longValue());
+            assertEquals(6L, read.traversal().E().count().next().longValue());
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void shouldFailFastOnScalarVerticesField() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":0}}";
+        final GraphReader reader = getReader(defaultMapperV3);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for malformed tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — the START_ARRAY check threw as intended
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void shouldFailFastOnScalarEdgesField() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":[],\"edges\":0}}";
+        final GraphReader reader = getReader(defaultMapperV3);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for malformed tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — the START_ARRAY check threw as intended
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void shouldFailFastOnTruncatedInput() throws IOException {
+        final String malformed = 
"{\"@type\":\"tinker:graph\",\"@value\":{\"vertices\":[";
+        final GraphReader reader = getReader(defaultMapperV3);
+        try {
+            reader.readObject(new ByteArrayInputStream(malformed.getBytes()), 
TinkerGraph.class);
+            fail("Expected IOException for truncated tinker:graph input");
+        } catch (IOException expected) {
+            // JsonParseException — nextTokenOrThrow detected end-of-input
+        }
+    }
+
+    private GraphWriter getWriter(Mapper paramMapper) {
+        return GraphSONWriter.build().mapper(paramMapper).create();
+    }
+
+    private GraphReader getReader(Mapper paramMapper) {
+        return GraphSONReader.build().mapper(paramMapper).create();
+    }
+}

Reply via email to