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

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

commit b907ebbed0fdf09b62131ba0348b3f3362c9be55
Merge: 9b75c7ddb7 3ba8ec0e7e
Author: Cole Greer <[email protected]>
AuthorDate: Wed Jul 29 15:39:56 2026 -0700

    Merge branch '3.8-dev'

 CHANGELOG.asciidoc                                 |   1 +
 THREAT_MODEL.md                                    |  22 +-
 docs/src/upgrade/release-3.7.x.asciidoc            |  43 +++
 .../process/traversal/step/sideEffect/IoStep.java  |   6 +-
 .../gremlin/structure/io/gryo/GryoIo.java          |   4 +-
 .../gremlin/structure/io/gryo/GryoMapper.java      |  52 ++-
 .../gremlin/structure/io/gryo/GryoReader.java      |   5 +-
 .../gremlin/structure/io/gryo/GryoWriter.java      |   5 +-
 .../gremlin/structure/io/gryo/GryoMapperTest.java  | 353 +++++++++++++++++++++
 .../gremlin/hadoop/structure/io/HadoopPools.java   |   2 +
 .../hadoop/structure/io/gryo/GryoRecordReader.java |   4 +-
 .../hadoop/structure/io/gryo/GryoRecordWriter.java |   4 +-
 .../io/gryo/GryoRecordReaderWriterTest.java        |  88 +++++
 13 files changed, 572 insertions(+), 17 deletions(-)

diff --cc THREAT_MODEL.md
index 6c6530318f,c5ca1a3f7d..9becade050
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@@ -255,12 -243,13 +255,12 @@@ Per-surface trust table
  
  | Surface | Input | Attacker-controllable? | Caller/operator must enforce |
  | --- | --- | --- | --- |
 -| Gremlin Server — string script request | Groovy/Gremlin script text | 
**yes** (pre-auth if auth off) | auth; script restriction / sandbox / 
bytecode-only; who may script |
 -| Gremlin Server — bytecode/traversal request | serialized traversal bytecode 
| **yes**, within privileges | auth; traversal-step allow-list; resource limits 
|
 -| Gremlin Server — session id (`SessionOpProcessor`) | client-supplied 
session string | **yes** | keyed by the string with no owning-user check, so 
any client presenting the id shares the session (see §11b) |
 -| Request deserialization (GraphSON / GraphBinary) | serialized bytes | 
**yes** (pre-auth) | robustness of the wire serializers |
 +| Gremlin Server — script request | Gremlin (or, if `gremlin-groovy` is 
enabled, Groovy) script text | **yes** (pre-auth if auth off) | auth; script 
restriction / sandbox; who may script; traversal-step allow-list; resource 
limits |
 +| Gremlin Server — transaction id | server-generated UUID, echoed back by the 
client on follow-up requests | **yes** (a client can present any id) | looked 
up with no owning-user check, so any client presenting a valid id shares that 
transaction (see §11b) |
 +| Request deserialization (GraphSON / GraphBinary) | serialized bytes | 
**yes** (pre-auth only when auth is off, see the ordering note below) | 
robustness of the wire serializers |
- | Graph IO — Gryo/GraphSON/GraphML files (`io()` step, persistence, OLAP) | 
on-disk / cluster bytes | only if the caller loads untrusted files | GraphSON, 
locked Gryo (`registrationRequired=true`), and GraphML with the default XML 
factory owe deserializer integrity. Unlocked Gryo and a caller-supplied 
unhardened `XMLInputFactory` (XXE) are the caller's responsibility |
+ | Graph IO — Gryo/GraphSON/GraphML files (`io()` step, persistence, OLAP) | 
on-disk / cluster bytes | only if the caller loads untrusted files | GraphSON, 
the hardened Gryo mappers the IO paths build (`registrationRequired=true` plus 
`javaSerializationAllowed=false`), and GraphML with the default XML factory owe 
deserializer integrity. Unlocked Gryo, a Gryo mapper that keeps Java 
serialization, and a caller-supplied unhardened `XMLInputFactory` (XXE) are the 
caller's responsibility |
  | Gremlin string parser (`gremlin-language` ANTLR) | Gremlin string | **yes** 
