ExampleRun opens an example as FILIUS opens it (SzenarioVerwaltung.laden into the window's lists), starts it as the action mode does, plays a story and writes every card's frames and tables, the calls, the addresses and the RIP tables' changes. examplesJava.test.ts reads the same file with this package's reader and runs it in the core (projectSnapshot, new): routing (internetwork_komplett), DHCP (einfaches_rechnernetz_komplett), DNS (dns_server), NAT (nat_with_mapped_port) and firewall (Internet_Komplett_..._DE) match card by card. Found and fixed: a host's ipForwardingEnabled, which Java saves and obeys, was unknown to the reader, the model, the core, the writer, the export and FlsOracle; and Java's ARP learns only from a request for its own address or a reply. Named Java bugs: a ping without a gateway throws a NullPointerException; a home router drops the answer of a second remote end. Open: Java picks between equally long RIP routes by thread timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
568 lines
25 KiB
Java
568 lines
25 KiB
Java
// A core scenario's network (packages/core/test/scenarios/*.scenario.json:
|
|
// nodes, links (wireless or not), devices, switches' and hosts' SSIDs,
|
|
// dhcpServers, dhcpClients, …) built in Java FILIUS the
|
|
// way the core reads it: a port's MAC is its `mac`, else the core's
|
|
// stableGeneratedMacAddress. Started as the action mode starts it (every
|
|
// system software's starten()); afterwards every frame each network card saw
|
|
// (FILIUS' own packet log, the Lauscher) and each host's addresses. Shared by
|
|
// DhcpRun and TcpRun.
|
|
|
|
import filius.gui.GUIContainer;
|
|
import filius.gui.netzwerksicht.GUIKnotenItem;
|
|
import filius.rahmenprogramm.SzenarioVerwaltung;
|
|
import filius.hardware.Kabel;
|
|
import filius.hardware.NetzwerkInterface;
|
|
import filius.hardware.Port;
|
|
import filius.hardware.knoten.Gateway;
|
|
import filius.hardware.knoten.InternetKnoten;
|
|
import filius.hardware.knoten.Knoten;
|
|
import filius.hardware.knoten.LokalerKnoten;
|
|
import filius.hardware.knoten.Notebook;
|
|
import filius.hardware.knoten.Rechner;
|
|
import filius.hardware.knoten.Switch;
|
|
import filius.hardware.knoten.Vermittlungsrechner;
|
|
import filius.rahmenprogramm.nachrichten.Lauscher;
|
|
import filius.software.dhcp.DHCPServer;
|
|
import filius.software.dns.DNSServer;
|
|
import filius.software.netzzugangsschicht.EthernetFrame;
|
|
import filius.software.system.Betriebssystem;
|
|
import filius.software.system.GatewayFirmware;
|
|
import filius.software.system.InternetKnotenBetriebssystem;
|
|
import filius.software.system.SwitchFirmware;
|
|
import filius.software.system.VermittlungsrechnerBetriebssystem;
|
|
import filius.software.transportschicht.Segment;
|
|
import filius.software.transportschicht.TcpSegment;
|
|
import filius.software.vermittlungsschicht.ArpPaket;
|
|
import filius.software.vermittlungsschicht.IcmpPaket;
|
|
import filius.software.vermittlungsschicht.IpPaket;
|
|
|
|
import java.io.FileOutputStream;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.Writer;
|
|
import java.lang.reflect.Field;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.TreeMap;
|
|
|
|
public class ScenarioNetwork {
|
|
final Map<String, Knoten> nodes = new LinkedHashMap<>();
|
|
/** Each card's MAC address, under its node's id (the first card) or `id#index` (the others). */
|
|
final Map<String, String> macByNode = new LinkedHashMap<>();
|
|
final List<String> servers = new ArrayList<>();
|
|
/** Loaded from a .fls file (fromFile): started as the action mode starts it, in the file's order. */
|
|
boolean fromFile;
|
|
/** fromFile: each card's MAC address and its node's name with the card's index, `name/index`. */
|
|
final Map<String, String> cardByMac = new LinkedHashMap<>();
|
|
long start;
|
|
|
|
/**
|
|
* A .fls file loaded as FILIUS opens it (SzenarioVerwaltung.laden into the
|
|
* window's lists): the nodes under their names (a repeated name gets
|
|
* `#2`, `#3`, …), each card's frames under its MAC address.
|
|
*/
|
|
static ScenarioNetwork fromFile(String path) throws Exception {
|
|
GUIContainer gui = GUIContainer.getGUIContainer();
|
|
if (!SzenarioVerwaltung.getInstance().laden(path, gui.getKnotenItems(), gui.getCableItems(), gui.getDocuItems())) {
|
|
throw new IllegalStateException("FILIUS could not load " + path);
|
|
}
|
|
ScenarioNetwork network = new ScenarioNetwork();
|
|
network.fromFile = true;
|
|
for (GUIKnotenItem item : gui.getKnotenItems()) {
|
|
Knoten knoten = item.getKnoten();
|
|
String name = knoten.getName();
|
|
for (int count = 2; network.nodes.containsKey(name); count++) name = knoten.getName() + "#" + count;
|
|
network.nodes.put(name, knoten);
|
|
if (knoten instanceof InternetKnoten) {
|
|
List<NetzwerkInterface> nics = ((InternetKnoten) knoten).getNetzwerkInterfaces();
|
|
for (int index = 0; index < nics.size(); index++) {
|
|
network.macByNode.put(nics.get(index).getMac(), nics.get(index).getMac());
|
|
network.cardByMac.put(nics.get(index).getMac(), name + "/" + index);
|
|
}
|
|
}
|
|
}
|
|
return network;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
static ScenarioNetwork read(String path) throws Exception {
|
|
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
|
|
new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value();
|
|
ScenarioNetwork network = new ScenarioNetwork();
|
|
network.build(scenario);
|
|
return network;
|
|
}
|
|
|
|
/** The scenario's seed. */
|
|
@SuppressWarnings("unchecked")
|
|
static long seed(String path) throws Exception {
|
|
Map<String, Object> scenario = (Map<String, Object>) new JsonReader(
|
|
new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value();
|
|
return ((Number) scenario.get("seed")).longValue();
|
|
}
|
|
|
|
private void build(Map<String, Object> scenario) {
|
|
Map<String, Port> ports = new HashMap<>();
|
|
Map<String, NetzwerkInterface> nicsByPort = new HashMap<>();
|
|
for (Object each : list(scenario.get("nodes"))) {
|
|
Map<String, Object> node = map(each);
|
|
String id = (String) node.get("id");
|
|
String kind = (String) node.get("kind");
|
|
List<Object> nodePorts = list(node.get("ports"));
|
|
Knoten knoten;
|
|
switch (kind) {
|
|
case "pc":
|
|
knoten = new Rechner();
|
|
break;
|
|
case "notebook":
|
|
knoten = new Notebook();
|
|
break;
|
|
case "networkSwitch":
|
|
knoten = new Switch();
|
|
break;
|
|
case "router":
|
|
knoten = new Vermittlungsrechner();
|
|
((InternetKnoten) knoten).setzeAnzahlAnschluesse(nodePorts.size());
|
|
break;
|
|
case "gateway":
|
|
// Its first card is the WAN port, its second the LAN port.
|
|
knoten = new Gateway();
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("node kind " + kind + " is not built by this tool");
|
|
}
|
|
knoten.setName(id);
|
|
if (knoten instanceof InternetKnoten) {
|
|
List<NetzwerkInterface> nics = ((InternetKnoten) knoten).getNetzwerkInterfaces();
|
|
for (int index = 0; index < nodePorts.size(); index++) {
|
|
Map<String, Object> port = map(nodePorts.get(index));
|
|
NetzwerkInterface nic = nics.get(index);
|
|
String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id"));
|
|
nic.setMac(mac);
|
|
ports.put((String) port.get("id"), nic.getPort());
|
|
nicsByPort.put((String) port.get("id"), nic);
|
|
macByNode.put(index == 0 ? id : id + "#" + index, mac);
|
|
}
|
|
} else {
|
|
LinkedList<Port> switchPorts = ((LokalerKnoten) knoten).getAnschluesse();
|
|
for (int index = 0; index < nodePorts.size(); index++) {
|
|
ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index));
|
|
}
|
|
}
|
|
nodes.put(id, knoten);
|
|
}
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) {
|
|
Map<String, Object> device = map(entry.getValue());
|
|
NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0);
|
|
nic.setIp((String) device.get("ip"));
|
|
nic.setSubnetzMaske((String) device.get("mask"));
|
|
nic.setGateway((String) device.getOrDefault("gateway", ""));
|
|
nic.setDns((String) device.getOrDefault("dns", ""));
|
|
}
|
|
// A router's other cards: `interfaces` names them by port.
|
|
for (Object each : list(scenario.getOrDefault("interfaces", new ArrayList<>()))) {
|
|
Map<String, Object> item = map(each);
|
|
NetzwerkInterface nic = nicsByPort.get(item.get("port"));
|
|
nic.setIp((String) item.get("ip"));
|
|
nic.setSubnetzMaske((String) item.get("mask"));
|
|
}
|
|
for (Object each : list(scenario.get("links"))) {
|
|
Map<String, Object> link = map(each);
|
|
Kabel cable = new Kabel();
|
|
cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) });
|
|
cable.setWireless(Boolean.TRUE.equals(link.get("wireless")));
|
|
}
|
|
// A switch's SSID and learning time; a host's wireless card and the SSID
|
|
// it joins. The SSIDs only the editor reads, to lay a wireless cable.
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("switches", new HashMap<>())).entrySet()) {
|
|
Map<String, Object> config = map(entry.getValue());
|
|
SwitchFirmware firmware = (SwitchFirmware) nodes.get(entry.getKey()).getSystemSoftware();
|
|
firmware.setSSID((String) config.get("ssid"));
|
|
// How long the switch keeps a learned MAC address (Java's default 300 s).
|
|
if (config.containsKey("retention")) firmware.setRetentionTime(((Number) config.get("retention")).longValue());
|
|
}
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("wireless", new HashMap<>())).entrySet()) {
|
|
Betriebssystem os = (Betriebssystem) system(nodes.get(entry.getKey()));
|
|
os.nicWireless(true);
|
|
os.setSsid((String) map(entry.getValue()).get("ssid"));
|
|
}
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) {
|
|
Map<String, Object> config = map(entry.getValue());
|
|
DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer();
|
|
server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0"));
|
|
server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0"));
|
|
server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0"));
|
|
server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0"));
|
|
server.setOwnSettings(Boolean.TRUE.equals(config.get("own")));
|
|
for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) {
|
|
server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1));
|
|
}
|
|
server.setAktiv(Boolean.TRUE.equals(config.get("active")));
|
|
servers.add(entry.getKey());
|
|
}
|
|
// A DNS server: installed, its records written to /dns/hosts (TTL 3600), switched on.
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("dnsServers", new HashMap<>())).entrySet()) {
|
|
Map<String, Object> config = map(entry.getValue());
|
|
InternetKnotenBetriebssystem os = system(nodes.get(entry.getKey()));
|
|
DNSServer dns = new DNSServer();
|
|
dns.setSystemSoftware(os);
|
|
os.getInstallierteAnwendungen().put("filius.software.dns.DNSServer", dns);
|
|
for (Object line : list(config.getOrDefault("records", new ArrayList<>()))) {
|
|
String[] parts = ((String) line).split(" ");
|
|
dns.hinzuRecord(parts[0], parts[1], parts[2]);
|
|
}
|
|
dns.setRecursiveResolutionEnabled(Boolean.TRUE.equals(config.get("recursive")));
|
|
dns.setAktiv(true);
|
|
servers.add(entry.getKey());
|
|
}
|
|
for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) {
|
|
system(nodes.get((String) each)).setDHCPKonfiguration(true);
|
|
}
|
|
// A home router's port forwarding: rows of protocol, public port, LAN address, LAN port.
|
|
for (Map.Entry<String, Object> entry : map(scenario.getOrDefault("portForwarding", new HashMap<>())).entrySet()) {
|
|
LinkedList<String[]> rows = new LinkedList<>();
|
|
for (Object row : list(entry.getValue())) rows.add(list(row).stream().map(String::valueOf).toArray(String[]::new));
|
|
((GatewayFirmware) system(nodes.get(entry.getKey()))).setStaticNAT(rows);
|
|
}
|
|
for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) {
|
|
((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true);
|
|
}
|
|
|
|
}
|
|
|
|
/** Starts the simulation; times in the log count from here. */
|
|
void start() throws InterruptedException {
|
|
if (fromFile) {
|
|
// GUIMainMenu.selectMode(MODUS_AKTION): every node in the window's order.
|
|
start = System.currentTimeMillis();
|
|
for (GUIKnotenItem item : GUIContainer.getGUIContainer().getKnotenItems()) item.getKnoten().getSystemSoftware().starten();
|
|
return;
|
|
}
|
|
// Switches and DHCP servers first: FILIUS' client waits for every active
|
|
// server to have started (waitUntilDhcpServerStarted, through the GUI's
|
|
// node list, which this tool has not got).
|
|
// FILIUS' window exists before a simulation starts; created lazily and
|
|
// unsynchronized, the first clients to ask for it would race.
|
|
GUIContainer.getGUIContainer();
|
|
start = System.currentTimeMillis();
|
|
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
|
|
if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten();
|
|
}
|
|
Thread.sleep(200);
|
|
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
|
|
if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten();
|
|
}
|
|
}
|
|
|
|
InternetKnotenBetriebssystem system(String nodeId) {
|
|
return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware();
|
|
}
|
|
|
|
/** Every frame each host's card saw, by node. */
|
|
void writeFrames(Json out) throws Exception {
|
|
Field field = Lauscher.class.getDeclaredField("datenEinheiten");
|
|
field.setAccessible(true);
|
|
@SuppressWarnings("unchecked")
|
|
Map<String, LinkedList<Object[]>> log = (Map<String, LinkedList<Object[]>>) field.get(Lauscher.getLauscher());
|
|
out.key("frames").open('{');
|
|
for (Map.Entry<String, String> entry : macByNode.entrySet()) {
|
|
out.key(entry.getKey()).open('[');
|
|
LinkedList<Object[]> frames = log.get(entry.getValue());
|
|
if (frames != null) {
|
|
synchronized (frames) {
|
|
for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]);
|
|
}
|
|
}
|
|
out.close(']');
|
|
}
|
|
out.close('}');
|
|
writeRows(out, "rows", true);
|
|
writeRows(out, "layeredRows", false);
|
|
if (fromFile) {
|
|
out.key("cards").open('{');
|
|
for (Map.Entry<String, String> entry : cardByMac.entrySet()) out.key(entry.getKey()).string(entry.getValue());
|
|
out.close('}');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The packet capture's table of each card as FILIUS shows it
|
|
* (Lauscher.getDaten): with addresses and ports (`rows`, the data exchange
|
|
* window) or ports only (`layeredRows`, the layered view). Each row: number,
|
|
* source, destination, protocol, layer, comment — its time (the wall clock)
|
|
* left out.
|
|
*/
|
|
void writeRows(Json out, String key, boolean withAddresses) {
|
|
out.key(key).open('{');
|
|
for (Map.Entry<String, String> entry : macByNode.entrySet()) {
|
|
out.key(entry.getKey()).open('[');
|
|
for (Object[] row : Lauscher.getLauscher().getDaten(entry.getValue(), withAddresses, 1)) {
|
|
out.open('[');
|
|
for (int column = 0; column < row.length; column++) {
|
|
if (column == 1) continue;
|
|
out.string(row[column] == null ? null : String.valueOf(row[column]));
|
|
}
|
|
out.close(']');
|
|
}
|
|
out.close(']');
|
|
}
|
|
out.close('}');
|
|
}
|
|
|
|
/** Each host's address, mask, gateway and DNS server. */
|
|
void writeDevices(Json out) {
|
|
out.key("devices").open('{');
|
|
for (Map.Entry<String, Knoten> entry : nodes.entrySet()) {
|
|
if (!(entry.getValue() instanceof InternetKnoten)) continue;
|
|
NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0);
|
|
out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway())
|
|
.string(nic.getDns()).close(']');
|
|
}
|
|
out.close('}');
|
|
}
|
|
|
|
void stop() {
|
|
for (Knoten knoten : nodes.values()) {
|
|
try {
|
|
knoten.getSystemSoftware().beenden();
|
|
} catch (Exception e) {}
|
|
}
|
|
}
|
|
|
|
static void write(String path, Json out) throws Exception {
|
|
try (Writer writer = new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)) {
|
|
writer.write(out.toString());
|
|
writer.write('\n');
|
|
}
|
|
}
|
|
|
|
private static void frame(Json out, long time, EthernetFrame frame) {
|
|
out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination")
|
|
.string(frame.getZielMacAdresse());
|
|
Object data = frame.getDaten();
|
|
if (data instanceof ArpPaket) {
|
|
ArpPaket arp = (ArpPaket) data;
|
|
out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp")
|
|
.string(arp.getTargetIP());
|
|
} else if (data instanceof IcmpPaket) {
|
|
IcmpPaket icmp = (IcmpPaket) data;
|
|
out.key("icmp").number(icmp.getIcmpType()).key("icmpCode").number(icmp.getIcmpCode()).key("sourceIp")
|
|
.string(icmp.getSender()).key("destinationIp").string(icmp.getEmpfaenger()).key("sequence")
|
|
.number(icmp.getSeqNr());
|
|
} else if (data instanceof IpPaket) {
|
|
IpPaket ip = (IpPaket) data;
|
|
Segment segment = ip.getSegment();
|
|
out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol")
|
|
.number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort")
|
|
.number(segment.getZielPort()).key("payload").string(segment.getDaten());
|
|
if (segment instanceof TcpSegment) {
|
|
TcpSegment tcp = (TcpSegment) segment;
|
|
String flags = (tcp.isSyn() ? "S" : "") + (tcp.isAck() ? "A" : "") + (tcp.isFin() ? "F" : "")
|
|
+ (tcp.isPush() ? "P" : "") + (tcp.isRst() ? "R" : "");
|
|
out.key("tcpFlags").string(flags).key("sequence").number(tcp.getSeqNummer()).key("acknowledgement")
|
|
.number(tcp.getAckNummer());
|
|
}
|
|
}
|
|
out.close('}');
|
|
}
|
|
|
|
private static InternetKnotenBetriebssystem system(Knoten knoten) {
|
|
return (InternetKnotenBetriebssystem) knoten.getSystemSoftware();
|
|
}
|
|
|
|
/** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */
|
|
static String stableMac(String portId) {
|
|
String compact = portId.toUpperCase().replace("-", "");
|
|
String suffix = compact.substring(compact.length() - 10);
|
|
StringBuilder mac = new StringBuilder("02");
|
|
for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2);
|
|
return mac.toString();
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private static Map<String, Object> map(Object value) {
|
|
return (Map<String, Object>) value;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private static List<Object> list(Object value) {
|
|
return (List<Object>) value;
|
|
}
|
|
|
|
/** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */
|
|
static final class JsonReader {
|
|
private final String text;
|
|
private int position;
|
|
|
|
JsonReader(String text) {
|
|
this.text = text;
|
|
}
|
|
|
|
Object value() {
|
|
skip();
|
|
char c = text.charAt(position);
|
|
if (c == '{') {
|
|
Map<String, Object> result = new TreeMap<>();
|
|
position++;
|
|
skip();
|
|
if (text.charAt(position) == '}') {
|
|
position++;
|
|
return result;
|
|
}
|
|
for (;;) {
|
|
skip();
|
|
String key = (String) value();
|
|
skip();
|
|
expect(':');
|
|
result.put(key, value());
|
|
skip();
|
|
if (text.charAt(position++) == '}') return result;
|
|
}
|
|
}
|
|
if (c == '[') {
|
|
List<Object> result = new ArrayList<>();
|
|
position++;
|
|
skip();
|
|
if (text.charAt(position) == ']') {
|
|
position++;
|
|
return result;
|
|
}
|
|
for (;;) {
|
|
result.add(value());
|
|
skip();
|
|
if (text.charAt(position++) == ']') return result;
|
|
}
|
|
}
|
|
if (c == '"') {
|
|
StringBuilder result = new StringBuilder();
|
|
position++;
|
|
for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) {
|
|
if (each == '\\') {
|
|
char escaped = text.charAt(position++);
|
|
switch (escaped) {
|
|
case 'n':
|
|
result.append('\n');
|
|
break;
|
|
case 't':
|
|
result.append('\t');
|
|
break;
|
|
case 'u':
|
|
result.append((char) Integer.parseInt(text.substring(position, position + 4), 16));
|
|
position += 4;
|
|
break;
|
|
default:
|
|
result.append(escaped);
|
|
}
|
|
} else {
|
|
result.append(each);
|
|
}
|
|
}
|
|
return result.toString();
|
|
}
|
|
if (text.startsWith("true", position)) {
|
|
position += 4;
|
|
return Boolean.TRUE;
|
|
}
|
|
if (text.startsWith("false", position)) {
|
|
position += 5;
|
|
return Boolean.FALSE;
|
|
}
|
|
if (text.startsWith("null", position)) {
|
|
position += 4;
|
|
return null;
|
|
}
|
|
int begin = position;
|
|
while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++;
|
|
return Double.parseDouble(text.substring(begin, position));
|
|
}
|
|
|
|
private void skip() {
|
|
while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++;
|
|
}
|
|
|
|
private void expect(char c) {
|
|
if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1));
|
|
}
|
|
}
|
|
|
|
/** A JSON writer with commas and indentation. */
|
|
static final class Json {
|
|
private final StringBuilder text = new StringBuilder();
|
|
private final LinkedList<Boolean> first = new LinkedList<>();
|
|
private boolean afterKey;
|
|
|
|
private void separate() {
|
|
if (afterKey) {
|
|
afterKey = false;
|
|
return;
|
|
}
|
|
if (first.isEmpty()) return;
|
|
if (!first.peek()) text.append(',');
|
|
first.pop();
|
|
first.push(false);
|
|
text.append('\n');
|
|
for (int i = 0; i < first.size(); i++) text.append(" ");
|
|
}
|
|
|
|
Json open(char bracket) {
|
|
separate();
|
|
text.append(bracket);
|
|
first.push(true);
|
|
return this;
|
|
}
|
|
|
|
Json close(char bracket) {
|
|
boolean empty = first.pop();
|
|
if (!empty) {
|
|
text.append('\n');
|
|
for (int i = 0; i < first.size(); i++) text.append(" ");
|
|
}
|
|
text.append(bracket);
|
|
return this;
|
|
}
|
|
|
|
Json key(String key) {
|
|
separate();
|
|
quote(key);
|
|
text.append(": ");
|
|
afterKey = true;
|
|
return this;
|
|
}
|
|
|
|
Json string(String value) {
|
|
separate();
|
|
if (value == null) text.append("null");
|
|
else quote(value);
|
|
return this;
|
|
}
|
|
|
|
Json number(long value) {
|
|
separate();
|
|
text.append(value);
|
|
return this;
|
|
}
|
|
|
|
private void quote(String value) {
|
|
text.append('"');
|
|
for (char c : value.toCharArray()) {
|
|
if (c == '"' || c == '\\') text.append('\\').append(c);
|
|
else if (c == '\n') text.append("\\n");
|
|
else if (c < 0x20) text.append(String.format("\\u%04x", (int) c));
|
|
else text.append(c);
|
|
}
|
|
text.append('"');
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return text.toString();
|
|
}
|
|
}
|
|
}
|