1 /*
  2  * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @bug 8388318
 27  * @summary Test serialization of value classes
 28  * @enablePreview
 29  * @modules java.base/jdk.internal.value
 30  * @library /test/lib
 31  * @compile ValueSerializationTest.java
 32  * @build jdk.test.lib.helpers.StrictInit jdk.test.lib.helpers.StrictProcessor
 33  * @comment run the StrictProcessor over the classes that use \@StrictInit to
 34  *          generate classfiles with STRICT_INIT access flags for its annotated fields
 35  * @run driver jdk.test.lib.helpers.StrictProcessor
 36  *             ValueSerializationTest$IdentityStrictPoint
 37  * @run junit/othervm -DdeserializerOnBootclasspath=false ${test.main.class}
 38  * @run junit/bootclasspath/othervm -DdeserializerOnBootclasspath=true ${test.main.class}
 39  */
 40 
 41 import java.io.ByteArrayInputStream;
 42 import java.io.ByteArrayOutputStream;
 43 import java.io.DataOutputStream;
 44 import java.io.Externalizable;
 45 import java.io.IOException;
 46 import java.io.InvalidClassException;
 47 import java.io.InvalidObjectException;
 48 import java.io.NotSerializableException;
 49 import java.io.ObjectInput;
 50 import java.io.ObjectInputStream;
 51 import java.io.ObjectOutput;
 52 import java.io.ObjectOutputStream;
 53 import java.io.ObjectStreamClass;
 54 import java.io.ObjectStreamException;
 55 import java.io.Serial;
 56 import java.io.Serializable;
 57 import java.util.stream.Stream;
 58 
 59 import jdk.internal.value.Deserializer;
 60 import jdk.test.lib.helpers.StrictInit;
 61 import org.junit.jupiter.api.BeforeAll;
 62 import org.junit.jupiter.params.ParameterizedTest;
 63 import org.junit.jupiter.params.provider.Arguments;
 64 import org.junit.jupiter.params.provider.MethodSource;
 65 import static java.io.ObjectStreamConstants.SC_EXTERNALIZABLE;
 66 import static java.io.ObjectStreamConstants.SC_SERIALIZABLE;
 67 import static java.io.ObjectStreamConstants.STREAM_MAGIC;
 68 import static java.io.ObjectStreamConstants.STREAM_VERSION;
 69 import static java.io.ObjectStreamConstants.TC_CLASSDESC;
 70 import static java.io.ObjectStreamConstants.TC_ENDBLOCKDATA;
 71 import static java.io.ObjectStreamConstants.TC_NULL;
 72 import static java.io.ObjectStreamConstants.TC_OBJECT;
 73 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 74 import static org.junit.jupiter.api.Assertions.assertEquals;
 75 import static org.junit.jupiter.api.Assertions.assertThrows;
 76 import static org.junit.jupiter.api.Assertions.assertTrue;
 77 
 78 public class ValueSerializationTest {
 79 
 80     private static final boolean DESERIALIZER_ON_BOOTCLASSPATH = Boolean.getBoolean("deserializerOnBootclasspath");
 81     private static final Class<NotSerializableException> NSE = NotSerializableException.class;
 82     private static final Class<InvalidClassException> ICE = InvalidClassException.class;
 83 
 84     @BeforeAll
 85     static void setup() {
 86         System.out.println("deserializerOnBootclasspath: " + DESERIALIZER_ON_BOOTCLASSPATH);
 87     }
 88 
 89     static Stream<Arguments> serializationFailingInstances() {
 90         return DESERIALIZER_ON_BOOTCLASSPATH ? serializationAlwaysFailingInstances()
 91                 : Stream.concat(serializationAlwaysFailingInstances(),
 92                                 // Unrecognized deserializer leads to ICE
 93                                 deserializerInstances().map(a -> Arguments.of(a, ICE)));
 94     }
 95 
 96     static Stream<Arguments> serializationAlwaysFailingInstances() {
 97         return Stream.of(
 98                 Arguments.of(
 99                         new NonSerializableValue(10, 100),
100                         NSE
101                 ),
102 
103                 Arguments.of(
104                         new ValueWithNoDeserializer(10, 100),
105                         ICE
106                 ),
107 
108                 Arguments.of(
109                         new IdentityStrictPoint(),
110                         ICE
111                 ),
112 
113                 Arguments.of(
114                         new StrictInSuper(),
115                         ICE
116                 ),
117 
118                 Arguments.of(
119                         new StrictInTwiceSuper(),
120                         ICE
121                 ),
122 
123                 Arguments.of(
124                         new StrictInAbstractValueSuper(),
125                         ICE
126                 ),
127 
128                 // an array of non-serializable value objects
129                 Arguments.of(
130                         new NonSerializableValue[]{
131                                 new NonSerializableValue(1, 5)
132                         },
133                         NSE
134                 ),
135 
136                 Arguments.of(
137                         new Object[]{
138                                 new NonSerializableValue(3, 7)
139                         },
140                         NSE
141                 ),
142 
143                 Arguments.of(
144                         new ExternalizableValue(12, 102),
145                         ICE
146                 ),
147 
148                 Arguments.of(
149                         new ExternalizableValue[]{
150                                 new ExternalizableValue(3, 7),
151                                 new ExternalizableValue(2, 8)
152                         },
153                         ICE
154                 ),
155 
156                 Arguments.of(
157                         new Object[]{
158                                 new ExternalizableValue(13, 17),
159                                 new ExternalizableValue(14, 18)
160                         },
161                         ICE
162                 )
163         );
164     }
165 
166     /*
167      * Verifies that the given obj that isn't expected to be serializable
168      * throws the expected exception from ObjectOutputStream.writeObject()
169      */
170     @ParameterizedTest
171     @MethodSource("serializationFailingInstances")
172     void testSerializationFails(Object obj, Class<? extends Exception> expectedException)
173             throws Exception {
174         // expect serialization to fail
175         try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
176             assertThrows(expectedException, () -> oos.writeObject(obj));
177         }
178     }
179 
180     static Stream<Object> deserializerInstances() {
181         return Stream.of(
182                 new ValueWithDeserializer(11, 101),
183 
184                 new ValueWithDeserializer[]{
185                         new ValueWithDeserializer(1, 5),
186                         new ValueWithDeserializer(2, 6)
187                 },
188 
189                 new Object[]{
190                         new ValueWithDeserializer(3, 7),
191                         new ValueWithDeserializer(4, 8)
192                 }
193         );
194     }
195 
196     static Stream<Object> serializingInstances() {
197         return DESERIALIZER_ON_BOOTCLASSPATH ? Stream.concat(deserializerInstances(), alwaysSerializingInstances())
198                 : alwaysSerializingInstances();
199     }
200 
201     static Stream<Object> alwaysSerializingInstances() {
202         return Stream.of(
203                 new ValueWriteReplaceWithIdentity(45),
204 
205                 new ValueWriteReplaceWithIdentity[]{
206                         new ValueWriteReplaceWithIdentity(46)
207                 },
208 
209                 new ExtValueWithIdentityReplacement("hello"),
210 
211                 new ExtValueWithIdentityReplacement[]{
212                         new ExtValueWithIdentityReplacement("there")
213                 },
214 
215                 new CustomNumberWithIdentity(16, 42),
216 
217                 new CustomNumberWithIdentity[] {
218                         new CustomNumberWithIdentity(-6, 77),
219                         new CustomNumberWithIdentity(54, -79),
220                 }
221         );
222     }
223 
224     /*
225      * Verifies that a value object that implements java.io.Serializable and is associated with
226      * the JDK internal jdk.internal.value.Deserializer can be serialized and deserialized through
227      * the use of ObjectOutputStream.writeObject() and ObjectInputStream.readObject() successfully.
228      * The deserialized object is then compared with the given obj to verify that they are equal.
229      */
230     @ParameterizedTest
231     @MethodSource("serializingInstances")
232     void testSerDeserSucceeds(Object obj) throws IOException, ClassNotFoundException {
233         // serialize
234         ByteArrayOutputStream baos = new ByteArrayOutputStream();
235         try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
236             oos.writeObject(obj);
237         }
238         byte[] bytes = baos.toByteArray();
239         Object actual;
240         // deserialize
241         try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
242             actual = ois.readObject();
243         }
244         // compare the deserialized with the original
245         if (obj.getClass().isArray()) {
246             assertArrayEquals((Object[]) obj, (Object[]) actual);
247         } else {
248             assertEquals(obj, actual);
249         }
250     }
251 
252     static Stream<Arguments> classes() {
253         return Stream.of(
254                 Arguments.of(
255                         ExtValueWithIdentityReplacement.class,
256                         SC_EXTERNALIZABLE,
257                         ICE
258                 ),
259 
260                 Arguments.of(
261                         ExtValueWithIdentityReplacement.class,
262                         SC_SERIALIZABLE,
263                         ICE
264                 ),
265 
266                 Arguments.of(
267                         IdentityStrictPoint.class,
268                         SC_SERIALIZABLE,
269                         ICE
270                 ),
271 
272                 Arguments.of(
273                         StrictInSuper.class,
274                         SC_SERIALIZABLE,
275                         ICE
276                 ),
277 
278                 Arguments.of(
279                         StrictInTwiceSuper.class,
280                         SC_SERIALIZABLE,
281                         ICE
282                 ),
283 
284                 Arguments.of(
285                         StrictInAbstractValueSuper.class,
286                         SC_SERIALIZABLE,
287                         ICE
288                 ),
289 
290                 Arguments.of(
291                         ValueWithDeserializer.class,
292                         SC_EXTERNALIZABLE,
293                         ICE
294                 ),
295 
296                 Arguments.of(
297                         ValueWithDeserializer.class,
298                         SC_SERIALIZABLE,
299                         DESERIALIZER_ON_BOOTCLASSPATH ? null : ICE
300                 ),
301 
302                 Arguments.of(
303                         CustomNumberWithIdentity.class,
304                         SC_SERIALIZABLE,
305                         null
306                 )
307         );
308     }
309 
310     /*
311      * A byte stream is generated containing a reference to the given class
312      * with the given flags and a serial version UID determined in the test method.
313      * The byte stream is then read using ObjectInputStream.readObject() and the test verifies
314      * that if an exception is expected to be thrown then it is thrown, or if the deserialization
315      * is expected to complete normally, then it verifies that no exception is thrown.
316      */
317     @ParameterizedTest
318     @MethodSource("classes")
319     void testDeser(Class<?> clazz, byte flags, Class<? extends Exception> expectedException)
320             throws Exception {
321         ObjectStreamClass clsDesc = ObjectStreamClass.lookup(clazz);
322         long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
323         byte[] serialBytes = byteStreamFor(clazz.getName(), uid, flags);
324         // deserialize
325         try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialBytes))) {
326             if (expectedException != null) {
327                 assertThrows(expectedException, () -> ois.readObject());
328             } else {
329                 ois.readObject();
330             }
331         }
332     }
333 
334     // Generate a byte stream containing a reference to the named class with the SVID and flags.
335     private static byte[] byteStreamFor(String className, long uid, byte flags) throws Exception {
336         ByteArrayOutputStream baos = new ByteArrayOutputStream();
337         DataOutputStream dos = new DataOutputStream(baos);
338         dos.writeShort(STREAM_MAGIC);
339         dos.writeShort(STREAM_VERSION);
340         dos.writeByte(TC_OBJECT);
341         dos.writeByte(TC_CLASSDESC);
342         dos.writeUTF(className);
343         dos.writeLong(uid);
344         dos.writeByte(flags);
345         dos.writeShort(0);             // number of fields
346         dos.writeByte(TC_ENDBLOCKDATA);   // no annotations
347         dos.writeByte(TC_NULL);           // no superclasses
348         dos.close();
349         return baos.toByteArray();
350     }
351 
352     /**
353      * A concrete value class that doesn't implement Serializable (or Externalizable) interface
354      */
355     public static value class NonSerializableValue {
356         public int x;
357         public int y;
358 
359         public NonSerializableValue(int x, int y) {
360             this.x = x;
361             this.y = y;
362         }
363 
364         @Override
365         public String toString() {
366             return "[NonSerializableValue x=" + x + " y=" + y + "]";
367         }
368     }
369 
370     /**
371      * An identity class with strict initialized instance fields
372      */
373     public static class IdentityStrictPoint implements Serializable {
374         static {
375             for (var f : IdentityStrictPoint.class.getDeclaredFields()) {
376                 assertTrue(f.isStrictInit(), "missing strict init on field: " + f.getName());
377             }
378         }
379 
380         @StrictInit
381         public int x;
382         @StrictInit
383         public int y;
384 
385         public IdentityStrictPoint() {
386             x = 3;
387             y = 5;
388             super();
389         }
390 
391         @Override
392         public String toString() {
393             return "[IdentityStrictPoint x=" + x + " y=" + y + "]";
394         }
395     }
396 
397     /**
398      * A concrete value class that implements java.io.Serializable and doesn't have any
399      * jdk.internal.value.Deserializer on its constructor.
400      */
401     public static value class ValueWithNoDeserializer implements Serializable {
402         public int x;
403         public int y;
404 
405         // Note: Must NOT have @Deserializer annotation
406         public ValueWithNoDeserializer(int x, int y) {
407             this.x = x;
408             this.y = y;
409         }
410 
411         @Override
412         public String toString() {
413             return "[ValueWithNoDeserializer x=" + x + " y=" + y + "]";
414         }
415     }
416 
417     /**
418      * A concrete value class which implements java.io.Externalizable and doesn't
419      * implement writeReplace().
420      */
421     static value class ExternalizableValue implements Externalizable {
422         public int x;
423         public int y;
424 
425         public ExternalizableValue() {
426             this.x = 0;
427             this.y = 0;
428         }
429 
430         ExternalizableValue(int x, int y) {
431             this.x = x;
432             this.y = y;
433         }
434 
435         @Override
436         public void readExternal(ObjectInput in) {
437             // concrete value class isn't expected to be deserializable, so we don't
438             // expect this method to be invoked during deserialization.
439             throw new AssertionError("not expected to be invoked on " + this);
440         }
441 
442         @Override
443         public void writeExternal(ObjectOutput out) {
444             // concrete value class isn't expected to be serializable, so we don't
445             // expect this method to be invoked during serialization.
446             throw new AssertionError("not expected to be invoked on " + this);
447         }
448 
449         @Override
450         public String toString() {
451             return "[ExternalizableValue x=" + x + " y=" + y + "]";
452         }
453     }
454 
455 
456     /**
457      * A concrete value class which implements java.io.Serializable and has a
458      * jdk.internal.value.Deserializer associated with its constructor.
459      * It may be serialized only if it is on the boot class path.
460      */
461     static value class ValueWithDeserializer implements Serializable {
462         public int x;
463         public int y;
464 
465         @Deserializer({"x", "y"})
466         private ValueWithDeserializer(int x, int y) {
467             this.x = x;
468             this.y = y;
469         }
470 
471         @Override
472         public String toString() {
473             return "[ValueWithDeserializer x=" + x + " y=" + y + "]";
474         }
475     }
476 
477     /**
478      * A concrete value class which implements java.io.Serializable
479      * and implements the writeReplace() method to return an identity
480      * object.
481      */
482     static value class ValueWriteReplaceWithIdentity implements Serializable {
483         public int x;
484 
485         ValueWriteReplaceWithIdentity(int x) {
486             this.x = x;
487         }
488 
489         @Serial
490         Object writeReplace() throws ObjectStreamException {
491             return new IdentityRecord(x);
492         }
493 
494         @Serial
495         private void readObject(ObjectInputStream s) throws InvalidObjectException {
496             // the writeReplace() implementation of this class, when the serialization side
497             // is preparing to write this object to the stream, has replaced this object
498             // with an instance of a different class, so we don't expect deserialization
499             // to invoke this method.
500             throw new AssertionError("not expected to be invoked on " + this);
501         }
502 
503         @Override
504         public String toString() {
505             return "[ValueWriteReplaceWithIdentity x=" + x + "]";
506         }
507 
508         private record IdentityRecord(int x) implements Serializable {
509             @Serial
510             Object readResolve() throws ObjectStreamException {
511                 return new ValueWriteReplaceWithIdentity(x);
512             }
513         }
514     }
515 
516     /**
517      * A concrete value class which implements java.io.Externalizable and implements
518      * the writeReplace() method to return an identity object.
519      */
520     static value class ExtValueWithIdentityReplacement implements Externalizable {
521         public String s;
522 
523         ExtValueWithIdentityReplacement(String s) {
524             this.s = s;
525         }
526 
527         @Override
528         public boolean equals(Object other) {
529             return other instanceof ExtValueWithIdentityReplacement foo && s.equals(foo.s);
530         }
531 
532         @Serial
533         Object writeReplace() throws ObjectStreamException {
534             return new IdentityRecord(s);
535         }
536 
537         private record IdentityRecord(String s) implements Serializable {
538             @Serial
539             Object readResolve() throws ObjectStreamException {
540                 return new ExtValueWithIdentityReplacement(s);
541             }
542         }
543 
544         @Override
545         public void readExternal(ObjectInput in) {
546             // the writeReplace() implementation of this class, when the serialization side
547             // is preparing to write this object to the stream, has replaced this object
548             // with an instance of a different class, so we don't expect deserialization
549             // to invoke this method.
550             throw new AssertionError("not expected to be invoked on " + this);
551         }
552 
553         @Override
554         public void writeExternal(ObjectOutput out) {
555             // the writeReplace() implementation of this class, when the serialization side
556             // is preparing to write this object to the stream, has replaced this object
557             // with an instance of a different class, so we don't expect this method to
558             // play any role during serialization.
559             throw new AssertionError("not expected to be invoked on " + this);
560         }
561 
562         @Override
563         public String toString() {
564             return "[ExtValueWithIdentityReplacement s=" + s + "]";
565         }
566     }
567 
568     /**
569      * A plain identity class that does not declare any strictly initialized
570      * instance field but its immediate superclasses, which implement Serializable,
571      * does.
572      */
573     public static class StrictInSuper extends IdentityStrictPoint {
574         // Declares no field, inherits strictly-initialized instance fields
575         // IdentityStrictPoint.x and y
576         public StrictInSuper() {
577             super();
578         }
579 
580         @Override
581         public String toString() {
582             return "[StrictInSuper x=" + x + " y=" + y + "]";
583         }
584     }
585 
586     /**
587      * A plain identity class that does not declare any strictly initialized
588      * instance field but one of its non-immediate superclasses that implement
589      * Serializable does.
590      */
591     public static class StrictInTwiceSuper extends StrictInSuper {
592         // Declares no field, inherits strictly-initialized instance fields
593         // IdentityStrictPoint.x and y
594         public StrictInTwiceSuper() {
595             super();
596         }
597 
598         @Override
599         public String toString() {
600             return "[StrictInTwiceSuper x=" + x + " y=" + y + "]";
601         }
602     }
603 
604     /**
605      * An abstract value class that declares instance fields. Such fields are
606      * always strictly initialized, making none of their subclasses serializable
607      * without jdk.internal.value.Deserializer.
608      */
609     public abstract static value class HasFieldAbstractValue implements Serializable {
610         public int x;
611         public int y;
612 
613         public HasFieldAbstractValue(int x, int y) {
614             this.x = x;
615             this.y = y;
616         }
617 
618         @Override
619         public String toString() {
620             return "[HasFieldAbstractValue x=" + x + " y=" + y + "]";
621         }
622     }
623 
624     /**
625      * An identity class that inherits strictly-initialized instance fields from
626      * its abstract value superclass that is serializable.  Therefore, this class
627      * is not serializable without jdk.internal.value.Deserializer.
628      */
629     public static class StrictInAbstractValueSuper extends HasFieldAbstractValue {
630         public StrictInAbstractValueSuper() {
631             super(42, -3);
632         }
633 
634         @Override
635         public String toString() {
636             return "[StrictInAbstractValueSuper x=" + x + " y=" + y + "]";
637         }
638     }
639 
640     /**
641      * An identity class that does not inherit any strictly-initialized instance
642      * field from its abstract value superclass that is serializable, in this
643      * case the migrated java.lang.Number.  This class is accepted by default
644      * serialization.
645      */
646     public static class CustomNumberWithIdentity extends Number {
647         public int x;
648         public int y;
649 
650         public CustomNumberWithIdentity(int x, int y) {
651             this.x = x;
652             this.y = y;
653         }
654 
655         @Override
656         public int intValue() {
657             return (int) longValue();
658         }
659 
660         @Override
661         public long longValue() {
662             return (long) x << 32 | y;
663         }
664 
665         @Override
666         public float floatValue() {
667             return longValue();
668         }
669 
670         @Override
671         public double doubleValue() {
672             return longValue();
673         }
674 
675         @Override
676         public final boolean equals(Object o) {
677             if (!(o instanceof CustomNumberWithIdentity that))
678                 return false;
679 
680             return x == that.x && y == that.y;
681         }
682 
683         @Override
684         public int hashCode() {
685             int result = x;
686             result = 31 * result + y;
687             return result;
688         }
689 
690         @Override
691         public String toString() {
692             return "[CustomNumberWithIdentity x=" + x + " y=" + y + "]";
693         }
694     }
695 }