| parser robustness, no crash/hang/OOM on malformed input and no grammar 
breakout / step injection (distinct from execution cost, §8/Q7) |
 -| Any string the grammar accepts as an argument (e.g. a `regex` pattern) | 
Gremlin string / bytecode | **yes** | a grammatically valid string must not 
enable DoS (e.g. ReDoS via a pathological pattern), the Q7 
super-linear-amplification carve-out (§8) |
 +| Any string the grammar accepts as an argument (e.g. a `regex` pattern) | 
Gremlin string | **yes** | a grammatically valid string must not enable DoS 
(e.g. ReDoS via a pathological pattern), the Q7 super-linear-amplification 
carve-out (§8) |
  | GLV (client) — server response | serialized bytes from the server | yes if 
the server is malicious/compromised, or a MITM (TLS off / cert not validated) | 
response-deserialization robustness; TLS with cert validation |
  | `gremlin-server.yaml`, host, data dir | local | no — operator-trusted | 
filesystem permissions |
  
@@@ -351,29 -334,22 +351,32 @@@
    security-critical.
  - **Parser integrity (`gremlin-language`).** A crafted Gremlin string cannot 
break out of a string literal
    to inject additional traversal steps. This is the safe string-to-traversal 
path, distinct from building a
 -  Groovy string by concatenation, which is the calling application's concern 
(§9). *Violation symptom:*
 +  Groovy string by concatenation, which is the calling application's concern 
(§9). The same holds for the
 +  GQL string of a `match(String)` request, parsed server-side by the 
`gql-gremlin` grammar. That grammar is
 +  reference code active on the default distribution (TinkerGraph registers 
it), so its robustness (no
 +  crash/hang/OOM on malformed GQL) and integrity (no breakout into the 
enclosing traversal) are in-model. A provider that does
 +  not register a declarative-match strategy leaves the step non-executable 
(§3). *Violation symptom:*
    grammar breakout / step injection from a value that should stay a literal. 
*Severity:* critical.
   
- - **Deserializer integrity.** The wire deserializer (GraphBinary), GraphSON 
and **Gryo in its locked default
-   (`registrationRequired=true`)** reading attacker bytes do not lead to 
arbitrary object instantiation / code
-   execution beyond the registered type set. Because `inject()` and value 
arguments let a request carry any
-   supported type, a bug in a **registered** type's (de)serializer that 
crashes/OOMs the reader is also
 -- **Deserializer integrity.** The wire deserializers (GraphSON, GraphBinary) 
and **the hardened Gryo mappers the
++- **Deserializer integrity.** The wire deserializer (GraphBinary), GraphSON 
and **the hardened Gryo mappers the
+   IO paths build** (`registrationRequired=true` plus 
`javaSerializationAllowed=false`, i.e. `io()`, `GryoReader`,
 -  `GryoWriter`, `GryoIo`, and the Hadoop Gryo input/output formats) reading 
attacker bytes do not reach native Java
 -  deserialization
 -  (`ObjectInputStream.readObject()`). Because `inject()` and value arguments 
let a request carry any
 -  supported type, a bug in a **registered** type's (de)serializer that 
crashes/OOMs the reader is also
++  `GryoWriter`, `GryoIo`, and the Hadoop Gryo input/output formats) reading 
attacker bytes do not lead to arbitrary
++  object instantiation / code execution beyond the registered type set. 
Because `inject()` and value arguments let a
++  request carry any supported type, a bug in a **registered** type's 
(de)serializer that crashes/OOMs the reader is also
    in-model, on **both** the server (request) and the GLV (response) side. The 
GraphML reader disables
    external entities and DTDs by default (XXE-safe). *Violation symptom:* 
deserialization gadget / RCE / XXE,
    or a registered-type serializer crashing/OOMing either end. *Severity:* 
critical. Gryo is not on the wire,
-   and unlocked Gryo or a caller-supplied unhardened XML factory is 
out-of-model (user responsibility, §9).
+   and unlocked Gryo (as run by `spark-gremlin` and the Hadoop object pools), 
a directly built `GryoMapper` or
+   `GryoPool` that keeps Java serialization, or a caller-supplied unhardened 
XML factory is out-of-model
+   (user responsibility, §9).
 - 
 +- **Provider Defined Type (PDT) hydration.** Wire (de)serialization of the 
