feat(fls): a writer for the XMLDecoder format, checked by Java
javaBeansWriter.ts writes what java.beans.XMLDecoder reads — the counterpart of javabeans.ts and the base of the .fls writer. Its input says explicitly what Java should do: construct a class with typed arguments, call setters, getters (with statements on their result), methods and indexed access; an object reached twice is written once with an id and referenced after. Unlike XMLEncoder it writes what it is given, defaults included. What XML 1.0 cannot hold (controls, lone surrogates, U+FFFE/F) and a CR, which XML would read as a line break, are written as <char code>. Checked by Java itself: nine files of the writer (strings with every kind of odd unit, all primitive types including NaN, -0.0 and a long beyond 2^53, collections, maps, shared objects, Rectangle, Color, FILIUS beans with getter chains, arrays, indexed access, a static constant) are read by XMLDecoder in the Java oracle (BeansDump), which describes what it got and collects the errors the decoder would only log. Mutation probes regenerate the files and let Java read each mutant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,16 @@ wird leer, der Eintrag bleibt trotzdem; Typen außer A/MX/NS (auch `a`,
|
||||
`CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int`
|
||||
ist, wird still 3600.
|
||||
|
||||
## Was Java aus unserem XML macht
|
||||
|
||||
`BeansDump` liest jede `*.xml` unter `packages/fls/test/beans/` mit
|
||||
`XMLDecoder` und beschreibt, was dabei herauskommt (Beans über ihre
|
||||
Eigenschaften mit Lese- und Schreibmethode, Listen, Maps, Arrays,
|
||||
`Rectangle`, `Color`; ein zweites Mal gesehenes Objekt als Verweis) —
|
||||
`beans.java.json`. Fehler, die der Decoder sonst nur ins Log schreibt,
|
||||
sammelt ein `ExceptionListener`. Zeichenketten stehen als ASCII mit
|
||||
`\uXXXX`, weil ein einsames Surrogat in UTF-8 sonst zu „?“ würde.
|
||||
|
||||
## E-Mail-Kontodateien
|
||||
|
||||
`EmailFileCases server|client` legt für jede Zeile von
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -82,3 +82,9 @@ for kind in server client; do
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" EmailFileCases "$kind" "$cases" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# What XMLDecoder makes of the files packages/fls/src/javaBeansWriter.ts writes.
|
||||
beans="$root/packages/fls/test/beans"
|
||||
rm -f "$beans/beans.java.json"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" BeansDump "$beans" "$beans/beans.java.json" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$beans/beans.java.json"
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Reads every *.xml of a directory with java.beans.XMLDecoder and describes
|
||||
// the objects it gets — the check that Java reads what
|
||||
// packages/fls/src/javaBeansWriter.ts writes. Errors the decoder would only
|
||||
// log are collected; strings are written escaped (see escaped). Beans are described by their read-write properties
|
||||
// (java.beans.Introspector), lists, maps, arrays, Rectangle and Color by
|
||||
// their content; a value met again is a reference to its first description.
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes BeansDump <directory> <output json>
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Rectangle;
|
||||
import java.beans.Introspector;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.beans.XMLDecoder;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.Array;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class BeansDump {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Map<String, Object> result = new TreeMap<>();
|
||||
List<Path> files;
|
||||
try (Stream<Path> list = Files.list(Path.of(args[0]))) {
|
||||
files = list.filter(file -> file.toString().endsWith(".xml")).sorted().toList();
|
||||
}
|
||||
for (Path file : files) {
|
||||
List<Object> errors = new ArrayList<>();
|
||||
List<Object> values = new ArrayList<>();
|
||||
try (XMLDecoder decoder = new XMLDecoder(new FileInputStream(file.toFile()), null,
|
||||
error -> errors.add(error.getClass().getSimpleName() + ": " + error.getMessage()))) {
|
||||
BeansDump dump = new BeansDump();
|
||||
while (true) {
|
||||
Object value;
|
||||
try {
|
||||
value = decoder.readObject();
|
||||
} catch (ArrayIndexOutOfBoundsException end) {
|
||||
break;
|
||||
}
|
||||
values.add(dump.describe(value, 0));
|
||||
}
|
||||
}
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("values", values);
|
||||
entry.put("errors", errors);
|
||||
result.put(file.getFileName().toString(), entry);
|
||||
}
|
||||
try (FileOutputStream out = new FileOutputStream(args[1])) {
|
||||
out.write((FlsOracle.Json.write(result, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
System.out.println("beans dump: " + files.size() + " files");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private final IdentityHashMap<Object, Integer> seen = new IdentityHashMap<>();
|
||||
|
||||
Object describe(Object value, int depth) throws Exception {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Boolean flag) return flag;
|
||||
if (value instanceof String text) return tagged("string", escaped(text));
|
||||
if (value instanceof Integer number) return tagged("int", number);
|
||||
if (value instanceof Short number) return tagged("short", (int) number);
|
||||
if (value instanceof Byte number) return tagged("byte", (int) number);
|
||||
if (value instanceof Long number) return tagged("long", number.toString());
|
||||
if (value instanceof Double || value instanceof Float) return tagged(value.getClass().getSimpleName().toLowerCase(), value.toString());
|
||||
if (value instanceof Character character) return tagged("char", (int) character.charValue());
|
||||
if (value instanceof Class<?> type) return tagged("class", type.getName());
|
||||
if (value instanceof Rectangle rectangle) {
|
||||
return tagged("rectangle", List.of(rectangle.x, rectangle.y, rectangle.width, rectangle.height));
|
||||
}
|
||||
if (value instanceof Color color) {
|
||||
return tagged("color", List.of(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
|
||||
}
|
||||
Integer reference = seen.get(value);
|
||||
if (reference != null) return tagged("ref", reference);
|
||||
int id = seen.size();
|
||||
seen.put(value, id);
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("id", id);
|
||||
json.put("class", value.getClass().getName());
|
||||
if (value.getClass().isArray()) {
|
||||
List<Object> items = new ArrayList<>();
|
||||
for (int index = 0; index < Array.getLength(value); index++) items.add(describe(Array.get(value, index), depth + 1));
|
||||
json.put("items", items);
|
||||
} else if (value instanceof Collection<?> collection) {
|
||||
List<Object> items = new ArrayList<>();
|
||||
for (Object item : collection) items.add(describe(item, depth + 1));
|
||||
json.put("items", items);
|
||||
} else if (value instanceof Map<?, ?> map) {
|
||||
List<Object> entries = new ArrayList<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
entries.add(Arrays.asList(describe(entry.getKey(), depth + 1), describe(entry.getValue(), depth + 1)));
|
||||
}
|
||||
json.put("entries", entries);
|
||||
} else if (depth < 8) {
|
||||
Map<String, Object> properties = new TreeMap<>();
|
||||
for (PropertyDescriptor property : Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors()) {
|
||||
if (property.getReadMethod() == null || property.getWriteMethod() == null) continue;
|
||||
try {
|
||||
properties.put(property.getName(), describe(property.getReadMethod().invoke(value), depth + 1));
|
||||
} catch (Exception error) {
|
||||
properties.put(property.getName(), tagged("error", error.getClass().getSimpleName()));
|
||||
}
|
||||
}
|
||||
json.put("properties", properties);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every UTF-16 unit outside printable ASCII, and the backslash, as
|
||||
* backslash-u and four hex digits, so that no encoding step can change
|
||||
* a unit (a lone surrogate would become "?" in UTF-8).
|
||||
*/
|
||||
static String escaped(String text) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (char unit : text.toCharArray()) {
|
||||
if (unit < 0x20 || unit > 0x7E || unit == '\\') out.append(String.format("\\u%04X", (int) unit));
|
||||
else out.append(unit);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static Map<String, Object> tagged(String tag, Object value) {
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put(tag, value);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user