1 /*
  2  * Copyright (c) 1999, 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  * @library /test/lib
 27  * @summary Serialize and deserialize value objects
 28  * @enablePreview
 29  * @modules java.base/jdk.internal
 30  * @modules java.base/jdk.internal.value
 31  * @run junit/othervm SimpleValueGraphs
 32  */
 33 
 34 import java.io.ByteArrayInputStream;
 35 import java.io.ByteArrayOutputStream;
 36 import java.io.Externalizable;
 37 import java.io.IOException;
 38 import java.io.ObjectInput;
 39 import java.io.ObjectInputStream;
 40 import java.io.ObjectOutput;
 41 import java.io.ObjectOutputStream;
 42 import java.io.Serial;
 43 import java.io.Serializable;
 44 import java.io.InvalidClassException;
 45 
 46 import java.util.Arrays;
 47 import java.util.function.BiFunction;
 48 import java.util.Objects;
 49 
 50 import java.nio.charset.StandardCharsets;
 51 import java.util.stream.Stream;
 52 
 53 import jdk.internal.value.Deserializer;
 54 
 55 import jdk.test.lib.hexdump.HexPrinter;
 56 import jdk.test.lib.hexdump.ObjectStreamPrinter;
 57 import org.junit.jupiter.api.Assertions;
 58 import org.junit.jupiter.api.Test;
 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 public class SimpleValueGraphs implements Serializable {
 64 
 65     private static final boolean DEBUG = true;
 66 
 67     private static final SimpleValue foo1 = new SimpleValue("One", 1);
 68     private static final SimpleValue foo2 = new SimpleValue("Two", 2);
 69 
 70     public static Stream<Arguments> valueObjects() {
 71         return Stream.of(
 72                 Arguments.of(new SimpleValue(new SimpleValue(1))),
 73                 Arguments.of(new SimpleValue(new SimpleValue(2), 3)),
 74                 Arguments.of((Object)(new SimpleValue[] {foo1, foo1, foo2, foo1, foo2 })));
 75     }
 76 
 77     @ParameterizedTest
 78     @MethodSource("valueObjects")
 79     public void roundTrip(Object expected) throws Exception {
 80         byte[] bytes = serialize(expected);
 81 
 82         if (DEBUG)
 83             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
 84 
 85         Object actual = deserialize(bytes);
 86         System.out.println("actual: " + actual.toString());
 87         if (actual.getClass().isArray()) {
 88             Assertions.assertArrayEquals((Object[]) expected, (Object[]) actual, "Mismatch " + expected.getClass());
 89         } else {
 90             Assertions.assertEquals(expected, actual, "Mismatch " + expected.getClass());
 91         }
 92     }
 93 
 94     private static final Tree treeI = Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
 95     private static final Tree treeV = Tree.makeTree(3, (l, r) -> new TreeV((TreeV)l, (TreeV)l));
 96 
 97     // Create a tree of identity objects with a cycle; it will serialize ok, but when deserialized as
 98     // a value class the cycle is broken by replacing the back ref with null.
 99     private static Tree treeCycle(boolean cycle) {
100         TreeI tree = (TreeI)Tree.makeTree(3, (l, r) -> new TreeI((TreeI)l, (TreeI)l));
101         tree.setLeft(cycle ? tree : null);     // force a cycle or null
102         return tree;
103     }
104 
105     public static Stream<Arguments> migrationObjects() {
106         return Stream.of(
107                 Arguments.of(treeI, "TreeI", "TreeV", treeV), // Serialize as an identity class, deserialize as Value class
108                 Arguments.of(treeCycle(true), "TreeI", "TreeV", treeCycle(false))
109         );
110     }
111 
112     /**
113      * Test serializing an object graph, and deserialize with a modification of the serialized form.
114      * The modifications to the stream change the class name being deserialized.
115      * The cases include serializing an identity class and deserialize the corresponding
116      * value class.
117      *
118      * @param origObj an object to serialize
119      * @param origName a string in the serialized stream to replace
120      * @param replName a string to replace the original string
121      * @param expectedObject the expected object (graph) or an exception if it should fail
122      * @throws Exception some unexpected exception may be thrown and cause the test to fail
123      */
124     @ParameterizedTest
125     @MethodSource("migrationObjects")
126     public void treeVTest(Object origObj, String origName, String replName, Object expectedObject) throws Exception {
127         byte[] bytes = serialize(origObj);
128         if (DEBUG) {
129             System.out.println("Original serialized " + origObj.getClass().getName());
130             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
131         }
132 
133         // Modify the serialized bytes to change a class name from the serialized name
134         // to a different class. The replacement name must be the same length as the original name.
135         byte[] replBytes = patchBytes(bytes, origName, replName);
136         if (DEBUG) {
137             System.out.println("Modified serialized " + origObj.getClass().getName());
138             HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(replBytes);
139         }
140         try {
141             Object actual = Assertions.assertDoesNotThrow(() ->deserialize(replBytes));
142 
143             // Compare the shape of the actual and expected trees
144             Assertions.assertEquals(expectedObject.toString(), actual.toString(),
145                     "Resulting object not equals: " + actual.getClass().getName());
146 
147         } catch (Exception ex) {
148             ex.printStackTrace();
149             Assertions.assertEquals(expectedObject.getClass(), ex.getClass(), ex.toString());
150             Assertions.assertEquals(((Exception)expectedObject).getMessage(), ex.getMessage(), ex.toString());
151         }
152     }
153 
154     /**
155      * Serialize an object and return the serialized bytes.
156      *
157      * @param expected an object to serialize
158      * @return a byte array containing the serialized object
159      */
160     private static byte[] serialize(Object expected) throws IOException {
161         try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
162              ObjectOutputStream oout = new ObjectOutputStream(bout)) {
163             oout.writeObject(expected);
164             oout.flush();
165             return bout.toByteArray();
166         }
167     }
168 
169     /**
170      * Deserialize an object from the byte array.
171      * @param bytes a byte array
172      * @return an Object read from the byte array
173      */
174     private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
175         try (ByteArrayInputStream bin = new ByteArrayInputStream(bytes);
176              ObjectInputStream oin = new ObjectInputStream(bin)) {
177             return oin.readObject();
178         }
179     }
180 
181     /**
182      * Replace every occurrence of the string in the byte array with the replacement.
183      * The strings are US_ASCII only.
184      * @param bytes a byte array
185      * @param orig a string, converted to bytes using US_ASCII, originally exists in the bytes
186      * @param repl a string, converted to bytes using US_ASCII, to replace the original bytes
187      * @return a new byte array that has been patched
188      */
189     private byte[] patchBytes(byte[] bytes, String orig, String repl) {
190         return patchBytes(bytes,
191                 orig.getBytes(StandardCharsets.US_ASCII),
192                 repl.getBytes(StandardCharsets.US_ASCII));
193     }
194 
195     /**
196      * Replace every occurrence of the original bytes in the byte array with the replacement bytes.
197      * @param bytes a byte array
198      * @param orig a byte array containing existing bytes in the byte array
199      * @param repl a byte array to replace the original bytes
200      * @return a copy of the bytes array with each occurrence of the orig bytes with the replacement bytes
201      */
202     static byte[] patchBytes(byte[] bytes, byte[] orig, byte[] repl) {
203         if (orig.length != repl.length && orig.length > 0)
204             throw new IllegalArgumentException("orig bytes and replacement must be same length");
205         byte[] result = Arrays.copyOf(bytes, bytes.length);
206         for (int i = 0; i < result.length - orig.length; i++) {
207             if (Arrays.equals(result, i, i + orig.length, orig, 0, orig.length)) {
208                 System.arraycopy(repl, 0, result, i, orig.length);
209                 i = i + orig.length - 1;    // continue replacing after this occurrence
210             }
211         }
212         return result;
213     }
214 
215     public static class SimpleValue implements Serializable {
216         @Serial
217         private static final long serialVersionUID = 1L;
218 
219         int i;
220         Serializable obj;
221 
222         public SimpleValue(Serializable o) {
223             this.obj = o;
224             this.i = 0;
225         }
226         SimpleValue(int i) {
227             this.i = i;
228         }
229 
230         public SimpleValue(Serializable o, int i) {
231             this.obj = o;
232             this.i = i;
233         }
234 
235         public boolean equals(Object other) {
236             if (other instanceof SimpleValue simpleValue) {
237                 return (i == simpleValue.i && Objects.equals(obj, simpleValue.obj));
238             }
239             return false;
240         }
241 
242         public int hashCode() {
243             return i;
244         }
245 
246         public String toString() {
247             return "SimpleValue{" + "i=" + i + ", obj=" + obj + '}';
248         }
249     }
250 
251     interface Tree {
252         static Tree makeTree(int depth, BiFunction<Tree, Tree, Tree> genNode) {
253             if (depth <= 0) return null;
254             Tree left = makeTree(depth - 1, genNode);
255             Tree right = makeTree(depth - 1, genNode);
256             return genNode.apply(left, right);
257         }
258 
259         Tree left();
260         Tree right();
261     }
262     static class TreeI implements Tree, Serializable {
263 
264         @Serial
265         private static final long serialVersionUID = 2L;
266         private TreeI left;
267         private TreeI right;
268 
269         TreeI(TreeI left, TreeI right) {
270             this.left = left;
271             this.right = right;
272         }
273 
274         public TreeI left() {
275             return left;
276         }
277         public TreeI right() {
278             return right;
279         }
280 
281         public void setLeft(TreeI left) {
282             this.left = left;
283         }
284         public void setRight(TreeI right) {
285             this.right = right;
286         }
287 
288         public boolean equals(Object other) {
289             if (other instanceof TreeV tree) {
290                 boolean leftEq = (this.left == null && tree.left == null) ||
291                         left.equals(tree.left);
292                 boolean rightEq = (this.right == null && tree.right == null) ||
293                         right.equals(tree.right);
294                 return leftEq == rightEq;
295             }
296             return false;
297         }
298         public String toString() {
299             return toString(5);
300         }
301         public String toString(int depth) {
302             if (depth <= 0)
303                 return "!";
304             String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
305             String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
306             return "(" + l + r + ")";
307         }
308     }
309 
310     static value class TreeV implements Tree, Serializable {
311 
312         @Serial
313         private static final long serialVersionUID = 2L;
314         private TreeV left;
315         private TreeV right;
316 
317         @Deserializer({"left", "right"})
318         TreeV(TreeV left, TreeV right) {
319             this.left = left;
320             this.right = right;
321         }
322 
323         public TreeV left() {
324             return left;
325         }
326         public TreeV right() {
327             return right;
328         }
329 
330         public boolean equals(Object other) {
331             // avoid ==, is substitutable check causes stack overflow.
332             if (other instanceof TreeV tree) {
333                 return compRef(this.left, tree.left) && compRef(this.right, tree.right);
334             }
335             return false;
336         }
337 
338         // Compare references but don't use ==; isSubstitutable may recurse
339         private static boolean compRef(Object o1, Object o2) {
340             if (o1 == null && o2 == null)
341                 return true;
342             if (o1 != null && o2 != null)
343                 return o1.equals(o2);
344             return false;
345 
346         }
347         public String toString() {
348             return toString(10);
349         }
350         public String toString(int depth) {
351             if (depth <= 0)
352                 return "!";
353             String l = (left != null) ? left.toString(depth - 1) : Character.toString(126);
354             String r = (right != null) ? right.toString(depth - 1) : Character.toString(126);
355             return "(" + l + r + ")";
356         }
357     }
358 
359     @Test
360     void testExternalizableNotSer() {
361         var obj = new ValueExt();
362         var ex = Assertions.assertThrows(InvalidClassException.class, () -> serialize(obj));
363         Assertions.assertEquals("SimpleValueGraphs$ValueExt; cannot serialize value class", ex.getMessage());
364     }
365 
366     @Test
367     void testExternalizableNotDeser() throws IOException {
368         var obj = new IdentExt();
369         byte[] bytes = serialize(obj);
370         byte[] newBytes = patchBytes(bytes, "IdentExt", "ValueExt");
371         var ex = Assertions.assertThrows(InvalidClassException.class, () -> deserialize(newBytes));
372         Assertions.assertTrue(ex.getMessage().contains("cannot serialize value class"));
373     }
374 
375     // Exception trying to serialize
376     // Exception trying to deserialize
377 
378     static class IdentExt implements Externalizable {
379         public void writeExternal(ObjectOutput is) {
380 
381         }
382         public void readExternal(ObjectInput is) {
383 
384         }
385         @Serial
386         private static final long serialVersionUID = 3L;
387     }
388 
389     // Not Deserializable or Deserializable, no writeable fields
390     static value class ValueExt implements Externalizable {
391         public void writeExternal(ObjectOutput is) {
392 
393         }
394         public void readExternal(ObjectInput is) {
395 
396         }
397         @Serial
398         private static final long serialVersionUID = 3L;
399     }
400 }