PDT holder is a registered type
 +  like any other (TinkerPop, above). Turning it into a typed object via a 
`PDTRegistry` adapter is **opt-in,
 +  off by default** (added by the operator server-side, the user client-side). 
The registered types and their
 +  constructor/`fromFields` side effects are **that party's** responsibility 
(§3). The hydration **mechanism**
 +  is TinkerPop's: it must stay **inert with no registry configured** (no 
request triggers hydration when
 +  nothing is registered) and, when configured, **instantiate only registered 
types** (an unknown name stays
 +  a data holder, not a gadget) without crash/OOM/hang. *Severity:* critical. 
*(documented — `PDTRegistry`,
 +  `@ProviderDefined`)*
  - **Resource bounds — split.** Malformed/pre-auth input that 
crashes/OOMs/hangs the server is **in-model**
    (above). Ordinary expensive traversals / large results are **operator 
capacity**, NOT in-model, unless a
    specific bug applies (super-linear amplification, a missing-where-expected 
limit, an unbounded traversal).
@@@ -422,15 -398,20 +425,20 @@@
    allow-list (`registrationRequired=true`), so it is not an 
arbitrary-instantiation sink, and a break within
    that locked config is a `VALID` bug like any deserializer. **Running Gryo 
unlocked
    (`registrationRequired=false`) is not a safe boundary against untrusted 
bytes and is the user's
-   responsibility.** (A few registered types use Java native serialization, a 
gadget caveat even when locked.)
+   responsibility.** A few registered types are also serialized with Kryo's 
`JavaSerializer`, which reads by way of
+   `ObjectInputStream.readObject()` and is a gadget caveat even when locked. 
The mappers the IO paths build
+   (`io()`, `GryoReader`, `GryoWriter`, `GryoIo`, and the Hadoop Gryo 
input/output formats) drop those registrations
+   (`GryoMapper.Builder.javaSerializationAllowed(boolean)` selects the 
behavior), so a break there is `VALID`. A
+   directly built `GryoMapper` and `GryoPool` keep them, as do the 
`spark-gremlin` and Hadoop object pools that
+   additionally run unlocked; those remain the user's responsibility.
    Gryo is not on the wire, so this is an IO/file-surface concern (`io()` 
step, persistence, OLAP).
   
 -- **A `TraversalStrategy` is not an access-control boundary on its own.** A 
remote request can remove or
 -  replace strategies on its traversal source, whether by bytecode source 
instruction
 -  (`withoutStrategies()`) or by script, so a strategy applied by the operator 
only restricts a client
 -  when an `Authorizer` denies strategy removal/modification, as the reference 
documentation states.
 -  Strategies also act at the traversal layer, not the storage layer (§5a). 
*(documented — `Bytecode`
 -  source instructions; `gremlin-applications.asciidoc` "Authorization")*
 +- **A `TraversalStrategy` is not an access-control boundary on its own.** The 
Gremlin language lets any
 +  request remove or replace strategies on its traversal source 
(`withoutStrategies()`,
 +  `withStrategies()`), so a strategy applied by the operator only restricts a 
client when an `Authorizer`
 +  denies strategy removal/modification, as the reference documentation 
states. Strategies also act at the
 +  traversal layer, not the storage layer (§5a). *(documented — Gremlin 
grammar `withoutStrategies`;
 +  `gremlin-applications.asciidoc` "Authorization")*
  - **Ordinary resource exhaustion is not a defended property.** Expensive 
traversals / large results that
    consume CPU/memory are an operator capacity concern unless a specific bug 
applies (§8).
  - **No defense against a malicious operator / host.**
diff --cc 
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
index c391bfa2e5,88384e8643..cd8e252686
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
@@@ -19,11 -19,13 +19,13 @@@
  package org.apache.tinkerpop.gremlin.structure.io.gryo;
  
  import 
org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser;
 -import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
 +import org.apache.tinkerpop.gremlin.process.traversal.GremlinLang;
  import org.apache.tinkerpop.gremlin.process.traversal.Merge;
  import org.apache.tinkerpop.gremlin.process.traversal.TextP;
