| 1 | package com.rox.save; | |
| 2 | ||
| 3 | import java.io.ByteArrayInputStream; | |
| 4 | import java.io.ByteArrayOutputStream; | |
| 5 | import java.io.DataInputStream; | |
| 6 | import java.io.DataOutputStream; | |
| 7 | import java.io.IOException; | |
| 8 | import java.io.UncheckedIOException; | |
| 9 | import java.lang.reflect.Constructor; | |
| 10 | import java.lang.reflect.InvocationTargetException; | |
| 11 | import java.lang.reflect.RecordComponent; | |
| 12 | import java.util.Arrays; | |
| 13 | import java.util.Optional; | |
| 14 | ||
| 15 | /** | |
| 16 | * Turns a snapshot record tree (see {@link SystemSnapshot}) into a live-snapshot save payload and | |
| 17 | * back, walking each record's components in declaration order - so a new field on any component's | |
| 18 | * snapshot record is picked up without touching this class, at the cost of that reordering or adding | |
| 19 | * components changes the byte layout (bump {@code SaveCodec}'s version when that matters). | |
| 20 | * | |
| 21 | * <p>Supported component types: {@code int}, {@code long}, {@code boolean}, {@code int[]} (every | |
| 22 | * element must be a byte value, 0-255 - all snapshot arrays are RAM/register bytes - and is stored as | |
| 23 | * one byte), {@code boolean[]}, nested records, and sealed interfaces of records (stored as the index | |
| 24 | * of the concrete type in {@link Class#getPermittedSubclasses()}, then that record). | |
| 25 | */ | |
| 26 | final class SnapshotCodec { | |
| 27 | private SnapshotCodec(){ | |
| 28 | } | |
| 29 | ||
| 30 | static byte[] encode(final Record record){ | |
| 31 | final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | |
| 32 | try (DataOutputStream out = new DataOutputStream(buffer)){ | |
| 33 |
1
1. encode : removed call to com/rox/save/SnapshotCodec::writeRecord → KILLED |
writeRecord(out, record); |
| 34 | } catch (IOException e){ | |
| 35 | //in-memory only, same reasoning as SaveCodec.encode() | |
| 36 | throw new UncheckedIOException(e); | |
| 37 | } | |
| 38 |
1
1. encode : replaced return value with null for com/rox/save/SnapshotCodec::encode → KILLED |
return buffer.toByteArray(); |
| 39 | } | |
| 40 | ||
| 41 | /** Empty if {@code bytes} doesn't decode cleanly into exactly one {@code type} with nothing left over. */ | |
| 42 | static <T extends Record> Optional<T> decode(final byte[] bytes, final Class<T> type){ | |
| 43 | try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytes))){ | |
| 44 | final T record = type.cast(readRecord(in, type)); | |
| 45 |
1
1. decode : negated conditional → KILLED |
return in.available() == 0 ? Optional.of(record) : Optional.empty(); |
| 46 | } catch (IOException | IllegalArgumentException e){ | |
| 47 | //truncated (EOFException), a bad sealed-type tag/array length, or a record's own constructor | |
| 48 | //rejecting what was read - all just "not a usable snapshot" | |
| 49 | return Optional.empty(); | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | private static void writeRecord(final DataOutputStream out, final Record record) throws IOException { | |
| 54 | for (final RecordComponent component : record.getClass().getRecordComponents()){ | |
| 55 |
1
1. writeRecord : removed call to com/rox/save/SnapshotCodec::write → KILLED |
write(out, component.getType(), accessorValue(component, record)); |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | private static void write(final DataOutputStream out, final Class<?> type, final Object value) throws IOException { | |
| 60 |
1
1. write : negated conditional → KILLED |
if (type == int.class){ |
| 61 |
1
1. write : removed call to java/io/DataOutputStream::writeInt → KILLED |
out.writeInt((Integer) value); |
| 62 |
1
1. write : negated conditional → KILLED |
} else if (type == long.class){ |
| 63 |
1
1. write : removed call to java/io/DataOutputStream::writeLong → KILLED |
out.writeLong((Long) value); |
| 64 |
1
1. write : negated conditional → KILLED |
} else if (type == boolean.class){ |
| 65 |
1
1. write : removed call to java/io/DataOutputStream::writeBoolean → KILLED |
out.writeBoolean((Boolean) value); |
| 66 |
1
1. write : negated conditional → KILLED |
} else if (type == int[].class){ |
| 67 | final int[] array = (int[]) value; | |
| 68 |
1
1. write : removed call to java/io/DataOutputStream::writeInt → KILLED |
out.writeInt(array.length); |
| 69 | for (final int element : array){ | |
| 70 |
4
1. write : changed conditional boundary → KILLED 2. write : negated conditional → KILLED 3. write : negated conditional → KILLED 4. write : changed conditional boundary → KILLED |
if (element < 0 || element > 0xFF){ |
| 71 | throw new IllegalArgumentException("Snapshot arrays must hold byte values (0-255), found " + element); | |
| 72 | } | |
| 73 |
1
1. write : removed call to java/io/DataOutputStream::writeByte → KILLED |
out.writeByte(element); |
| 74 | } | |
| 75 |
1
1. write : negated conditional → KILLED |
} else if (type == boolean[].class){ |
| 76 | final boolean[] array = (boolean[]) value; | |
| 77 |
1
1. write : removed call to java/io/DataOutputStream::writeInt → KILLED |
out.writeInt(array.length); |
| 78 | for (final boolean element : array){ | |
| 79 |
1
1. write : removed call to java/io/DataOutputStream::writeBoolean → KILLED |
out.writeBoolean(element); |
| 80 | } | |
| 81 |
1
1. write : negated conditional → KILLED |
} else if (type.isRecord()){ |
| 82 |
1
1. write : removed call to com/rox/save/SnapshotCodec::writeRecord → KILLED |
writeRecord(out, (Record) value); |
| 83 |
1
1. write : negated conditional → KILLED |
} else if (type.isSealed()){ |
| 84 |
1
1. write : removed call to java/io/DataOutputStream::writeByte → KILLED |
out.writeByte(Arrays.asList(type.getPermittedSubclasses()).indexOf(value.getClass())); |
| 85 |
1
1. write : removed call to com/rox/save/SnapshotCodec::writeRecord → KILLED |
writeRecord(out, (Record) value); |
| 86 | } else { | |
| 87 | throw new IllegalArgumentException("Unsupported snapshot component type " + type); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | private static Record readRecord(final DataInputStream in, final Class<?> type) throws IOException { | |
| 92 | final RecordComponent[] components = type.getRecordComponents(); | |
| 93 | final Class<?>[] componentTypes = new Class<?>[components.length]; | |
| 94 | final Object[] values = new Object[components.length]; | |
| 95 |
2
1. readRecord : changed conditional boundary → KILLED 2. readRecord : negated conditional → KILLED |
for (int i = 0; i < components.length; i++){ |
| 96 | componentTypes[i] = components[i].getType(); | |
| 97 | values[i] = read(in, componentTypes[i]); | |
| 98 | } | |
| 99 | try { | |
| 100 | final Constructor<?> canonical = type.getDeclaredConstructor(componentTypes); | |
| 101 |
1
1. readRecord : replaced return value with null for com/rox/save/SnapshotCodec::readRecord → KILLED |
return (Record) canonical.newInstance(values); |
| 102 | } catch (InvocationTargetException e){ | |
| 103 | throw new IllegalArgumentException("Snapshot record rejected its decoded values", e.getCause()); | |
| 104 | } catch (ReflectiveOperationException e){ | |
| 105 | throw new IllegalStateException("Snapshot record " + type + " has no usable canonical constructor", e); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | private static Object read(final DataInputStream in, final Class<?> type) throws IOException { | |
| 110 |
1
1. read : negated conditional → KILLED |
if (type == int.class){ |
| 111 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return in.readInt(); |
| 112 |
1
1. read : negated conditional → KILLED |
} else if (type == long.class){ |
| 113 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return in.readLong(); |
| 114 |
1
1. read : negated conditional → KILLED |
} else if (type == boolean.class){ |
| 115 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return in.readBoolean(); |
| 116 |
1
1. read : negated conditional → KILLED |
} else if (type == int[].class){ |
| 117 | final int[] array = new int[checkedLength(in)]; | |
| 118 |
2
1. read : changed conditional boundary → KILLED 2. read : negated conditional → KILLED |
for (int i = 0; i < array.length; i++){ |
| 119 | array[i] = in.readUnsignedByte(); | |
| 120 | } | |
| 121 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return array; |
| 122 |
1
1. read : negated conditional → KILLED |
} else if (type == boolean[].class){ |
| 123 | final boolean[] array = new boolean[checkedLength(in)]; | |
| 124 |
2
1. read : negated conditional → KILLED 2. read : changed conditional boundary → KILLED |
for (int i = 0; i < array.length; i++){ |
| 125 | array[i] = in.readBoolean(); | |
| 126 | } | |
| 127 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return array; |
| 128 |
1
1. read : negated conditional → KILLED |
} else if (type.isRecord()){ |
| 129 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return readRecord(in, type); |
| 130 |
1
1. read : negated conditional → KILLED |
} else if (type.isSealed()){ |
| 131 | final Class<?>[] permitted = type.getPermittedSubclasses(); | |
| 132 | final int tag = in.readUnsignedByte(); | |
| 133 |
2
1. read : negated conditional → KILLED 2. read : changed conditional boundary → KILLED |
if (tag >= permitted.length){ |
| 134 | throw new IllegalArgumentException("Unknown " + type.getSimpleName() + " type tag " + tag); | |
| 135 | } | |
| 136 |
1
1. read : replaced return value with null for com/rox/save/SnapshotCodec::read → KILLED |
return readRecord(in, permitted[tag]); |
| 137 | } | |
| 138 | throw new IllegalArgumentException("Unsupported snapshot component type " + type); | |
| 139 | } | |
| 140 | ||
| 141 | /** Rejects a corrupt length before it can drive a huge allocation - same guard as {@code SaveCodec.decode()}'s payload length. */ | |
| 142 | private static int checkedLength(final DataInputStream in) throws IOException { | |
| 143 | final int length = in.readInt(); | |
| 144 |
4
1. checkedLength : negated conditional → KILLED 2. checkedLength : negated conditional → KILLED 3. checkedLength : changed conditional boundary → KILLED 4. checkedLength : changed conditional boundary → KILLED |
if (length < 0 || length > in.available()){ |
| 145 | throw new IllegalArgumentException("Array length " + length + " exceeds the remaining payload"); | |
| 146 | } | |
| 147 |
1
1. checkedLength : replaced int return with 0 for com/rox/save/SnapshotCodec::checkedLength → KILLED |
return length; |
| 148 | } | |
| 149 | ||
| 150 | private static Object accessorValue(final RecordComponent component, final Record record){ | |
| 151 | try { | |
| 152 |
1
1. accessorValue : replaced return value with null for com/rox/save/SnapshotCodec::accessorValue → KILLED |
return component.getAccessor().invoke(record); |
| 153 | } catch (ReflectiveOperationException e){ | |
| 154 | throw new IllegalStateException("Can't read snapshot component " + component, e); | |
| 155 | } | |
| 156 | } | |
| 157 | } | |
Mutations | ||
| 33 |
1.1 |
|
| 38 |
1.1 |
|
| 45 |
1.1 |
|
| 55 |
1.1 |
|
| 60 |
1.1 |
|
| 61 |
1.1 |
|
| 62 |
1.1 |
|
| 63 |
1.1 |
|
| 64 |
1.1 |
|
| 65 |
1.1 |
|
| 66 |
1.1 |
|
| 68 |
1.1 |
|
| 70 |
1.1 2.2 3.3 4.4 |
|
| 73 |
1.1 |
|
| 75 |
1.1 |
|
| 77 |
1.1 |
|
| 79 |
1.1 |
|
| 81 |
1.1 |
|
| 82 |
1.1 |
|
| 83 |
1.1 |
|
| 84 |
1.1 |
|
| 85 |
1.1 |
|
| 95 |
1.1 2.2 |
|
| 101 |
1.1 |
|
| 110 |
1.1 |
|
| 111 |
1.1 |
|
| 112 |
1.1 |
|
| 113 |
1.1 |
|
| 114 |
1.1 |
|
| 115 |
1.1 |
|
| 116 |
1.1 |
|
| 118 |
1.1 2.2 |
|
| 121 |
1.1 |
|
| 122 |
1.1 |
|
| 124 |
1.1 2.2 |
|
| 127 |
1.1 |
|
| 128 |
1.1 |
|
| 129 |
1.1 |
|
| 130 |
1.1 |
|
| 133 |
1.1 2.2 |
|
| 136 |
1.1 |
|
| 144 |
1.1 2.2 3.3 4.4 |
|
| 147 |
1.1 |
|
| 152 |
1.1 |