Skip to content

Commit 06e2334

Browse files
committed
Make Record serializable again
Instead of relying on the unstable default Java serialization of each record type, this uses a proxy object. The proxy object only contains the DNS wire data of a record, which is guaranteed to be stable. This avoids all concerns about compatibility when Record classes evolve and with inheritance. Closes #132 See #114
1 parent 4eff203 commit 06e2334

File tree

3 files changed

+94
-20
lines changed

3 files changed

+94
-20
lines changed

src/main/java/org/xbill/DNS/Record.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55

66
import java.io.ByteArrayOutputStream;
77
import java.io.IOException;
8+
import java.io.InvalidObjectException;
9+
import java.io.ObjectInputStream;
10+
import java.io.ObjectStreamException;
11+
import java.io.Serializable;
812
import java.text.DecimalFormat;
913
import java.util.Arrays;
1014
import java.util.function.Supplier;
15+
import lombok.extern.slf4j.Slf4j;
1116
import org.xbill.DNS.utils.base16;
1217

1318
/**
@@ -16,9 +21,11 @@
1621
*
1722
* @author Brian Wellington
1823
*/
19-
public abstract class Record implements Cloneable, Comparable<Record> {
24+
@Slf4j
25+
public abstract class Record implements Cloneable, Comparable<Record>, Serializable {
2026
protected Name name;
21-
protected int type, dclass;
27+
protected int type;
28+
protected int dclass;
2229
protected long ttl;
2330

2431
private static final DecimalFormat byteFormat = new DecimalFormat();
@@ -27,6 +34,25 @@ public abstract class Record implements Cloneable, Comparable<Record> {
2734
byteFormat.setMinimumIntegerDigits(3);
2835
}
2936

37+
private static class RecordSerializationProxy implements Serializable {
38+
private static final long serialVersionUID = 1434159920070152561L;
39+
private final byte[] wireData;
40+
private final boolean isEmpty;
41+
42+
RecordSerializationProxy(Record r) {
43+
this.isEmpty = r instanceof EmptyRecord;
44+
wireData = r.toWire(isEmpty ? Section.QUESTION : Section.ANSWER);
45+
}
46+
47+
protected Object readResolve() throws ObjectStreamException {
48+
try {
49+
return Record.fromWire(wireData, isEmpty ? Section.QUESTION : Section.ANSWER);
50+
} catch (IOException e) {
51+
throw new InvalidObjectException(e.getMessage());
52+
}
53+
}
54+
}
55+
3056
protected Record() {}
3157

3258
/** @since 3.1 */
@@ -43,6 +69,15 @@ protected Record(Name name, int type, int dclass, long ttl) {
4369
this.ttl = ttl;
4470
}
4571

72+
Object writeReplace() {
73+
log.trace("Creating proxy object for serialization");
74+
return new RecordSerializationProxy(this);
75+
}
76+
77+
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
78+
throw new InvalidObjectException("Use RecordSerializationProxy");
79+
}
80+
4681
private static Record getEmptyRecord(Name name, int type, int dclass, long ttl, boolean hasData) {
4782
Record rec;
4883
if (hasData) {
@@ -169,7 +204,8 @@ public static Record newRecord(Name name, int type, int dclass) {
169204
}
170205

171206
static Record fromWire(DNSInput in, int section, boolean isUpdate) throws IOException {
172-
int type, dclass;
207+
int type;
208+
int dclass;
173209
long ttl;
174210
int length;
175211
Name name;
@@ -404,12 +440,7 @@ protected static String byteArrayToString(byte[] array, boolean quote) {
404440

405441
/** Converts a byte array into the unknown RR format. */
406442
protected static String unknownToString(byte[] data) {
407-
StringBuilder sb = new StringBuilder();
408-
sb.append("\\# ");
409-
sb.append(data.length);
410-
sb.append(" ");
411-
sb.append(base16.toString(data));
412-
return sb.toString();
443+
return "\\# " + data.length + " " + base16.toString(data);
413444
}
414445

415446
/**
@@ -453,8 +484,7 @@ public static Record fromString(
453484
rec.rdataFromString(st, origin);
454485
t = st.get();
455486
if (t.type != Tokenizer.EOL && t.type != Tokenizer.EOF) {
456-
throw st.exception(
457-
"unexpected tokens at end of record (wanted EOL/EOF, got " + t.toString() + ")");
487+
throw st.exception("unexpected tokens at end of record (wanted EOL/EOF, got " + t + ")");
458488
}
459489
return rec;
460490
}

src/test/java/org/xbill/DNS/ARecordTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
import static org.junit.jupiter.api.Assertions.assertNull;
4141
import static org.junit.jupiter.api.Assertions.assertThrows;
4242

43+
import java.io.ByteArrayInputStream;
44+
import java.io.ByteArrayOutputStream;
4345
import java.io.IOException;
46+
import java.io.ObjectInputStream;
47+
import java.io.ObjectOutputStream;
4448
import java.net.InetAddress;
4549
import java.net.UnknownHostException;
4650
import org.junit.jupiter.api.BeforeEach;
@@ -135,4 +139,19 @@ void rrToWire() {
135139
ar.rrToWire(dout, null, false);
136140
assertArrayEquals(m_addr_bytes, dout.toByteArray());
137141
}
142+
143+
@Test
144+
void testSerializable() throws IOException, ClassNotFoundException {
145+
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
146+
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
147+
ARecord expected = new ARecord(Name.root, DClass.IN, 60, m_addr);
148+
oos.writeObject(expected);
149+
try (ObjectInputStream ois =
150+
new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
151+
Record actual = (Record) ois.readObject();
152+
assertEquals(expected, actual);
153+
}
154+
}
155+
}
156+
}
138157
}

src/test/java/org/xbill/DNS/RecordTest.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@
4646
import static org.junit.jupiter.api.Assertions.assertTrue;
4747
import static org.junit.jupiter.api.Assertions.fail;
4848

49+
import java.io.ByteArrayInputStream;
50+
import java.io.ByteArrayOutputStream;
4951
import java.io.IOException;
52+
import java.io.ObjectInputStream;
53+
import java.io.ObjectOutputStream;
5054
import java.net.InetAddress;
5155
import java.net.UnknownHostException;
5256
import java.time.Instant;
@@ -552,7 +556,6 @@ void fromString_invalid() throws IOException {
552556
int t = Type.A;
553557
int d = DClass.IN;
554558
int ttl = 0xABE99;
555-
InetAddress addr = InetAddress.getByName("191.234.43.10");
556559

557560
assertThrows(
558561
RelativeNameException.class,
@@ -863,14 +866,36 @@ void testAllTypesHaveNoArgConstructor() {
863866
assertNotNull(proto.get());
864867
} catch (Exception e) {
865868
fail(
866-
"Record type "
867-
+ Type.string(i)
868-
+ " ("
869-
+ i
870-
+ ", "
871-
+ proto.getClass().getSimpleName()
872-
+ ")"
873-
+ " seems to have no or invalid 0arg ctor");
869+
String.format(
870+
"Record type %s, (%d, %s) seems to have no or invalid 0arg ctor",
871+
Type.string(i), i, proto.getClass().getSimpleName()));
872+
}
873+
}
874+
}
875+
}
876+
877+
@Test
878+
void testSerializable() throws IOException {
879+
for (int i = 1; i < 65535; i++) {
880+
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
881+
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
882+
if (Type.getFactory(i) != null) {
883+
Record expected = Record.newRecord(Name.root, i, DClass.IN);
884+
try {
885+
oos.writeObject(expected);
886+
try (ObjectInputStream ois =
887+
new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
888+
Record actual = (Record) ois.readObject();
889+
assertEquals(expected, actual);
890+
}
891+
} catch (Exception e) {
892+
fail(
893+
String.format(
894+
"Record type %s (%d, %s) failed to (de)serialize",
895+
Type.string(i), i, expected.getClass().getSimpleName()),
896+
e);
897+
}
898+
}
874899
}
875900
}
876901
}

0 commit comments

Comments
 (0)