+ import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
  import 
org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation;
  import org.apache.tinkerpop.gremlin.structure.Vertex;
+ import org.apache.tinkerpop.gremlin.structure.io.Io;
  import org.apache.tinkerpop.gremlin.structure.io.IoX;
  import org.apache.tinkerpop.gremlin.structure.io.IoXIoRegistry;
  import org.apache.tinkerpop.gremlin.structure.io.IoY;
@@@ -349,6 -378,313 +378,307 @@@ public class GryoMapperTest 
          assertEquals(te.toString(), serializeDeserialize(te, 
TraversalExplanation.class).toString());
      }
  
+     /**
+      * Gryo registers a handful of types with the shaded Kryo {@code 
JavaSerializer}, which deserializes by way of
+      * {@code java.io.ObjectInputStream.readObject()}. A stream that presents 
one of those type ids therefore
+      * reconstructs whatever {@code Serializable} object graph follows and 
runs its {@code readObject()} methods,
+      * which is an unsafe-deserialization sink on caller-supplied bytes. The 
canary used here carries no
+      * payload at all: the mere execution of its {@code readObject()} is the 
proof.
+      */
+     @Test
+     public void shouldNotInvokeJavaDeserializationOnGryoRead() throws 
Exception {
+         final Kryo kryo = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+ 
+         // Kryo frames an object as varint(class id + 2) followed by a 
NOT_NULL reference marker. Confirm that
+         // framing against a known registration (HashMap is id 11 in both 
V1_0 and V3_0) rather than trusting a
+         // hard coded Kryo internal, so the crafted stream below cannot 
silently stop reaching the serializer.
+         final int hashMapId = kryo.getRegistration(HashMap.class).getId();
+         assertEquals(11, hashMapId);
+         final Output probe = new Output(64, -1);
+         kryo.writeClassAndObject(probe, new HashMap<String, Object>());
+         probe.flush();
+         final Input probeInput = new Input(probe.toBytes());
+         assertEquals(hashMapId + CLASS_ID_OFFSET, 
probeInput.readVarInt(true));
+         assertEquals(KRYO_NOT_NULL, probeInput.readVarInt(true));
+ 
+         final byte[] malicious = maliciousGryoBytes();
+ 
+         DeserializationCanary.FIRED = false;
+         try {
+             kryo.readClassAndObject(new Input(new 
ByteArrayInputStream(malicious)));
+         } catch (Exception ignored) {
+             // refusing the stream outright is the expected outcome. what 
matters is that nothing was deserialized on
+             // the way to that decision.
+         }
+ 
+         assertFalse("Reading Gryo " + name + " bytes must not invoke 
ObjectInputStream.readObject() on the stream, " +
+                         "since a crafted stream presenting a 
JavaSerializer-backed type id such as " +
+                         OPTIONS_STRATEGY_GRYO_ID + " (OptionsStrategy) could 
otherwise carry an arbitrary Java " +
+                         "object graph",
+                 DeserializationCanary.FIRED);
+     }
+ 
+     /**
+      * Positive control for {@link 
#shouldNotInvokeJavaDeserializationOnGryoRead()}. Without hardening, the same 
bytes
+      * do reach {@code ObjectInputStream.readObject()}, which is what gives 
the assertion there any meaning: were the
+      * crafted framing ever to stop selecting the {@code JavaSerializer}, 
this test would fail and say so. It also
+      * documents that a directly built mapper stays full fidelity and must 
only be pointed at trusted bytes.
+      */
+     @Test
+     public void shouldInvokeJavaDeserializationOnDefaultMapperRead() throws 
Exception {
+         final Kryo kryo = builder.get().create().createMapper();
+         final byte[] malicious = maliciousGryoBytes();
+ 
+         DeserializationCanary.FIRED = false;
+         try {
+             kryo.readClassAndObject(new Input(new 
ByteArrayInputStream(malicious)));
+         } catch (Exception ignored) {
+             // the payload deserializes to the canary rather than to an 
OptionsStrategy, so a failure is possible
+             // here, but it would come after readObject() has already run
+         }
+ 
+         assertTrue("the crafted stream must reach 
ObjectInputStream.readObject() on a full fidelity mapper, " +
+                         "otherwise the hardened assertions prove nothing",
+                 DeserializationCanary.FIRED);
+     }
+ 
+     /**
+      * The same crafted stream fed through the reader that {@code io()} and 
graph persistence use, whose default
+      * mapper is hardened.
+      */
+     @Test
+     public void shouldNotInvokeJavaDeserializationOnGryoReaderRead() throws 
Exception {
+         final GryoReader reader = GryoReader.build().create();
+ 
+         DeserializationCanary.FIRED = false;
+         try (final InputStream stream = new 
ByteArrayInputStream(maliciousGryoBytes())) {
+             reader.readObject(stream, Object.class);
+         } catch (Exception ignored) {
+             // as above, refusing the stream is the expected outcome
+         }
+ 
+         assertFalse("GryoReader must not invoke 
ObjectInputStream.readObject() on the bytes it reads",
+                 DeserializationCanary.FIRED);
+     }
+ 
+     /**
+      * Hardening the mapper must not cost anything on the graph structure 
that a Gryo document actually carries.
+      */
+     @Test
+     public void shouldRoundTripGraphStructureWithJavaSerializationDisabled() 
throws Exception {
+         final Kryo kryo = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+ 
+         final Map<String, Object> props = new HashMap<>();
+         final List<Map<String, Object>> propertyNames = new ArrayList<>(1);
+         final Map<String, Object> propertyName = new HashMap<>();
+         propertyName.put(GraphSONTokens.ID, "x");
+         propertyName.put(GraphSONTokens.KEY, "x");
+         propertyName.put(GraphSONTokens.VALUE, "no-way-this-will-ever-work");
+         propertyNames.add(propertyName);
+         props.put("x", propertyNames);
+ 
+         final Output out = new Output(1024, -1);
+         kryo.writeClassAndObject(out, new DetachedVertex(100, 
Vertex.DEFAULT_LABEL, props));
+         out.flush();
+ 
+         final DetachedVertex readX = (DetachedVertex) kryo.readClassAndObject(
+                 new Input(new ByteArrayInputStream(out.toBytes())));
+         assertEquals("no-way-this-will-ever-work", readX.value("x"));
+     }
+ 
+     /**
+      * A Gryo stream that presents {@code OptionsStrategy}'s type id and then 
a raw Java-serialized payload. Crafting
+      * it needs no cooperation from the Gryo writer, which is why the sink 
was reachable from untrusted bytes.
+      */
+     private byte[] maliciousGryoBytes() throws Exception {
+         final ByteArrayOutputStream javaPayload = new ByteArrayOutputStream();
+         try (final ObjectOutputStream oos = new 
ObjectOutputStream(javaPayload)) {
+             oos.writeObject(new DeserializationCanary());
+         }
+ 
+         final Output malicious = new Output(javaPayload.size() + 64, -1);
+         malicious.writeVarInt(OPTIONS_STRATEGY_GRYO_ID + CLASS_ID_OFFSET, 
true);
+         malicious.writeVarInt(KRYO_NOT_NULL, true);
+         malicious.writeBytes(javaPayload.toByteArray());
+         malicious.flush();
+         return malicious.toBytes();
+     }
+ 
+     /**
+      * Companion to {@link #shouldNotInvokeJavaDeserializationOnGryoRead()} 
that covers the whole sink surface rather
+      * than one carrier type. The assertion is made against the {@code Kryo} 
instance that actually decodes bytes, so
+      * that it cannot pass merely by asking the same question of the same 
metadata the filter itself used.
+      */
+     @Test
+     public void shouldNotRegisterTypesWithJavaSerializerWhenDisabled() {
+         final Kryo hardened = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+ 
+         for (final TypeRegistration<?> tr : javaSerializedRegistrations()) {
+             final Class<?> clazz = tr.getTargetClass();
+             try {
+                 hardened.getRegistration(clazz);
+                 fail(clazz.getSimpleName() + " must not be registered on a 
hardened mapper");
+             } catch (IllegalArgumentException expected) {
+                 // Kryo refuses an unregistered class while registration is 
required
+             }
+         }
+     }
+ 
+     /**
+      * A custom type contributed with Kryo's {@code JavaSerializer} is 
filtered on the same terms as the defaults,
+      * since an {@link org.apache.tinkerpop.gremlin.structure.io.IoRegistry} 
is an untrusted-input path too.
+      */
+     @Test
+     public void shouldNotRegisterCustomTypesWithJavaSerializerWhenDisabled() {
+         final Kryo hardened = builder.get().addCustom(IoX.class, new 
JavaSerializer()).
+                 javaSerializationAllowed(false).create().createMapper();
+ 
+         try {
+             hardened.getRegistration(IoX.class);
+             fail("a custom JavaSerializer registration must not survive on a 
hardened mapper");
+         } catch (IllegalArgumentException expected) {
+             // as above
+         }
+     }
+ 
+     /**
+      * A custom type whose serializer is supplied as a {@code Function} 
resolving to a {@code JavaSerializer} can only
+      * be recognized once a {@code Kryo} exists, so it is dropped at 
mapper-creation time rather than at build time.
+      */
+     @Test
+     public void 
shouldNotRegisterCustomFunctionTypesWithJavaSerializerWhenDisabled() {
+         final Kryo hardened = builder.get().addCustom(IoX.class, 
(Function<Kryo, Serializer>) k -> new JavaSerializer()).
+                 javaSerializationAllowed(false).create().createMapper();
+ 
+         try {
+             hardened.getRegistration(IoX.class);
+             fail("a custom Function supplied JavaSerializer must not survive 
on a hardened mapper");
+         } catch (IllegalArgumentException expected) {
+             // Kryo refuses an unregistered class while registration is 
required
+         }
+     }
+ 
+     /**
+      * A type carrying {@code @DefaultSerializer(JavaSerializer.class)} and 
registered without an explicit serializer
+      * resolves to a {@code JavaSerializer} through Kryo's default, which is 
likewise dropped at mapper-creation time.
+      */
+     @Test
+     public void 
shouldNotRegisterDefaultSerializerJavaSerializerTypesWhenDisabled() {
+         final Kryo hardened = 
builder.get().addCustom(JavaSerializedByDefault.class).
+                 javaSerializationAllowed(false).create().createMapper();
+ 
+         try {
+             hardened.getRegistration(JavaSerializedByDefault.class);
+             fail("a @DefaultSerializer(JavaSerializer) registration must not 
survive on a hardened mapper");
+         } catch (IllegalArgumentException expected) {
+             // as above
+         }
+     }
+ 
+     /**
+      * The full fidelity mapper is unchanged and remains available for 
trusted, in-process round-trips. This test
+      * documents which registrations that leaves on native Java serialization.
+      */
+     @Test
+     public void shouldRegisterTypesWithJavaSerializerByDefault() {
+         final List<String> found = new ArrayList<>();
+         for (final TypeRegistration<?> tr : javaSerializedRegistrations())
+             found.add(String.format("%s(%d)", 
tr.getTargetClass().getSimpleName(), tr.getId()));
+ 
+         final List<String> expected = name.equals("1_0") ?
+                 Arrays.asList("TraversalExplanation(106)", 
"GroupBiOperator(117)", "OrderBiOperator(118)",
+                         "PartitionStrategy(140)", "SubgraphStrategy(141)", 
"SeedStrategy(192)",
+                         "VertexProgramStrategy(142)", 
"ProductiveByStrategy(195)", "OptionsStrategy(187)",
+                         "GValue(199)") :
+                 Arrays.asList("PartitionStrategy(140)", 
"SubgraphStrategy(141)", "SeedStrategy(192)",
+                         "VertexProgramStrategy(142)", 
"ProductiveByStrategy(195)", "OptionsStrategy(187)",
+                         "TraversalExplanation(106)", "GValue(199)");
+         assertEquals(expected, found);
+     }
+ 
+     /**
+      * The inverse of {@link 
#shouldNotRegisterTypesWithJavaSerializerWhenDisabled()}. Setting the value 
explicitly
+      * keeps the affected types usable, which is what a trusted, in-process 
round-trip relies on.
+      */
+     @Test
+     public void shouldRoundTripStrategyWhenJavaSerializationAllowed() throws 
Exception {
+         final Kryo kryo = 
builder.get().javaSerializationAllowed(true).create().createMapper();
+ 
+         final Output out = new Output(1024, -1);
+         kryo.writeClassAndObject(out, 
OptionsStrategy.build().with("some-key", "some-value").create());
+         out.flush();
+ 
+         final OptionsStrategy read = (OptionsStrategy) 
kryo.readClassAndObject(
+                 new Input(new ByteArrayInputStream(out.toBytes())));
+         assertEquals("some-value", read.getOptions().get("some-key"));
+     }
+ 
+     /**
+      * {@link GryoIo} hardens its mapper, and the {@code onMapper} consumer 
is the documented way to restore full
+      * fidelity where the bytes are trusted. This pins the form shown in the 
upgrade documentation, including the cast.
+      */
+     @Test
+     public void shouldRestoreJavaSerializationThroughGryoIoOnMapper() {
+         final Io.Builder<GryoIo> io = GryoIo.build(gryoVersion());
+         io.graph(EmptyGraph.instance());
+         io.onMapper(m -> ((GryoMapper.Builder) 
m).javaSerializationAllowed(true));
+ 
+         final Kryo restored = io.create().mapper().create().createMapper();
+         assertEquals(OPTIONS_STRATEGY_GRYO_ID, 
restored.getRegistration(OptionsStrategy.class).getId());
+     }
+ 
+     /**
+      * Without such a consumer, {@link GryoIo} is hardened like the reader 
and writer defaults.
+      */
+     @Test
+     public void shouldNotRegisterTypesWithJavaSerializerOnGryoIoDefault() {
+         final Io.Builder<GryoIo> io = GryoIo.build(gryoVersion());
+         io.graph(EmptyGraph.instance());
+ 
+         final Kryo hardened = io.create().mapper().create().createMapper();
+         try {
+             hardened.getRegistration(OptionsStrategy.class);
+             fail("GryoIo must not register OptionsStrategy by default");
+         } catch (IllegalArgumentException expected) {
+             // Kryo refuses an unregistered class while registration is 
required
+         }
+     }
+ 
+     /**
+      * The writer default is hardened too, so a document carrying one of the 
dropped types cannot be produced by the
+      * paths that could not read it back.
+      */
+     @Test
+     public void shouldNotWriteTypesWithJavaSerializerOnGryoWriterDefault() 
throws Exception {
+         final GryoWriter writer = GryoWriter.build().create();
+ 
+         try (final OutputStream stream = new ByteArrayOutputStream()) {
+             writer.writeObject(stream, 
OptionsStrategy.build().with("some-key", "some-value").create());
+             fail("the GryoWriter default must not write a JavaSerializer 
backed type");
+         } catch (IllegalArgumentException expected) {
+             // as above, Kryo refuses the unregistered class
+         }
+     }
+ 
+     private GryoVersion gryoVersion() {
+         return name.equals("1_0") ? GryoVersion.V1_0 : GryoVersion.V3_0;
+     }
+ 
+     /**
+      * The registrations that the full fidelity mapper of the version under 
test backs with Kryo's
+      * {@code JavaSerializer}, each of which is a carrier for the sink.
+      */
+     private List<TypeRegistration<?>> javaSerializedRegistrations() {
+         final List<TypeRegistration<?>> found = new ArrayList<>();
+         for (final TypeRegistration<?> tr : 
builder.get().create().getTypeRegistrations()) {
+             if (tr.getShadedSerializer() instanceof JavaSerializer) 
found.add(tr);
+         }
+ 
+         // if detection ever breaks, the tests that loop over this would pass 
without checking anything
+         assertThat(found.size(), greaterThan(0));
+         return found;
+     }
+ 
 -    @Test
 -    public void shouldHandleBytecode() throws Exception {
 -        final Bytecode bytecode = 
__().out().outV().outE().asAdmin().getBytecode();
 -        assertEquals(bytecode.toString(), serializeDeserialize(bytecode, 
Bytecode.class).toString());
 -    }
 -
      @Test
      public void shouldHandleClass() throws Exception {
          final Class<?> clazz = java.io.File.class;

Reply via email to