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