1 /*
  2  * Copyright (c) 2019, 2024, 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 testng/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 
 52 import jdk.internal.MigratedValueClass;
 53 import jdk.internal.value.DeserializeConstructor;
 54 
 55 import org.testng.Assert;
 56 import org.testng.annotations.DataProvider;
 57 import org.testng.annotations.Test;
 58 import static org.testng.Assert.assertEquals;
 59 import static org.testng.Assert.assertThrows;
 60 
 61 public class ValueSerializationTest {
 62 
 63     static final Class<NotSerializableException> NSE = NotSerializableException.class;
 64     private static final Class<InvalidClassException> ICE = InvalidClassException.class;
 65 
 66     @DataProvider(name = "doesNotImplementSerializable")
 67     public Object[][] doesNotImplementSerializable() {
 68         return new Object[][] {
 69             new Object[] { new NonSerializablePoint(10, 100), NSE},
 70             new Object[] { new NonSerializablePointNoCons(10, 100), ICE},
 71             // an array of Points
 72             new Object[] { new NonSerializablePoint[] {new NonSerializablePoint(1, 5)}, NSE},
 73             new Object[] { new Object[] {new NonSerializablePoint(3, 7)}, NSE},
 74             new Object[] { new ExternalizablePoint(12, 102), ICE},
 75             new Object[] { new ExternalizablePoint[] {
 76                     new ExternalizablePoint(3, 7),
 77                     new ExternalizablePoint(2, 8) }, ICE},
 78             new Object[] { new Object[] {
 79                     new ExternalizablePoint(13, 17),
 80                     new ExternalizablePoint(14, 18) }, ICE},
 81         };
 82     }
 83 
 84     // value class that DOES NOT implement Serializable should throw ICE
 85     @Test(dataProvider = "doesNotImplementSerializable")
 86     public void doesNotImplementSerializable(Object obj, Class expectedException) {
 87         assertThrows(expectedException, () -> serialize(obj));
 88     }
 89 
 90     /* Non-Serializable point. */
 91     public static value class NonSerializablePoint {
 92         public int x;
 93         public int y;
 94 
 95         public NonSerializablePoint(int x, int y) {
 96             this.x = x;
 97             this.y = y;
 98         }
 99         @Override public String toString() {
100             return "[NonSerializablePoint x=" + x + " y=" + y + "]";
101         }
102     }
103 
104     /* Non-Serializable point, because it does not have an @DeserializeConstructor constructor. */
105     public static value class NonSerializablePointNoCons implements Serializable {
106         public int x;
107         public int y;
108 
109         // Note: Must NOT have @DeserializeConstructor annotation
110         public NonSerializablePointNoCons(int x, int y) {
111             this.x = x;
112             this.y = y;
113         }
114         @Override public String toString() {
115             return "[NonSerializablePointNoCons x=" + x + " y=" + y + "]";
116         }
117     }
118 
119     /* An Externalizable Point is not Serializable, readExternal cannot modify fields */
120     static value class ExternalizablePoint implements Externalizable {
121         public int x;
122         public int y;
123         public ExternalizablePoint() {this.x = 0; this.y = 0;}
124         ExternalizablePoint(int x, int y) { this.x = x; this.y = y; }
125         @Override public void readExternal(ObjectInput in) {  }
126         @Override public void writeExternal(ObjectOutput out) {  }
127         @Override public String toString() {
128             return "[ExternalizablePoint x=" + x + " y=" + y + "]"; }
129     }
130 
131     @DataProvider(name = "ImplementSerializable")
132     public Object[][] implementSerializable() {
133         return new Object[][]{
134                 new Object[]{new SerializablePoint(11, 101)},
135                 new Object[]{new SerializablePoint[]{
136                         new SerializablePoint(1, 5),
137                         new SerializablePoint(2, 6)}},
138                 new Object[]{new Object[]{
139                         new SerializablePoint(3, 7),
140                         new SerializablePoint(4, 8)}},
141                 new Object[]{new SerializableFoo(45)},
142                 new Object[]{new SerializableFoo[]{new SerializableFoo(46)}},
143                 new Object[]{new ExternalizableFoo("hello")},
144                 new Object[]{new ExternalizableFoo[]{new ExternalizableFoo("there")}},
145         };
146     }
147 
148     // value class that DOES implement Serializable is supported
149     @Test(dataProvider = "ImplementSerializable")
150     public void implementSerializable(Object obj) throws IOException, ClassNotFoundException {
151         byte[] bytes = serialize(obj);
152         Object actual = deserialize(bytes);
153         if (obj.getClass().isArray())
154             assertEquals((Object[])obj, (Object[])actual);
155         else
156             assertEquals(obj, actual);
157     }
158 
159     /* A Serializable value class Point */
160     @MigratedValueClass
161     static value class SerializablePoint implements Serializable {
162         public int x;
163         public int y;
164         @DeserializeConstructor
165         private SerializablePoint(int x, int y) { this.x = x; this.y = y; }
166 
167         @Override public String toString() {
168             return "[SerializablePoint x=" + x + " y=" + y + "]";
169         }
170     }
171 
172     /* A Serializable Foo, with a serial proxy */
173     static value class SerializableFoo implements Serializable {
174         public int x;
175         @DeserializeConstructor
176         SerializableFoo(int x) { this.x = x; }
177 
178         @Serial Object writeReplace() throws ObjectStreamException {
179             return new SerialFooProxy(x);
180         }
181         @Serial private void readObject(ObjectInputStream s) throws InvalidObjectException {
182             throw new InvalidObjectException("Proxy required");
183         }
184         private record SerialFooProxy(int x) implements Serializable {
185             @Serial Object readResolve() throws ObjectStreamException {
186                 return new SerializableFoo(x);
187             }
188         }
189     }
190 
191     /* An Externalizable Foo, with a serial proxy */
192     static value class ExternalizableFoo implements Externalizable {
193         public String s;
194         ExternalizableFoo(String s) {  this.s = s; }
195         public boolean equals(Object other) {
196             if (other instanceof ExternalizableFoo foo) {
197                 return s.equals(foo.s);
198             } else {
199                 return false;
200             }
201         }
202         @Serial  Object writeReplace() throws ObjectStreamException {
203             return new SerialFooProxy(s);
204         }
205         private record SerialFooProxy(String s) implements Serializable {
206             @Serial Object readResolve() throws ObjectStreamException {
207                 return new ExternalizableFoo(s);
208             }
209         }
210         @Override public void readExternal(ObjectInput in) {  }
211         @Override public void writeExternal(ObjectOutput out) {  }
212     }
213 
214     // Generate a byte stream containing a reference to the named class with the SVID and flags.
215     private static byte[] byteStreamFor(String className, long uid, byte flags)
216         throws Exception
217     {
218         ByteArrayOutputStream baos = new ByteArrayOutputStream();
219         DataOutputStream dos = new DataOutputStream(baos);
220         dos.writeShort(STREAM_MAGIC);
221         dos.writeShort(STREAM_VERSION);
222         dos.writeByte(TC_OBJECT);
223         dos.writeByte(TC_CLASSDESC);
224         dos.writeUTF(className);
225         dos.writeLong(uid);
226         dos.writeByte(flags);
227         dos.writeShort(0);             // number of fields
228         dos.writeByte(TC_ENDBLOCKDATA);   // no annotations
229         dos.writeByte(TC_NULL);           // no superclasses
230         dos.close();
231         return baos.toByteArray();
232     }
233 
234     @DataProvider(name = "classes")
235     public Object[][] classes() {
236         return new Object[][] {
237             new Object[] { ExternalizableFoo.class, SC_EXTERNALIZABLE, ICE },
238             new Object[] { ExternalizableFoo.class, SC_SERIALIZABLE, ICE },
239             new Object[] { SerializablePoint.class, SC_EXTERNALIZABLE, ICE },
240             new Object[] { 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     @Test(dataProvider = "classes")
249     public void deserialize(Class<?> cls, byte flags, Class<?> expected) throws Exception {
250         var clsDesc = ObjectStreamClass.lookup(cls);
251         long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
252         byte[] serialBytes = byteStreamFor(cls.getName(), uid, flags);
253         try {
254             deserialize(serialBytes);
255             Assert.assertNull(expected, "Expected exception");
256         } catch (IOException ioe) {
257             Assert.assertEquals(ioe.getClass(), expected);
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 }