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 }