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