1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.lang3;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertNotSame;
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23 import static org.junit.jupiter.api.Assertions.assertTrue;
24
25 import java.io.ByteArrayInputStream;
26 import java.io.ByteArrayOutputStream;
27 import java.io.IOException;
28 import java.io.InvalidObjectException;
29 import java.io.ObjectInputStream;
30 import java.io.ObjectOutputStream;
31 import java.io.Serializable;
32 import java.util.Base64;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.Objects;
36
37 import org.apache.commons.lang3.reflect.FieldUtils;
38 import org.junit.jupiter.api.Test;
39
40
41
42
43 class RangeReadObjectTest {
44
45 private static final class ChangingHashEndpoint extends IdentityEndpoint {
46 private static final long serialVersionUID = 1L;
47 private transient int hash = 123;
48
49 ChangingHashEndpoint(final int value) {
50 super(value);
51 }
52
53 @Override
54 public boolean equals(final Object other) {
55 return this == other;
56 }
57
58 @Override
59 public int hashCode() {
60 return hash;
61 }
62 }
63
64 private static class IdentityEndpoint implements Serializable, Comparable<IdentityEndpoint> {
65 private static final long serialVersionUID = 1L;
66 private final int value;
67
68 IdentityEndpoint(final int value) {
69 this.value = value;
70 }
71
72 @Override
73 public int compareTo(final IdentityEndpoint other) {
74 return Integer.compare(value, other.value);
75 }
76 }
77
78
79
80
81
82
83 private static final class RangeForge implements Serializable {
84
85 private static final long serialVersionUID = 1L;
86 private final Object comparator;
87 private final Object maximum;
88 private final Object minimum;
89
90 RangeForge(final Object comparator, final Object minimum, final Object maximum) {
91 this.comparator = comparator;
92 this.minimum = minimum;
93 this.maximum = maximum;
94 }
95 }
96
97 private static Object deserialize(final byte[] bytes) throws IOException, ClassNotFoundException {
98 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
99 return ois.readObject();
100 }
101 }
102
103
104
105
106
107
108 private static byte[] forgeRangeStream(final Object comparator, final Object minimum, final Object maximum) throws IOException {
109
110 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
111 try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
112
113 @Override
114 protected void writeClassDescriptor(final java.io.ObjectStreamClass desc) throws IOException {
115 if (desc.getName().equals(RangeForge.class.getName())) {
116
117 final java.io.ObjectStreamClass spoofed = java.io.ObjectStreamClass.lookup(Range.class);
118 super.writeClassDescriptor(spoofed);
119 } else {
120 super.writeClassDescriptor(desc);
121 }
122 }
123 }) {
124 oos.writeObject(new RangeForge(comparator, minimum, maximum));
125 }
126 return baos.toByteArray();
127 }
128
129 @Test
130 void testCachedHashCodeRecomputed() throws Exception {
131 final Range<Integer> range = Range.of(1, 100);
132 FieldUtils.writeDeclaredField(range, "hashCode", 0xDEADBEEF, true);
133 final Range<Integer> copy = SerializationUtils.roundtrip(range);
134 assertEquals(Range.of(1, 100).hashCode(), copy.hashCode());
135 assertEquals(range, copy);
136 }
137
138 @Test
139 void testCloneIdentityHashEndpoints() {
140 final Range<IdentityEndpoint> original = Range.of(new IdentityEndpoint(1), new IdentityEndpoint(2));
141 final Range<IdentityEndpoint> copy = SerializationUtils.clone(original);
142 assertNotSame(original.getMinimum(), copy.getMinimum());
143 assertNotSame(original.getMaximum(), copy.getMaximum());
144 assertEquals(0, original.getMinimum().compareTo(copy.getMinimum()));
145 assertEquals(0, original.getMaximum().compareTo(copy.getMaximum()));
146 assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()), copy.hashCode());
147 }
148
149 @Test
150 void testComparatorNullViaForgedStream() throws Exception {
151 final Integer min = Integer.valueOf(1);
152 final Integer max = Integer.valueOf(10);
153 final byte[] forged = forgeRangeStream(null, min, max);
154 assertThrows(InvalidObjectException.class, () -> deserialize(forged));
155 }
156
157 @Test
158 void testDeserializeVersion320() {
159
160 final Range<?>[] expected = {Range.of(1, 2), IntegerRange.of(1, 2), LongRange.of(1, 2), DoubleRange.of(1, 2)};
161 final String[] streams = {
162 "rO0ABXNyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIAA0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGls" +
163 "L0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21pbmltdW1xAH4AAnhwfnIAM29yZy5hcGFjaGUu" +
164 "Y29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAA" +
165 "AAASAAB4cHQACElOU1RBTkNFc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVt" +
166 "YmVyhqyVHQuU4IsCAAB4cAAAAAJzcQB+AAgAAAAB",
167 "rO0ABXNyACVvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuSW50ZWdlclJhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21t" +
168 "b25zLmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAEC" +
169 "AANMAApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdt" +
170 "aW5pbXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAA" +
171 "ABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4" +
172 "AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAACc3EAfgAKAAAAAQ==",
173 "rO0ABXNyACJvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuTG9uZ1JhbmdlAAAAAAAAAAECAAB4cgAkb3JnLmFwYWNoZS5jb21tb25z" +
174 "LmxhbmczLk51bWJlclJhbmdlAAAAAAAAAAECAAB4cgAeb3JnLmFwYWNoZS5jb21tb25zLmxhbmczLlJhbmdlAAAAAAAAAAECAANM" +
175 "AApjb21wYXJhdG9ydAAWTGphdmEvdXRpbC9Db21wYXJhdG9yO0wAB21heGltdW10ABJMamF2YS9sYW5nL09iamVjdDtMAAdtaW5p" +
176 "bXVtcQB+AAR4cH5yADNvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UkQ29tcGFyYWJsZUNvbXBhcmF0b3IAAAAAAAAAABIA" +
177 "AHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AAhJTlNUQU5DRXNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAF" +
178 "dmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAnNxAH4ACgAAAAAAAAAB",
179 "rO0ABXNyACRvcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuRG91YmxlUmFuZ2UAAAAAAAAAAQIAAHhyACRvcmcuYXBhY2hlLmNvbW1v" +
180 "bnMubGFuZzMuTnVtYmVyUmFuZ2UAAAAAAAAAAQIAAHhyAB5vcmcuYXBhY2hlLmNvbW1vbnMubGFuZzMuUmFuZ2UAAAAAAAAAAQIA" +
181 "A0wACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7TAAHbWF4aW11bXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAB21p" +
182 "bmltdW1xAH4ABHhwfnIAM29yZy5hcGFjaGUuY29tbW9ucy5sYW5nMy5SYW5nZSRDb21wYXJhYmxlQ29tcGFyYXRvcgAAAAAAAAAA" +
183 "EgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQACElOU1RBTkNFc3IAEGphdmEubGFuZy5Eb3VibGWAs8JKKWv7BAIA" +
184 "AUQABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwQAAAAAAAAABzcQB+AAo/8AAAAAAAAA=="
185 };
186 for (int i = 0; i < streams.length; i++) {
187 final Range<?> actual = SerializationUtils.deserialize(Base64.getDecoder().decode(streams[i]));
188 assertEquals(expected[i].getClass(), actual.getClass());
189 assertEquals(expected[i], actual);
190 assertEquals(expected[i].hashCode(), actual.hashCode());
191 }
192 }
193
194 @Test
195 void testForgedReversedComparatorBreaksContains() throws Exception {
196 final Range<Integer> reference = Range.of(Integer.valueOf(1), Integer.valueOf(10));
197 final Range<Integer> forged = Range.of(Integer.valueOf(1), Integer.valueOf(10));
198 final Comparator<Integer> reversed = Collections.reverseOrder();
199 FieldUtils.writeDeclaredField(forged, "comparator", reversed, true);
200 assertThrows(InvalidObjectException.class, () -> deserialize(SerializationUtils.serialize(forged)));
201 assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(SerializationUtils.serialize(forged)));
202 assertThrows(SerializationException.class, () -> SerializationUtils.roundtrip(forged));
203 assertTrue(reference.contains(Integer.valueOf(5)));
204 }
205
206 @Test
207 void testMaximumNullViaForgedStream() throws Exception {
208 final Integer min = Integer.valueOf(1);
209 final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
210 final byte[] forged = forgeRangeStream(comparator, min, null);
211 assertThrows(InvalidObjectException.class, () -> deserialize(forged));
212 }
213
214 @Test
215 void testMinimumNullViaForgedStream() throws Exception {
216 final Integer max = Integer.valueOf(10);
217
218
219 final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
220 final byte[] forged = forgeRangeStream(comparator, null, max);
221 assertThrows(InvalidObjectException.class, () -> deserialize(forged));
222 }
223
224
225 @Test
226 void testNaNEndpointViaForgedStream() throws Exception {
227 final Double min = Double.valueOf(5.0);
228 final Double max = Double.valueOf(Double.NaN);
229 final Object comparator = Range.of(Integer.valueOf(1), Integer.valueOf(2)).getComparator();
230 final byte[] forged = forgeRangeStream(comparator, min, max);
231 assertThrows(InvalidObjectException.class, () -> deserialize(forged));
232 }
233
234 @Test
235 void testRoundTripChangingHashEndpoints() {
236 final Range<ChangingHashEndpoint> original = Range.of(new ChangingHashEndpoint(1), new ChangingHashEndpoint(2));
237 final Range<ChangingHashEndpoint> copy = SerializationUtils.roundtrip(original);
238 assertEquals(123, original.getMinimum().hashCode());
239 assertEquals(0, copy.getMinimum().hashCode());
240 assertEquals(Objects.hash(copy.getMinimum(), copy.getMaximum()), copy.hashCode());
241 }
242
243 @Test
244 void testRoundTripPreservesCorrectHashCode() throws Exception {
245 final Range<String> range = Range.of("apple", "mango");
246 final Range<String> roundtrip = SerializationUtils.roundtrip(range);
247 assertEquals(range.hashCode(), roundtrip.hashCode(), "Round-trip serialization must preserve the correct hashCode");
248 assertEquals(range, roundtrip);
249 }
250
251 }