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;
