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 java.base/jdk.internal.value
 29  * @compile ValueSerializationTest.java
 30  * @run junit/othervm ValueSerializationTest
 31  */
 32 
 33 import static java.io.ObjectStreamConstants.*;
 34 
 35 import java.io.ByteArrayInputStream;
 36 import java.io.ByteArrayOutputStream;
 37 import java.io.DataOutputStream;
 38 import java.io.Externalizable;
 39 import java.io.IOException;
 40 import java.io.InvalidClassException;
 41 import java.io.InvalidObjectException;
 42 import java.io.NotSerializableException;
 43 import java.io.ObjectInput;
 44 import java.io.ObjectInputStream;
 45 import java.io.ObjectOutput;
 46 import java.io.ObjectOutputStream;
 47 import java.io.ObjectStreamClass;
 48 import java.io.ObjectStreamException;
 49 import java.io.Serial;
 50 import java.io.Serializable;
 51 import java.util.stream.Stream;
 52 
 53 import jdk.internal.MigratedValueClass;
 54 import jdk.internal.value.DeserializeConstructor;
 55 
 56 import org.junit.jupiter.api.Assertions;
 57 import static org.junit.jupiter.api.Assertions.assertEquals;
 58 import static org.junit.jupiter.api.Assertions.assertThrows;
 59 import org.junit.jupiter.api.TestInstance;
 60 import org.junit.jupiter.params.ParameterizedTest;
 61 import org.junit.jupiter.params.provider.Arguments;
 62 import org.junit.jupiter.params.provider.MethodSource;
 63 
 64 public class ValueSerializationTest {
 65 
 66     static final Class<NotSerializableException> NSE = NotSerializableException.class;
 67     private static final Class<InvalidClassException> ICE = InvalidClassException.class;
 68 
 69     public static Stream<Arguments> doesNotImplementSerializable() {
 70         return Stream.of(
 71             Arguments.of( new NonSerializablePoint(10, 100), NSE),
 72             Arguments.of( new NonSerializablePointNoCons(10, 100), ICE),
 73             // an array of Points
 74             Arguments.of( new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE),
 75             Arguments.of( Arguments.of(new NonSerializablePoint(3, 7)), NSE),
 76             Arguments.of( new ExternalizablePoint(12, 102), ICE),
 77             Arguments.of( new ExternalizablePoint[] {
 78                     new ExternalizablePoint(3, 7),
 79                     new ExternalizablePoint(2, 8) }, ICE),
 80             Arguments.of( new Object[] {
 81                     new ExternalizablePoint(13, 17),
 82                     new ExternalizablePoint(14, 18) }, ICE));
 83     }
 84 
 85     // value class that DOES NOT implement Serializable should throw ICE
 86     @ParameterizedTest
 87     @MethodSource("doesNotImplementSerializable")
 88     public void doesNotImplementSerializable(Object obj, Class expectedException) {
 89         assertThrows(expectedException, () -> serialize(obj));
 90     }
 91 
 92     /* Non-Serializable point. */
 93     public static value class NonSerializablePoint {
 94         public int x;
 95         public int y;
 96 
 97         public NonSerializablePoint(int x, int y) {
 98             this.x = x;
 99             this.y = y;
100         }
101         @Override public String toString() {
102             return "[NonSerializablePoint x=" + x + " y=" + y + "]";
103         }
104     }
105 
106     /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */
107     public static value class NonSerializablePointNoCons implements Serializable {
108         public int x;
109         public int y;
110 
111         // Note: Must NOT have @DeserializeConstructor annotation
112         public NonSerializablePointNoCons(int x, int y) {
113             this.x = x;
114             this.y = y;
115         }
116         @Override public String toString() {
117             return "[NonSerializablePointNoCons x=" + x + " y=" + y + "]";
118         }
119     }
120 
121     /* An Externalizable Point is not Serializable, readExternal cannot modify fields */
122     static value class ExternalizablePoint implements Externalizable {
123         public int x;
124         public int y;
125         public ExternalizablePoint() {this.x = 0; this.y = 0;}
126         ExternalizablePoint(int x, int y) { this.x = x; this.y = y; }
127         @Override public void readExternal(ObjectInput in) {  }
128         @Override public void writeExternal(ObjectOutput out) {  }
129         @Override public String toString() {
130             return "[ExternalizablePoint x=" + x + " y=" + y + "]"; }
131     }
132 
133     public static Stream<Arguments> implementSerializable() {
134         return Stream.of(
135                 Arguments.of(new SerializablePoint(11, 101)),
136                 Arguments.of((Object)(new SerializablePoint[]{
137                         new SerializablePoint(1, 5),
138                         new SerializablePoint(2, 6)}),
139                 Arguments.of(Arguments.of(
140                         new SerializablePoint(3, 7),
141                         new SerializablePoint(4, 8))),
142                 Arguments.of(new SerializableFoo(45)),
143                 Arguments.of((Object)(new SerializableFoo[]{new SerializableFoo(46)})),
144                 Arguments.of(new ExternalizableFoo("hello")),
145                 Arguments.of((Object)new ExternalizableFoo[]{new ExternalizableFoo("there")})));
146     }
147 
148     // value class that DOES implement Serializable is supported
149     @ParameterizedTest
150     @MethodSource("implementSerializable")
151     public void implementSerializable(Object obj) throws IOException, ClassNotFoundException {
152         byte[] bytes = serialize(obj);
153         Object actual = deserialize(bytes);
154         if (obj.getClass().isArray())
155             Assertions.assertArrayEquals((Object[])actual, (Object[])obj);
156         else
157             assertEquals(actual, obj);
158     }
159 
160     /* A Serializable value class Point */
161     @MigratedValueClass
162     static value class SerializablePoint implements Serializable {
163         public int x;
164         public int y;
165         @DeserializeConstructor
166         private SerializablePoint(int x, int y) { this.x = x; this.y = y; }
167 
168         @Override public String toString() {
169             return "[SerializablePoint x=" + x + " y=" + y + "]";
170         }
171     }
172 
173     /* A Serializable Foo, with a serial proxy */
174     static value class SerializableFoo implements Serializable {
175         public int x;
176         @DeserializeConstructor
177         SerializableFoo(int x) { this.x = x; }
178 
179         @Serial Object writeReplace() throws ObjectStreamException {
180             return new SerialFooProxy(x);
181         }
182         @Serial private void readObject(ObjectInputStream s) throws InvalidObjectException {
183             throw new InvalidObjectException("Proxy required");
184         }
185         private record SerialFooProxy(int x) implements Serializable {
186             @Serial Object readResolve() throws ObjectStreamException {
187                 return new SerializableFoo(x);
188             }
189         }
190     }
191 
192     /* An Externalizable Foo, with a serial proxy */
193     static value class ExternalizableFoo implements Externalizable {
194         public String s;
195         ExternalizableFoo(String s) {  this.s = s; }
196         public boolean equals(Object other) {
197             if (other instanceof ExternalizableFoo foo) {
198                 return s.equals(foo.s);
199             } else {
200                 return false;
201             }
202         }
203         @Serial  Object writeReplace() throws ObjectStreamException {
204             return new SerialFooProxy(s);
205         }
206         private record SerialFooProxy(String s) implements Serializable {
207             @Serial Object readResolve() throws ObjectStreamException {
208                 return new ExternalizableFoo(s);
209             }
210         }
211         @Override public void readExternal(ObjectInput in) {  }
212         @Override public void writeExternal(ObjectOutput out) {  }
213     }
214 
215     // Generate a byte stream containing a reference to the named class with the SVID and flags.
216     private static byte[] byteStreamFor(String className, long uid, byte flags)
217         throws Exception
218     {
219         ByteArrayOutputStream baos = new ByteArrayOutputStream();
220         DataOutputStream dos = new DataOutputStream(baos);
221         dos.writeShort(STREAM_MAGIC);
222         dos.writeShort(STREAM_VERSION);
223         dos.writeByte(TC_OBJECT);
224         dos.writeByte(TC_CLASSDESC);
225         dos.writeUTF(className);
226         dos.writeLong(uid);
227         dos.writeByte(flags);
228         dos.writeShort(0);             // number of fields
229         dos.writeByte(TC_ENDBLOCKDATA);   // no annotations
230         dos.writeByte(TC_NULL);           // no superclasses
231         dos.close();
232         return baos.toByteArray();
233     }
234 
235     public static Stream<Arguments> classes() {
236         return Stream.of(
237             Arguments.of( ExternalizableFoo.class, SC_EXTERNALIZABLE, ICE ),
238             Arguments.of( ExternalizableFoo.class, SC_SERIALIZABLE, ICE ),
239             Arguments.of( SerializablePoint.class, SC_EXTERNALIZABLE, ICE ),
240             Arguments.of( SerializablePoint.class, SC_SERIALIZABLE, null )
241         );
242     }
243 
244     // value class read directly from a byte stream
245     // a byte stream is generated containing a reference to the class with the flags  and SVID.
246     // Reading the class from the stream verifies the exceptions thrown if there is a mismatch
247     // between the stream and the local class.
248     @ParameterizedTest
249     @MethodSource("classes")
250     public void deserialize(Class<?> cls, byte flags, Class<Exception> expected) throws Exception {
251         var clsDesc = ObjectStreamClass.lookup(cls);
252         long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
253         byte[] serialBytes = byteStreamFor(cls.getName(), uid, flags);
254         if (expected == null) {
255             Assertions.assertDoesNotThrow(() -> deserialize(serialBytes));
256         } else {
257             Assertions.assertThrows(expected, () -> deserialize(serialBytes));
258         }
259     }
260 
261     static <T> byte[] serialize(T obj) throws IOException {
262         ByteArrayOutputStream baos = new ByteArrayOutputStream();
263         ObjectOutputStream oos = new ObjectOutputStream(baos);
264         oos.writeObject(obj);
265         oos.close();
266         return baos.toByteArray();
267     }
268 
269     @SuppressWarnings("unchecked")
270     static <T> T deserialize(byte[] streamBytes)
271         throws IOException, ClassNotFoundException
272     {
273         ByteArrayInputStream bais = new ByteArrayInputStream(streamBytes);
274         ObjectInputStream ois  = new ObjectInputStream(bais);
275         return (T) ois.readObject();
276     }
277 }