diff --git a/CHANGES.md b/CHANGES.md index 622febd..1e64422 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,8 +30,12 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | --- | --- | --- | | `src/zip.ts` | `Persistence/TopologyFLSArchive.swift` (Fehler, Grenzen, Pfadregeln, ZIP-Leser/-Schreiber, CRC-32) | übertragen; DEFLATE über `fflate`, mit Apples Abschneide-Verhalten nachgebildet; Pfadfaltung (NFC, Breite, volles Case-Folding) an 41 Paaren gegen das Swift-Orakel gemessen | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | -| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte mit `tools/java-oracle` gemessen | +| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | +| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | +| `src/topology.ts` | Java `SzenarioVerwaltung.netzwerkLaden`, Konstruktoren und Setter der Geräte; Swift `Persistence/TopologyProjectStore.swift` (Parser) als Vergleich | neu nach Java statt Übertragung: Geräteart aus der Klasse statt aus der Beschriftung, Geräte ohne Koordinaten bei 0,0 statt verworfen, Werte durch Javas Setter | | `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java | +| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt | +| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | | `test/zip.test.ts` | `TopologyProjectPersistenceTests.swift` (Archiv-Ablehnungstests, `makeMinimalZIP`, `makePayloadZIP`) | übertragen, ergänzt um DEFLATE-Grenzfälle | | `test/corpus.test.ts` | — | neu: 23 Korpus-Dateien, gegengeprüft mit `fflate.unzipSync` | | `test/pathFolding.test.ts`, `test/path-folding.*.json` | — | neu: welche Pfade dieselbe Datei meinen, Ergebnis aus dem Swift-Kern | diff --git a/corpus/corpus.java.json b/corpus/corpus.java.json index 5d1457e..ce82209 100644 --- a/corpus/corpus.java.json +++ b/corpus/corpus.java.json @@ -12,6 +12,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.12", @@ -93,6 +95,8 @@ 240, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "160.10.20.10", @@ -113,6 +117,8 @@ 68, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "8.8.8.8", @@ -133,6 +139,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -558,6 +566,8 @@ 149, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.12", @@ -578,6 +588,8 @@ 98, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.10", @@ -598,6 +610,8 @@ 90, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.11", @@ -618,6 +632,8 @@ 120, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.13", @@ -675,6 +691,8 @@ 138, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.14", @@ -695,6 +713,8 @@ 177, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.15", @@ -888,6 +908,8 @@ 134, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.2.10", @@ -908,6 +930,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.10", @@ -928,6 +952,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.11", @@ -1048,6 +1074,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.10.20.11", @@ -1105,6 +1133,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.10.20.12", @@ -1343,6 +1373,8 @@ 111, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "8.8.8.8", @@ -1363,6 +1395,8 @@ 246, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "120.130.140.80", @@ -1383,6 +1417,8 @@ 233, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "130.100.200.25", @@ -1403,6 +1439,8 @@ 264, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "140.100.200.25", @@ -1423,6 +1461,8 @@ 185, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "10.0.0.1", @@ -1470,6 +1510,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.1.11", @@ -1490,6 +1532,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.13", @@ -1510,6 +1554,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.11", @@ -1530,6 +1576,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.14", @@ -1550,6 +1598,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.12", @@ -1661,6 +1711,8 @@ 189, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "43.0.0.2", @@ -2339,6 +2391,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.10.20.11", @@ -2396,6 +2450,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.10.20.12", @@ -2634,6 +2690,8 @@ 111, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "8.8.8.8", @@ -2654,6 +2712,8 @@ 236, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "120.130.140.80", @@ -2674,6 +2734,8 @@ 223, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "130.100.200.25", @@ -2694,6 +2756,8 @@ 254, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "140.100.200.25", @@ -2714,6 +2778,8 @@ 207, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "10.0.0.1", @@ -2761,6 +2827,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.1.11", @@ -2781,6 +2849,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.11", @@ -2801,6 +2871,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.14", @@ -2821,6 +2893,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.13", @@ -2841,6 +2915,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.50.50.12", @@ -2952,6 +3028,8 @@ 184, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "43.0.0.2", @@ -3588,6 +3666,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.12", @@ -3669,6 +3749,8 @@ 240, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "160.10.20.10", @@ -3689,6 +3771,8 @@ 68, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "8.8.8.8", @@ -3709,6 +3793,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -4097,6 +4183,8 @@ 87, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -4117,6 +4205,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.1", @@ -4174,6 +4264,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.2", @@ -4241,6 +4333,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -4261,6 +4355,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.101", @@ -4281,6 +4377,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -4338,6 +4436,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -4385,6 +4485,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -4515,6 +4617,8 @@ 149, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.12", @@ -4535,6 +4639,8 @@ 98, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.10", @@ -4555,6 +4661,8 @@ 90, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.11", @@ -4575,6 +4683,8 @@ 120, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.13", @@ -4632,6 +4742,8 @@ 138, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.14", @@ -4652,6 +4764,8 @@ 177, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.15", @@ -4771,6 +4885,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -4791,6 +4907,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.12", @@ -4811,6 +4929,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.13", @@ -4831,6 +4951,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.14", @@ -4851,6 +4973,8 @@ 67, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -4908,6 +5032,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.59.98", @@ -4928,6 +5054,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.149.102", @@ -4948,6 +5076,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.43.216", @@ -4968,6 +5098,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.19.11", @@ -5213,11 +5345,13 @@ 104, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.231", "mask": "255.255.255.128", - "mac": null, + "mac": "1A:77:96:CF:32:33", "gateway": "", "dns": "141.99.50.230" } @@ -5233,11 +5367,13 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.183", "mask": "255.255.255.128", - "mac": null, + "mac": "5F:A4:08:F1:FC:88", "gateway": "", "dns": "141.99.50.230" } @@ -5253,11 +5389,13 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.184", "mask": "255.255.255.128", - "mac": null, + "mac": "9D:8C:F6:C9:98:AE", "gateway": "", "dns": "141.99.50.230" } @@ -5273,11 +5411,13 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.181", "mask": "255.255.255.128", - "mac": null, + "mac": "A8:13:4D:62:BF:61", "gateway": "", "dns": "141.99.50.230" } @@ -5293,11 +5433,13 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.182", "mask": "255.255.255.128", - "mac": null, + "mac": "FD:92:36:FB:99:6D", "gateway": "", "dns": "141.99.50.230" } @@ -5350,11 +5492,13 @@ 101, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.230", "mask": "255.255.255.128", - "mac": null, + "mac": "9D:26:F9:23:B2:F9", "gateway": "", "dns": "" } @@ -5370,11 +5514,13 @@ 172, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.232", "mask": "255.255.255.128", - "mac": null, + "mac": "37:DF:DC:1A:CF:E0", "gateway": "", "dns": "141.99.50.230" } @@ -5390,11 +5536,13 @@ 183, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.233", "mask": "255.255.255.128", - "mac": null, + "mac": "7A:10:61:61:15:A3", "gateway": "", "dns": "141.99.50.230" } @@ -5721,6 +5869,8 @@ 134, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.2.10", @@ -5741,6 +5891,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.10", @@ -5761,6 +5913,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.11", @@ -5781,6 +5935,8 @@ 112, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.3.10", @@ -5801,6 +5957,8 @@ 131, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.10", @@ -5821,6 +5979,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.4.10", @@ -5878,6 +6038,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.5.11", @@ -6075,6 +6237,8 @@ 54, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.183", @@ -6095,6 +6259,8 @@ 287, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.184", @@ -6115,6 +6281,8 @@ 48, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.181", @@ -6172,6 +6340,8 @@ 201, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.230", @@ -6192,6 +6362,8 @@ 271, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.232", @@ -6212,6 +6384,8 @@ 282, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.50.233", @@ -6378,6 +6552,8 @@ 112, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -6398,6 +6574,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -6480,6 +6658,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.1", @@ -6500,6 +6680,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -6520,6 +6702,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.20", @@ -6651,6 +6835,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -6671,6 +6857,8 @@ 92, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "10.0.0.100", @@ -6937,6 +7125,8 @@ 101, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.2.10", @@ -6957,6 +7147,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -6977,6 +7169,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.101", @@ -6997,6 +7191,8 @@ 213, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.4.10", @@ -7017,6 +7213,8 @@ 198, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -7074,6 +7272,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -7590,6 +7790,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -7610,6 +7812,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -7694,6 +7898,8 @@ 100, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "141.99.2.10", @@ -7964,6 +8170,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -7984,6 +8192,8 @@ 108, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.12", @@ -8068,6 +8278,8 @@ 301, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "42.0.0.2", @@ -8122,6 +8334,8 @@ 236, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "120.130.140.10", @@ -8142,6 +8356,8 @@ 111, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "8.8.8.8", @@ -8516,6 +8732,8 @@ 116, 19 ], + "useIpAsName": true, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -8536,6 +8754,8 @@ 90, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.10", @@ -8701,6 +8921,8 @@ 134, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.2.10", @@ -8721,6 +8943,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.10", @@ -8741,6 +8965,8 @@ 123, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "141.99.1.11", @@ -8861,6 +9087,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.11", @@ -8881,6 +9109,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.12", @@ -8901,6 +9131,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.13", @@ -8921,6 +9153,8 @@ 91, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.14", @@ -8941,6 +9175,8 @@ 67, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "192.168.0.100", @@ -8998,6 +9234,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.59.98", @@ -9018,6 +9256,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.149.102", @@ -9038,6 +9278,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.43.216", @@ -9058,6 +9300,8 @@ 102, 19 ], + "useIpAsName": false, + "useMacAsName": false, "ports": [ { "ip": "169.254.19.11", diff --git a/packages/fls/src/index.ts b/packages/fls/src/index.ts index 5adcc9b..8e53cd7 100644 --- a/packages/fls/src/index.ts +++ b/packages/fls/src/index.ts @@ -1,4 +1,6 @@ // Public surface of the .fls codec. export * from './archive.js'; +export * from './javaIp.js'; export * from './javabeans.js'; +export * from './topology.js'; export * from './zip.js'; diff --git a/packages/fls/src/javaDefaults.json b/packages/fls/src/javaDefaults.json index dba9c77..c181f20 100644 --- a/packages/fls/src/javaDefaults.json +++ b/packages/fls/src/javaDefaults.json @@ -11,6 +11,7 @@ "RIPServer#port": 520, "Rechner#maxLayerOfOperation": 4, "ServerBaustein#port": 55555, + "SwingConstants.CENTER": 0, "ThreadGroup#maxPriority": 10, "Vermittlungsrechner#maxLayerOfOperation": 2, "WebServer#port": 80 diff --git a/packages/fls/src/javaIp.ts b/packages/fls/src/javaIp.ts new file mode 100644 index 0000000..94e03b5 --- /dev/null +++ b/packages/fls/src/javaIp.ts @@ -0,0 +1,65 @@ +// What Java FILIUS stores when a file sets an interface's IP address or mask: +// NetzwerkInterface.setIp(ip) keeps IP.ipCheck(ip), which is null for text +// that is not an address and the canonical dotted quad otherwise. +// Java counterpart: filius.software.vermittlungsschicht.IP.inetAton/ipCheck. +// +// Measured against Java (test/ip-check.cases.txt, tools/java-oracle), since +// the rules are wider than they look: +// - StringTokenizer(".") skips empty tokens: "10..0.0.1" is 10.0.0.1; +// - Integer.parseInt takes a sign and leading zeros: "+010" is 10, "-0" is 0; +// - and any Unicode decimal digit in the BMP: Arabic-Indic "١٠" is 10 — +// parseInt reads UTF-16 units, so "𝟏" (a surrogate pair) is no digit. + +/** + * `Integer.parseInt(text)`: an optional sign, then one or more decimal digits + * of any script; undefined where Java throws NumberFormatException. + */ +export function javaParseInt(text: string): number | undefined { + const characters = text.split(''); + let index = 0; + let negative = false; + if (characters[0] === '-' || characters[0] === '+') { + negative = characters[0] === '-'; + index = 1; + } + if (index >= characters.length) return undefined; + let value = 0; + for (; index < characters.length; index++) { + const digit = decimalDigitValue(characters[index]!); + if (digit === undefined) return undefined; + value = value * 10 + digit; + // Past the int range Java throws; the octet check would reject it anyway. + if (value > 2 ** 31) return undefined; + } + if (!negative && value > 2 ** 31 - 1) return undefined; + return negative ? -value : value; +} + +/** + * `Character.digit(c, 10)` for one UTF-16 unit: Unicode lays out every + * script's digits as a run of ten from zero to nine, so the value is the + * distance from the start of the run, modulo ten (in case two runs touch). + */ +function decimalDigitValue(unit: string): number | undefined { + if (!/^\p{Nd}$/u.test(unit)) return undefined; + let code = unit.charCodeAt(0); + let offset = 0; + while (/^\p{Nd}$/u.test(String.fromCharCode(code - 1))) { + code -= 1; + offset += 1; + } + return offset % 10; +} + +/** `IP.ipCheck(text)`: four octets 0…255 as `a.b.c.d`, or null. */ +export function javaIpCheck(text: string): string | null { + const tokens = text.split('.').filter((token) => token !== ''); + if (tokens.length !== 4) return null; + const octets: number[] = []; + for (const token of tokens) { + const octet = javaParseInt(token); + if (octet === undefined || octet < 0 || octet > 255) return null; + octets.push(octet); + } + return octets.join('.'); +} diff --git a/packages/fls/src/javabeans.ts b/packages/fls/src/javabeans.ts index 4e604d9..f8304d9 100644 --- a/packages/fls/src/javabeans.ts +++ b/packages/fls/src/javabeans.ts @@ -9,11 +9,12 @@ // cannot follow values XMLEncoder shares by reference (plan §13, M2): // - `id`/`idref` resolve anywhere, including the results of getters; // - a getter on a property nobody set answers the value Java would, from -// measured defaults (javaDefaults.json), or an explicit "unresolved". +// measured defaults (javaDefaults.json), or an explicit "unresolved"; +// - static constants (`SwingConstants.CENTER`) are their measured values. // // XMLDecoder's rules, as far as XMLEncoder uses them: // args… statements… new C(args), then the statements -// the static field C.F +// the static field C.F (measured value if known) // args the static factory C.m(args) // the value stored under id X // target.getP() (a value) @@ -46,8 +47,12 @@ export interface JavaObject { readonly properties: Map; /** Method calls in order (`add`, `put`, …), with their arguments. */ readonly calls: { method: string; args: JavaValue[] }[]; - /** `set(i, v)` on the object. */ - readonly indexed: Map; + /** + * Elements the object already held before the file's statements — a list + * its constructor filled, reached by `` on a getter's + * result. `add`/`set` go to `calls`, in order. + */ + readonly presetElements: Map; /** True when the object was not in the file but returned by a getter. */ readonly fromGetter: boolean; } @@ -97,14 +102,40 @@ export function simpleClassName(className: string): string { return className.slice(className.lastIndexOf('.') + 1); } -/** The elements of a list: `add` calls, then `set(i, v)` replacing by index. */ -export function listItems(value: JavaValue | undefined): JavaValue[] { +/** + * The elements of a list: the ones it held before the file's statements, + * then `add`, `add(i, v)` and `set(i, v)` replayed in order. + * + * `constructed` is how many elements the list's constructor made — a Java + * `Switch` starts with 24 ports, a `Gateway` with two interfaces. The file + * only mentions those it changed; the rest come back as untouched objects. + */ +export function listItems(value: JavaValue | undefined, constructed = 0): JavaValue[] { if (typeof value === 'object' && value !== null && (value as JavaArray).kind === 'array') { return [...(value as JavaArray).items]; } if (!isJavaObject(value)) return []; - const items = value.calls.filter((call) => call.method === 'add').map((call) => call.args.at(-1) ?? null); - for (const [index, item] of value.indexed) items[index] = item; + const items = replayList(value, constructed); + return Array.from(items, (item) => item ?? newObject({ className: 'unknown', fromGetter: true })); +} + +/** Like listItems, with `undefined` for a constructed element nobody touched. */ +function replayList(value: JavaObject, constructed: number): (JavaValue | undefined)[] { + const presetLength = Math.max(constructed, ...Array.from(value.presetElements.keys(), (index) => index + 1)); + const items: (JavaValue | undefined)[] = Array.from({ length: presetLength }, (_, index) => + value.presetElements.get(index), + ); + for (const { method, args } of value.calls) { + if (method === 'add' && args.length === 1) { + items.push(args[0]!); + } else if (method === 'add' && args.length === 2 && typeof args[0] === 'number') { + items.splice(args[0], 0, args[1]!); + } else if (method === 'set' && args.length === 2 && typeof args[0] === 'number') { + items[args[0]] = args[1]!; + } else if (method === 'clear' && args.length === 0) { + items.length = 0; + } + } return items; } @@ -194,7 +225,7 @@ function newObject(fields: Partial & { className: string }): JavaObj factoryMethod: fields.factoryMethod, properties: new Map(), calls: [], - indexed: new Map(), + presetElements: new Map(), fromGetter: fields.fromGetter ?? false, }; } @@ -297,6 +328,12 @@ class Decoder { } const className = attributes.class; if (className === undefined) throw new JavaBeansError(' without class or idref.'); + if (attributes.field !== undefined) { + // A static constant: XMLEncoder writes a shared Integer 0 as + // SwingConstants.CENTER when that was the first 0 it met. + const constantKey = `${simpleClassName(className)}.${attributes.field}`; + if (Object.hasOwn(DEFAULTS, constantKey)) return this.primitive(element, DEFAULTS[constantKey]!); + } const args = element.children.filter((child) => child.name !== 'void'); const object = newObject({ className, @@ -342,7 +379,7 @@ class Decoder { : this.setProperty(target, attributes.property, args[0]!); } else if (attributes.index !== undefined) { const index = Number.parseInt(attributes.index, 10); - value = args.length === 0 ? this.getIndex(target, index) : this.setIndex(target, index, args[0]!); + value = args.length === 0 ? this.getIndex(target, index, attributes.id) : this.setIndex(target, index, args[0]!); } else if (attributes.method !== undefined) { value = this.call(target, attributes.method, args, attributes.id); } else if (attributes.idref !== undefined) { @@ -374,7 +411,7 @@ class Decoder { } if (isJavaObject(target)) { target.calls.push({ method, args }); - if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0]); + if (method === 'get' && args.length === 1 && typeof args[0] === 'number') return this.getIndex(target, args[0], id); } return null; } @@ -405,17 +442,24 @@ class Decoder { return placeholder; } - private getIndex(target: JavaValue | undefined, index: number): JavaValue { + private getIndex(target: JavaValue | undefined, index: number, id: string | undefined): JavaValue { if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') { return (target as JavaArray).items[index] ?? null; } if (!isJavaObject(target)) return { kind: 'unresolved', reason: `index ${index} on a value that is not an object` }; - const existing = listItems(target)[index]; + const items = replayList(target, 0); + const existing = items[index]; if (existing !== undefined) return existing; - // A list the file never filled, like a node's interface list created by - // its constructor: the element is a placeholder that later reads return. - const placeholder = newObject({ className: 'unknown', fromGetter: true }); - target.indexed.set(index, placeholder); + // An element the list held before the file touched it, like a node's + // interface list created by its constructor: a placeholder that later + // reads return. XMLEncoder writes these before any `add`; after one, the + // index could mean either, and this reader does not guess. + const changed = target.calls.some((call) => call.method === 'add' || call.method === 'set' || call.method === 'clear'); + if (changed && index >= items.length) { + return { kind: 'unresolved', reason: `index ${index} after the list was changed` }; + } + const placeholder = newObject({ className: id?.replace(/\d+$/, '') ?? 'unknown', id, fromGetter: true }); + target.presetElements.set(index, placeholder); return placeholder; } @@ -423,7 +467,7 @@ class Decoder { if (typeof target === 'object' && target !== null && (target as JavaArray).kind === 'array') { (target as JavaArray).items[index] = value; } else if (isJavaObject(target)) { - target.indexed.set(index, value); + target.calls.push({ method: 'set', args: [index, value] }); } return null; } diff --git a/packages/fls/src/topology.ts b/packages/fls/src/topology.ts new file mode 100644 index 0000000..2f234fd --- /dev/null +++ b/packages/fls/src/topology.ts @@ -0,0 +1,408 @@ +// The network drawn in a .fls file — devices, cables, documentation items — +// read from the decoded JavaBeans graph (javabeans.ts) the way Java FILIUS +// loads it (SzenarioVerwaltung.netzwerkLaden). Only the topology: system +// software, file systems and applications are later layers, which start +// from `FlsNode.source`. +// +// The result is Java's view, indexed like Java's lists, and is checked +// against Java itself (corpus.java.json). The editor model (node kinds, UUIDs, +// ports as Swift's TopologyPortMetadata, runtime configurations) is built +// from it in a separate step. +// +// Where this departs from filius-ipad's reader, Java is right (plan §13, M2): +// - the device kind comes from the Java class; the palette label ("Heimrouter") +// is not file data — Java overwrites it with the class's type name; +// - a device without coordinates stands at 0,0, as in Java, instead of +// being dropped; +// - interface values go through Java's setters (IP.ipCheck for address and +// mask, upper-cased MAC). + +import { isJavaObject, isUnresolved, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js'; +import { javaIpCheck } from './javaIp.js'; + +export type FlsNodeClass = 'Rechner' | 'Notebook' | 'Switch' | 'Vermittlungsrechner' | 'Gateway' | 'Modem'; + +export interface FlsInterface { + /** After IP.ipCheck: a canonical dotted quad, or null for text that is not one. */ + ip: string | null; + mask: string | null; + /** Upper-cased, as getMac() returns it; undefined when the file has none (Java rolls a random one). */ + mac: string | undefined; + gateway: string | null; + dns: string | null; +} + +export interface FlsNode { + javaClass: FlsNodeClass; + name: string | null; + x: number; + y: number; + /** Hosts only (Rechner, Notebook); false for the others. */ + useIpAsName: boolean; + useMacAsName: boolean; + /** The interfaces of a Rechner, Notebook, Vermittlungsrechner or Gateway; empty for the others. */ + interfaces: FlsInterface[]; + /** Interfaces, or the ports of a Switch or Modem. */ + portCount: number; + /** The node's JavaBean (`filius.hardware.knoten.*`), for the software layers. */ + source: JavaObject; +} + +/** A cable end: the node's index and the port's index on it, as in Java's lists. */ +export interface FlsPortReference { + node: number; + port: number; +} + +export interface FlsLink { + ends: [FlsPortReference, FlsPortReference]; + wireless: boolean; +} + +export type FlsColor = [red: number, green: number, blue: number, alpha: number]; + +export interface FlsFont { + name: string; + size: number; + style: number; +} + +export interface FlsDocumentationItem { + /** GUIDocuItem.RECT = 1, TEXT = 2; 0 when the file sets none. */ + type: number; + text: string | null; + x: number; + y: number; + width: number; + height: number; + color: FlsColor | null; + font: FlsFont | null; +} + +export interface FlsTopology { + /** The version line newer files start with ("Filius version: 1.7.4 (…)"), else null. */ + versionLine: string | null; + nodes: FlsNode[]; + links: FlsLink[]; + documentation: FlsDocumentationItem[]; + /** What Java would read and this reader could not, and what it left out. */ + warnings: string[]; +} + +interface NodeClassInfo { + /** Elements the constructor creates: interfaces (InternetKnoten) or ports (LokalerKnoten). */ + constructed: number; + local: boolean; + host: boolean; + /** + * The name the constructor gives, in German — the locale Java FILIUS starts + * in (and the one corpus.java.json was measured with). An English FILIUS + * would say "Computer" or "Router" here. + */ + defaultName: string; +} + +const NODE_CLASSES: Record = { + Rechner: { constructed: 1, local: false, host: true, defaultName: 'Rechner' }, + Notebook: { constructed: 1, local: false, host: true, defaultName: 'Notebook' }, + Vermittlungsrechner: { + constructed: 1, + local: false, + host: false, + defaultName: 'Vermittlungsrechner', + }, + Gateway: { constructed: 2, local: false, host: false, defaultName: 'Heimrouter' }, + Switch: { constructed: 24, local: true, host: false, defaultName: 'Switch / WLAN' }, + Modem: { constructed: 1, local: true, host: false, defaultName: 'Modem' }, +}; + +const DEVICE_PACKAGE = 'filius.hardware.knoten.'; + +function isFlsNodeClass(name: string): name is FlsNodeClass { + return Object.hasOwn(NODE_CLASSES, name); +} + +/** Maps the top-level objects of `konfiguration.xml` to the topology. */ +export function readTopology(values: readonly JavaValue[]): FlsTopology { + const warnings: string[] = []; + const context = new Reader(warnings); + + // netzwerkLaden: an optional version string, then the node list, the + // cable list and the documentation list, each by position. A list counts + // only if its first element has the expected class. + let position = 0; + let versionLine: string | null = null; + if (typeof values[0] === 'string') { + versionLine = values[0]; + position = 1; + } + const nodeItems = listOf(values[position], 'filius.gui.netzwerksicht.GUIKnotenItem'); + const cableItems = listOf(values[position + 1], 'filius.gui.netzwerksicht.GUIKabelItem'); + const documentationItems = listOf(values[position + 2], 'filius.gui.netzwerksicht.GUIDocuItem'); + + const nodes: FlsNode[] = []; + for (const [index, item] of nodeItems.entries()) { + const node = context.node(item, index, nodes.length); + if (node !== undefined) nodes.push(node); + } + const links: FlsLink[] = []; + for (const [index, item] of cableItems.entries()) { + const link = context.link(item, index); + if (link !== undefined) links.push(link); + } + const documentation = documentationItems.flatMap((item) => (isJavaObject(item) ? [context.documentation(item)] : [])); + + return { versionLine, nodes, links, documentation, warnings }; +} + +/** The elements of `value` if it is a list whose first element is an object of `className`. */ +function listOf(value: JavaValue | undefined, className: string): JavaValue[] { + const items = listItems(value); + const first = items[0]; + return isJavaObject(first) && first.className === className ? items : []; +} + +class Reader { + /** Every port object a node owns, to resolve cable ends by identity as Java does. */ + private readonly portOwners = new Map(); + + constructor(private readonly warnings: string[]) {} + + node(item: JavaValue, itemIndex: number, nodeIndex: number): FlsNode | undefined { + const device = isJavaObject(item) ? item.properties.get('knoten') : undefined; + if (!isJavaObject(device)) { + this.warnings.push(`node ${itemIndex + 1}: no device (knoten), left out`); + return undefined; + } + const simpleName = simpleClassName(device.className); + if (!device.className.startsWith(DEVICE_PACKAGE) || !isFlsNodeClass(simpleName)) { + this.warnings.push(`node ${itemIndex + 1}: unknown device class ${device.className}, left out`); + return undefined; + } + const info = NODE_CLASSES[simpleName]; + const [x, y] = this.position(item as JavaObject, itemIndex); + + const node: FlsNode = { + javaClass: simpleName, + name: this.stringProperty(device, 'name', info.defaultName, `node ${itemIndex + 1}`), + x, + y, + useIpAsName: info.host && this.booleanProperty(device, 'useIPAsName', `node ${itemIndex + 1}`), + useMacAsName: info.host && this.booleanProperty(device, 'useMACAsName', `node ${itemIndex + 1}`), + interfaces: [], + portCount: 0, + source: device, + }; + + if (info.local) { + const ports = this.constructedList(device, 'anschluesse', info.constructed); + node.portCount = ports.length; + ports.forEach((port, portIndex) => this.ownPort(port, nodeIndex, portIndex)); + } else { + const networkInterfaces = this.constructedList(device, 'netzwerkInterfaces', info.constructed); + node.interfaces = networkInterfaces.map((nic, nicIndex) => + this.networkInterface(nic, simpleName === 'Gateway' && nicIndex === 0, `node ${itemIndex + 1}`), + ); + node.portCount = networkInterfaces.length; + networkInterfaces.forEach((nic, nicIndex) => { + if (isJavaObject(nic)) this.ownPort(nic.properties.get('port'), nodeIndex, nicIndex); + }); + } + return node; + } + + /** + * A list property the constructor filled: a getter's result keeps the + * constructed elements, a list the file sets replaces them. + */ + private constructedList(device: JavaObject, property: string, constructed: number): JavaValue[] { + const list = device.properties.get(property); + // Untouched: the constructed elements, which carry nothing but defaults. + if (list === undefined) return Array.from({ length: constructed }, () => null); + return listItems(list, isJavaObject(list) && list.fromGetter ? constructed : 0); + } + + private ownPort(port: JavaValue | undefined, node: number, index: number): void { + if (isJavaObject(port) && !this.portOwners.has(port)) this.portOwners.set(port, { node, port: index }); + } + + /** + * The imageLabel's bounds: fields set one by one in newer files, + * `new Rectangle(x, y, width, height)` in older ones. Without either, Java's + * label sits at 0,0. + */ + private position(item: JavaObject, itemIndex: number): [number, number] { + const label = item.properties.get('imageLabel'); + const bounds = isJavaObject(label) ? label.properties.get('bounds') : undefined; + if (!isJavaObject(bounds)) return [0, 0]; + return (['x', 'y'] as const).map((field, index) => { + const value = bounds.properties.get(field) ?? bounds.args[index] ?? 0; + if (typeof value === 'number') return value; + this.warnings.push(`node ${itemIndex + 1}: position ${field} is ${describe(value)}, used 0`); + return 0; + }) as [number, number]; + } + + /** + * NetzwerkInterface as its constructor leaves it, then the file's setters. + * A Gateway's constructor gives its first interface (WAN) its own address. + */ + private networkInterface(nic: JavaValue, gatewayWan: boolean, where: string): FlsInterface { + const result: FlsInterface = { + ip: gatewayWan ? '42.0.0.10' : '192.168.0.10', + mask: gatewayWan ? '255.0.0.0' : '255.255.255.0', + mac: undefined, + gateway: '', + dns: '', + }; + if (!isJavaObject(nic)) return result; + // Only the constructor's own interface gets the WAN address; one the file + // creates with `new NetzwerkInterface()` starts like any other. + if (gatewayWan && !nic.fromGetter) { + result.ip = '192.168.0.10'; + result.mask = '255.255.255.0'; + } + const at = `${where}, interface`; + for (const [property, field] of [ + ['ip', 'ip'], + ['subnetzMaske', 'mask'], + ] as const) { + const value = nic.properties.get(property); + if (value === undefined) continue; + // setIp(null) throws inside Java's setter; XMLDecoder swallows that and + // the constructor's value stays. + if (typeof value === 'string') result[field] = javaIpCheck(value); + else this.unexpected(at, property, value); + } + const mac = nic.properties.get('mac'); + if (typeof mac === 'string') result.mac = mac.toUpperCase(); + else if (mac !== undefined && mac !== null) this.unexpected(at, 'mac', mac); + for (const [property, field] of [ + ['gateway', 'gateway'], + ['dns', 'dns'], + ] as const) { + const value = nic.properties.get(property); + if (value === undefined) continue; + if (typeof value === 'string' || value === null) result[field] = value; + else this.unexpected(at, property, value); + } + return result; + } + + link(item: JavaValue, itemIndex: number): FlsLink | undefined { + const cable = isJavaObject(item) ? item.properties.get('dasKabel') : undefined; + if (!isJavaObject(cable)) { + this.warnings.push(`cable ${itemIndex + 1}: no cable (dasKabel), left out`); + return undefined; + } + // Kabel.setAnschluesse(Port[]) in newer files, new Kabel(port, port) in older ones. + const declared = cable.properties.get('anschluesse'); + const ports = declared === undefined ? cable.args : listItems(declared); + const ends = ports.map((port) => (isJavaObject(port) ? this.portOwners.get(port) : undefined)); + const [first, second] = ends; + if (ends.length !== 2 || first === undefined || second === undefined) { + this.warnings.push(`cable ${itemIndex + 1}: an end is not a port of a device, left out`); + return undefined; + } + return { + ends: [first, second], + wireless: this.booleanProperty(cable, 'wireless', `cable ${itemIndex + 1}`), + }; + } + + documentation(item: JavaObject): FlsDocumentationItem { + const where = 'documentation item'; + const integer = (property: string): number => { + const value = item.properties.get(property); + if (value === undefined) return 0; + if (typeof value === 'number' && Number.isInteger(value)) return value; + this.unexpected(where, property, value); + return 0; + }; + const text = item.properties.get('text'); + return { + type: integer('type'), + text: typeof text === 'string' ? text : null, + x: integer('x'), + y: integer('y'), + width: integer('width'), + height: integer('height'), + color: this.color(item.properties.get('color'), where), + font: this.font(item.properties.get('font'), where), + }; + } + + /** + * `java.awt.Color` as XMLEncoder writes it: `new Color(r, g, b, a)`. Also + * `new Color(rgb)` and `new Color(r, g, b)` (ColorUIResource). A component + * outside 0…255 makes Java's constructor throw — the color stays unset. + */ + private color(value: JavaValue | undefined, where: string): FlsColor | null { + if (value === undefined || value === null) return null; + if (!isJavaObject(value)) { + this.unexpected(where, 'color', value); + return null; + } + const args = value.args; + if (!args.every((arg) => typeof arg === 'number' && Number.isInteger(arg))) { + this.warnings.push(`${where}: color ${value.className} with arguments this reader does not know, left unset`); + return null; + } + const numbers = args as number[]; + if (numbers.length === 1) { + const rgb = numbers[0]!; + return [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff, 255]; + } + if (numbers.length === 3 || numbers.length === 4) { + const rgba = numbers.length === 3 ? [...numbers, 255] : numbers; + if (rgba.every((component) => component >= 0 && component <= 255)) return rgba as FlsColor; + this.warnings.push(`${where}: color component outside 0…255, left unset (as in Java)`); + return null; + } + this.warnings.push(`${where}: color with ${numbers.length} arguments, left unset`); + return null; + } + + /** `new Font(name, style, size)`, also as FontUIResource. Java keeps style 0…3 and makes others 0. */ + private font(value: JavaValue | undefined, where: string): FlsFont | null { + if (value === undefined || value === null) return null; + const [name, style, size] = isJavaObject(value) ? value.args : []; + if ( + !isJavaObject(value) || + value.args.length !== 3 || + (typeof name !== 'string' && name !== null) || + typeof style !== 'number' || + typeof size !== 'number' + ) { + this.unexpected(where, 'font', value); + return null; + } + return { name: name ?? 'Default', size, style: (style & ~0x03) === 0 ? style : 0 }; + } + + private stringProperty(target: JavaObject, property: string, fallback: string, where: string): string | null { + const value = target.properties.get(property); + if (value === undefined) return fallback; + if (typeof value === 'string' || value === null) return value; + this.unexpected(where, property, value); + return fallback; + } + + private booleanProperty(target: JavaObject, property: string, where: string): boolean { + const value = target.properties.get(property); + if (value === undefined) return false; + if (typeof value === 'boolean') return value; + this.unexpected(where, property, value); + return false; + } + + private unexpected(where: string, property: string, value: JavaValue): void { + this.warnings.push(`${where}: ${property} is ${describe(value)}, ignored`); + } +} + +function describe(value: JavaValue): string { + if (isUnresolved(value)) return `unresolved (${value.reason})`; + if (isJavaObject(value)) return `an object of ${value.className}`; + return JSON.stringify(typeof value === 'bigint' ? String(value) : value) ?? String(value); +} diff --git a/packages/fls/test/ip-check.cases.txt b/packages/fls/test/ip-check.cases.txt new file mode 100644 index 0000000..dcb3aa9 --- /dev/null +++ b/packages/fls/test/ip-check.cases.txt @@ -0,0 +1,46 @@ +10.0.0.1 +0.0.0.0 +255.255.255.255 +010.000.000.001 ++10.0.0.1 +-0.0.0.0 +-1.0.0.0 +10..0.0.1 +.10.0.0.1 +10.0.0.1. +...10.0.0.1... +10.0.0 +10.0.0.1.5 +256.0.0.1 + 10.0.0.1 +10.0.0.1 + +. +.... +1e1.0.0.1 +0x10.0.0.1 +10.0.0.1\u0000 +2147483648.0.0.1 +-2147483648.0.0.1 +99999999999999999999.0.0.1 ++.0.0.1 +-.0.0.1 +++1.0.0.1 +1 .0.0.1 +\u0661\u0660.0.0.1 +\u0967.\u0968.\u0969.\u096a +\uff11\uff10.0.0.1 +\u0e51\u0e59\u0e52.168.0.1 +\u1040.\u1049.0.1 +\u07c1.0.0.1 +\u194f.0.0.1 +\u19d9.0.0.1 +\u19da.0.0.1 +\uabf9.0.0.1 +\u0de7.0.0.1 +\ud835\udfcf.0.0.1 +\u00b2.0.0.1 +\u2460.0.0.1 +\u1369.0.0.1 +\uff0e10.0.0.1 +10\u30020.0.1 diff --git a/packages/fls/test/ip-check.java.json b/packages/fls/test/ip-check.java.json new file mode 100644 index 0000000..3aaa2d2 --- /dev/null +++ b/packages/fls/test/ip-check.java.json @@ -0,0 +1,186 @@ +[ + [ + "10.0.0.1", + "10.0.0.1" + ], + [ + "0.0.0.0", + "0.0.0.0" + ], + [ + "255.255.255.255", + "255.255.255.255" + ], + [ + "010.000.000.001", + "10.0.0.1" + ], + [ + "+10.0.0.1", + "10.0.0.1" + ], + [ + "-0.0.0.0", + "0.0.0.0" + ], + [ + "-1.0.0.0", + null + ], + [ + "10..0.0.1", + "10.0.0.1" + ], + [ + ".10.0.0.1", + "10.0.0.1" + ], + [ + "10.0.0.1.", + "10.0.0.1" + ], + [ + "...10.0.0.1...", + "10.0.0.1" + ], + [ + "10.0.0", + null + ], + [ + "10.0.0.1.5", + null + ], + [ + "256.0.0.1", + null + ], + [ + " 10.0.0.1", + null + ], + [ + "10.0.0.1 ", + null + ], + [ + "", + null + ], + [ + ".", + null + ], + [ + "....", + null + ], + [ + "1e1.0.0.1", + null + ], + [ + "0x10.0.0.1", + null + ], + [ + "10.0.0.1\\u0000", + null + ], + [ + "2147483648.0.0.1", + null + ], + [ + "-2147483648.0.0.1", + null + ], + [ + "99999999999999999999.0.0.1", + null + ], + [ + "+.0.0.1", + null + ], + [ + "-.0.0.1", + null + ], + [ + "++1.0.0.1", + null + ], + [ + "1 .0.0.1", + null + ], + [ + "\\u0661\\u0660.0.0.1", + "10.0.0.1" + ], + [ + "\\u0967.\\u0968.\\u0969.\\u096a", + "1.2.3.4" + ], + [ + "\\uff11\\uff10.0.0.1", + "10.0.0.1" + ], + [ + "\\u0e51\\u0e59\\u0e52.168.0.1", + "192.168.0.1" + ], + [ + "\\u1040.\\u1049.0.1", + "0.9.0.1" + ], + [ + "\\u07c1.0.0.1", + "1.0.0.1" + ], + [ + "\\u194f.0.0.1", + "9.0.0.1" + ], + [ + "\\u19d9.0.0.1", + "9.0.0.1" + ], + [ + "\\u19da.0.0.1", + null + ], + [ + "\\uabf9.0.0.1", + "9.0.0.1" + ], + [ + "\\u0de7.0.0.1", + "1.0.0.1" + ], + [ + "\\ud835\\udfcf.0.0.1", + null + ], + [ + "\\u00b2.0.0.1", + null + ], + [ + "\\u2460.0.0.1", + null + ], + [ + "\\u1369.0.0.1", + null + ], + [ + "\\uff0e10.0.0.1", + null + ], + [ + "10\\u30020.0.1", + null + ] +] diff --git a/packages/fls/test/javaIp.test.ts b/packages/fls/test/javaIp.test.ts new file mode 100644 index 0000000..b44cc03 --- /dev/null +++ b/packages/fls/test/javaIp.test.ts @@ -0,0 +1,39 @@ +// IP.ipCheck as Java FILIUS runs it: every case in ip-check.cases.txt was run +// through Java (ip-check.java.json, tools/java-oracle) and must come out the +// same here. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { javaIpCheck, javaParseInt } from '../src/javaIp.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const measured = JSON.parse(readFileSync(join(here, 'ip-check.java.json'), 'utf8')) as [string, string | null][]; + +/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */ +function unescape(line: string): string { + return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))); +} + +describe('javaIpCheck against Java', () => { + it('covers every line of the cases file', () => { + const lines = readFileSync(join(here, 'ip-check.cases.txt'), 'ascii').split('\n').slice(0, -1); + expect(measured.map(([line]) => line)).toEqual(lines); + }); + + for (const [line, expected] of measured) { + it(JSON.stringify(line), () => { + expect(javaIpCheck(unescape(line))).toBe(expected); + }); + } +}); + +describe('javaParseInt', () => { + it('keeps to the int range', () => { + expect(javaParseInt('2147483647')).toBe(2147483647); + expect(javaParseInt('-2147483648')).toBe(-2147483648); + expect(javaParseInt('2147483648')).toBeUndefined(); + expect(javaParseInt('-2147483649')).toBeUndefined(); + }); +}); diff --git a/packages/fls/test/javabeans.test.ts b/packages/fls/test/javabeans.test.ts index ba58a59..d095d07 100644 --- a/packages/fls/test/javabeans.test.ts +++ b/packages/fls/test/javabeans.test.ts @@ -126,18 +126,93 @@ describe('XMLDecoder rules', () => { it('reads static fields, static factories, maps and arrays', () => { const [constant, manager, map, array] = decodeJavaBeans( - doc(` + doc(` k1 b`), ); - expect(object(constant)).toMatchObject({ className: 'javax.swing.SwingConstants', staticField: 'CENTER' }); + expect(object(constant)).toMatchObject({ className: 'javax.swing.SwingConstants', staticField: 'LEFT' }); expect(object(manager).factoryMethod).toBe('sharedInstance'); expect(mapEntries(map)).toEqual([['k', 1]]); expect(listItems(array)).toEqual([null, 'b']); }); + it('answers a measured static constant with its value, which later references share', () => { + const [color, holder] = decodeJavaBeans( + doc(` + + 128255 + + `), + ); + + expect(object(color).args).toEqual([0, 128, 255]); + expect(listItems(holder)).toEqual([0]); + }); + + it('keeps the elements a constructor made ahead of the ones the file adds', () => { + // A Java router starts with one interface; the file changes it by index + // and adds two more — three interfaces, in this order. + const [node] = decodeJavaBeans( + doc(` + + 10.0.0.1 + + + + `), + ); + + const interfaces = listItems(object(node).properties.get('netzwerkInterfaces')); + expect(interfaces.map((nic) => object(nic).id)).toEqual(['NetzwerkInterface0', 'NetzwerkInterface1', 'NetzwerkInterface2']); + expect(object(interfaces[0]).properties.get('ip')).toBe('10.0.0.1'); + }); + + it('fills the constructed elements the file never mentions, in index order', () => { + // A switch has 24 ports; the file names the three that carry a cable, + // in any order. + const [ports] = decodeJavaBeans( + doc(` + + `), + ); + + const list = object(ports).properties.get('anschluesse'); + const items = listItems(list, 24); + expect(items).toHaveLength(24); + expect(items.slice(0, 3).map((port) => object(port).id)).toEqual(['Port10', 'Port9', 'Port11']); + expect(object(items[3])).toMatchObject({ className: 'unknown', fromGetter: true }); + expect(listItems(list)).toHaveLength(3); + }); + + it('replays add at an index and set by index in order', () => { + const [list] = decodeJavaBeans( + doc(` + a + c + 1b + C + `), + ); + + expect(listItems(list)).toEqual(['a', 'b', 'C']); + }); + + it('does not guess what an index means once the list was changed', () => { + const [, holder] = decodeJavaBeans( + doc(` + + + + + + `), + ); + + expect(isUnresolved(listItems(holder)[0])).toBe(true); + }); + it('reports an idref to an id that was never defined', () => { const [value] = decodeJavaBeans(doc('')); diff --git a/packages/fls/test/topology.test.ts b/packages/fls/test/topology.test.ts new file mode 100644 index 0000000..f18300e --- /dev/null +++ b/packages/fls/test/topology.test.ts @@ -0,0 +1,196 @@ +// The topology of every corpus file must be what Java FILIUS loads +// (corpus.java.json, tools/java-oracle): each device with class, name, +// position and interfaces, each cable with both ends, each documentation +// item — field by field. Then the rules the corpus does not exercise. + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { decodeJavaBeans, readFlsArchive, readTopology, type FlsNodeClass, type FlsTopology } from '../src/index.js'; + +const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus'); +const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record; + +function flsFiles(directory: string): string[] { + return readdirSync(directory) + .flatMap((name) => { + const path = join(directory, name); + return statSync(path).isDirectory() ? flsFiles(path) : name.endsWith('.fls') ? [path] : []; + }) + .sort(); +} + +/** + * The palette label Java shows: not read from the file but set from the + * class while loading (setTyp(holeHardwareTyp())), in German. + */ +const TYPE_LABELS: Record = { + Rechner: 'Rechner', + Notebook: 'Notebook', + Switch: 'Switch / WLAN', + Vermittlungsrechner: 'Vermittlungsrechner', + Gateway: 'Heimrouter', + Modem: 'Modem', +}; + +/** The topology in the shape FlsOracle.java writes, less what Swing computes (label width and height). */ +function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) { + return { + success: true, + nodes: topology.nodes.map((node, index) => ({ + class: node.javaClass, + name: node.name, + label: TYPE_LABELS[node.javaClass], + bounds: [node.x, node.y, ...(javaBounds[index]?.slice(2) ?? [])], + ...(node.javaClass === 'Rechner' || node.javaClass === 'Notebook' + ? { useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName } + : {}), + ports: + node.javaClass === 'Switch' || node.javaClass === 'Modem' + ? Array.from({ length: node.portCount }, () => ({})) + : node.interfaces.map((nic) => ({ ...nic, mac: nic.mac ?? null })), + })), + cables: topology.links.map((link) => ({ + ends: link.ends.map((end) => [end.node, end.port]), + wireless: link.wireless, + })), + documentation: topology.documentation.map((item) => ({ + type: item.type, + text: item.text, + bounds: [item.x, item.y, item.width, item.height], + color: item.color, + font: item.font === null ? null : [item.font.name, item.font.size, item.font.style], + })), + }; +} + +describe('corpus: the topology as Java loads it', () => { + for (const file of flsFiles(corpusRoot)) { + const name = relative(corpusRoot, file); + it(name, () => { + const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml); + const topology = readTopology(values); + const expected = javaView[name] as { nodes: { bounds: number[] | null }[] }; + + expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(expected); + expect(topology.warnings).toEqual([]); + }); + } +}); + +// ── Rules the corpus does not exercise ───────────────────────────────────── + +function doc(body: string): string { + return `\n\n${body}\n\n`; +} + +function topologyOf(body: string): FlsTopology { + return readTopology(decodeJavaBeans(doc(body))); +} + +function nodeItem(device: string, extra = ''): string { + return ` + ${device}${extra}`; +} + +describe('readTopology rules', () => { + it('passes addresses through Java setIp: canonical, null when invalid, unchanged when null', () => { + const { nodes } = topologyOf(`Filius version: 2.9.4 (1.1.2026) + ${nodeItem(` + + 010.0..0.1 + 255.255.255.256 + + 0a:0b:0c:0d:0e:0f + + `)}`); + + expect(nodes[0]!.interfaces).toEqual([ + { ip: '10.0.0.1', mask: null, mac: undefined, gateway: '', dns: '' }, + { ip: '192.168.0.10', mask: '255.255.255.0', mac: '0A:0B:0C:0D:0E:0F', gateway: '', dns: '' }, + ]); + }); + + it("gives only a Gateway's own first interface the WAN address, not one the file creates", () => { + const own = topologyOf(`${nodeItem( + '', + )}`); + const replaced = topologyOf(`${nodeItem(` + + + `)}`); + + expect(own.nodes[0]!.interfaces.map((nic) => [nic.ip, nic.mask])).toEqual([ + ['42.0.0.10', '255.0.0.0'], + ['192.168.0.10', '255.255.255.0'], + ]); + expect(own.nodes[0]!.name).toBe('Heimrouter'); + expect(replaced.nodes[0]!.interfaces.map((nic) => nic.ip)).toEqual(['192.168.0.10']); + }); + + it('places a device without bounds at 0,0 instead of leaving it out', () => { + const { nodes, warnings } = topologyOf( + `${nodeItem('')}`, + ); + + expect(nodes.map((node) => [node.javaClass, node.x, node.y, node.portCount])).toEqual([['Notebook', 0, 0, 1]]); + expect(warnings).toEqual([]); + }); + + it('leaves out an unknown device and a cable to a port no device owns, and says so', () => { + const { nodes, links, warnings } = topologyOf(` + ${nodeItem('')} + ${nodeItem(` + `)} + + + + + + + `); + + expect(nodes.map((node) => node.javaClass)).toEqual(['Switch']); + expect(links).toEqual([{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false }]); + expect(warnings).toEqual([ + 'node 1: unknown device class filius.hardware.knoten.Drucker, left out', + 'cable 2: an end is not a port of a device, left out', + ]); + }); + + it('reads the lists by position and only when the first element fits, as netzwerkLaden does', () => { + // No version line (files before 1.3.0); an empty cable list still takes its place. + const topology = topologyOf(`${nodeItem( + '', + )} + + + 1`); + const wrongFirst = topologyOf(` + not a node + ${nodeItem('')}`); + + expect(topology.versionLine).toBeNull(); + expect(topology.nodes).toHaveLength(1); + expect(topology.documentation.map((item) => item.type)).toEqual([1]); + expect(wrongFirst.nodes).toEqual([]); + }); + + it("reads colors as Java's constructors do and keeps an invalid one unset", () => { + const colors = ['16711935', '123', '123004']; + const { documentation, warnings } = topologyOf(` + ${colors + .map( + (args) => ` + ${args} + Dialog514 + `, + ) + .join('')}`); + + expect(documentation.map((item) => item.color)).toEqual([[255, 0, 255, 255], [1, 2, 3, 255], null]); + expect(documentation[0]!.font).toEqual({ name: 'Dialog', size: 14, style: 0 }); + expect(warnings).toEqual(['documentation item: color component outside 0…255, left unset (as in Java)']); + }); +}); diff --git a/scripts/code_language_allow.txt b/scripts/code_language_allow.txt index 6eb6b2a..7dd3257 100644 --- a/scripts/code_language_allow.txt +++ b/scripts/code_language_allow.txt @@ -13,3 +13,6 @@ vermittlungsrechner verbindung netzzugangsschicht vermittlungsschicht +# The class name filius.hardware.knoten.Rechner, as the device classes of +# the .fls format are keyed in packages/fls (topology.ts and its test). +packages/fls/::rechner diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index 56a3e9f..be6110c 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -19,8 +19,12 @@ virtuellen Bildschirm statt „headless". Lauf nach `.upstream/` geholt und gebaut. * **MAC-Adressen**, die nicht in der Datei stehen, würfelt Filius beim Anlegen der Netzwerkkarte. Sie erscheinen als `null`; zwei Läufe sind byte-gleich. +* Ob eine MAC in der Datei steht, wird **ohne Rücksicht auf Groß-/Kleinschreibung** + geprüft: `getMac()` schreibt groß, acht Korpus-Dateien speichern klein (bis + 29.09.2026 erschienen sie fälschlich als `null`). * Bisher ausgelesen: Geräte (Klasse, Name, Beschriftung, Position, - Schnittstellen), Kabel (Enden als Gerät/Schnittstelle), Doku-Elemente. + Schnittstellen, bei Rechner/Notebook `useIPAsName`/`useMACAsName`), Kabel + (Enden als Gerät/Schnittstelle), Doku-Elemente. Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise. ## Getter-Standardwerte @@ -34,3 +38,19 @@ x-Koordinate eines Geräts. Daran scheitert der Swift-Leser (Plan §13, M2). Argumente) und ruft den Getter auf; das Ergebnis steht in `packages/fls/src/javaDefaults.json`. Gemessen, nicht geraten: Filius' Knopf setzt `verticalTextPosition` auf 3 (BOTTOM), nicht auf Swings 0 (CENTER). + +Dasselbe gilt für **statische Konstanten**: Die erste `0` einer Datei kann als +`` stehen, und jede +spätere `0` — etwa die Rot-, Grün- und Blauwerte einer schwarzen Doku-Farbe — +verweist darauf. Schreibweise im Aufruf: `klasse::FELD`, Ergebnis +`"SwingConstants.CENTER": 0`. + +## `IP.ipCheck` + +Java-Filius schickt jede IP-Adresse und Maske beim Laden durch +`IP.ipCheck`. `IpCheckCases` ruft es für jede Zeile von +`packages/fls/test/ip-check.cases.txt` auf (ASCII, Nicht-ASCII als +`\uXXXX`) und schreibt `ip-check.java.json`. Gemessen: leere Abschnitte +zählen nicht (`10..0.0.1`), Vorzeichen und führende Nullen gehen, ebenso +Ziffern jeder Schrift in der BMP (Thai, Devanagari, Vollbreite) — aber kein +Leerzeichen und kein 𝟏 (Ersatzpaar). diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index 0b3cb8a..61fbd70 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -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" +javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.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. @@ -30,7 +30,7 @@ HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" FlsOracl "$root/corpus" "$root/corpus/corpus.java.json" 2>&1 | grep -v 'logback\|^$' || true test -s "$root/corpus/corpus.java.json" -# Getter defaults for the XMLEncoder references (packages/fls/src/javaDefaults.json). +# Getter defaults and static constants for the XMLEncoder references (packages/fls/src/javaDefaults.json). defaults="$root/packages/fls/src/javaDefaults.json" rm -f "$defaults" HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" GetterDefaults "$defaults" \ @@ -49,5 +49,13 @@ HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" GetterDe filius.software.dns.DNSServer#port \ filius.software.rip.RIPServer#port \ filius.software.www.WebServer#port \ + javax.swing.SwingConstants::CENTER \ 2>&1 | grep -v 'logback\|^$' || true test -s "$defaults" + +# IP.ipCheck on hand-picked cases (packages/fls/src/javaIp.ts). +cases="$root/packages/fls/test/ip-check.cases.txt" +ipcheck="$root/packages/fls/test/ip-check.java.json" +rm -f "$ipcheck" +HOME="$here/.build/home" java -cp "$classpath:$here/.build" IpCheckCases "$cases" "$ipcheck" 2>&1 | grep -v 'logback\|^$' || true +test -s "$ipcheck" diff --git a/tools/java-oracle/src/FlsOracle.java b/tools/java-oracle/src/FlsOracle.java index 8d0257c..0a07e67 100644 --- a/tools/java-oracle/src/FlsOracle.java +++ b/tools/java-oracle/src/FlsOracle.java @@ -10,6 +10,7 @@ import filius.gui.netzwerksicht.GUIKnotenItem; import filius.hardware.Kabel; import filius.hardware.NetzwerkInterface; import filius.hardware.Port; +import filius.hardware.knoten.Host; import filius.hardware.knoten.InternetKnoten; import filius.hardware.knoten.Knoten; import filius.hardware.knoten.LokalerKnoten; @@ -31,6 +32,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -114,6 +116,10 @@ public class FlsOracle { json.put("label", item.getImageLabel() == null ? null : item.getImageLabel().getTyp()); Rectangle bounds = item.getImageLabel() == null ? null : item.getImageLabel().getBounds(); json.put("bounds", bounds == null ? null : List.of(bounds.x, bounds.y, bounds.width, bounds.height)); + if (node instanceof Host host) { + json.put("useIpAsName", host.isUseIPAsName()); + json.put("useMacAsName", host.isUseMACAsName()); + } List ports = new ArrayList<>(); if (node instanceof InternetKnoten internet) { List nics = internet.getNetzwerkInterfaces(); @@ -125,7 +131,10 @@ public class FlsOracle { // Without a MAC address in the file, FILIUS rolls a random one when it // creates the interface; only a MAC that stands in the file is stable. String mac = nic.getMac(); - port.put("mac", mac != null && xmlText.contains(">" + mac + "<") ? mac : null); + // getMac() upper-cases; the file may not. + boolean macInFile = mac != null && Pattern + .compile(">" + Pattern.quote(mac) + "<", Pattern.CASE_INSENSITIVE).matcher(xmlText).find(); + port.put("mac", macInFile ? mac : null); port.put("gateway", nic.getGateway()); port.put("dns", nic.getDns()); ports.add(port); diff --git a/tools/java-oracle/src/GetterDefaults.java b/tools/java-oracle/src/GetterDefaults.java index b66a606..b1f2491 100644 --- a/tools/java-oracle/src/GetterDefaults.java +++ b/tools/java-oracle/src/GetterDefaults.java @@ -4,7 +4,10 @@ // later referenced by id — e.g. as a node's x coordinate. Decoding such a file // needs these defaults; they are measured here, not guessed. // -// Usage: java -cp filius.jar:lib/*:classes GetterDefaults ... +// The same holds for static constants: the first Integer 0 in a file may be +// written as `SwingConstants.CENTER`, and every later 0 refers to it. +// +// Usage: java -cp filius.jar:lib/*:classes GetterDefaults ... import java.beans.Introspector; import java.beans.PropertyDescriptor; @@ -20,6 +23,13 @@ public class GetterDefaults { Map result = new TreeMap<>(); for (int index = 1; index < args.length; index++) { String key = args[index]; + if (key.contains("::")) { + String className = key.substring(0, key.indexOf("::")); + String field = key.substring(key.indexOf("::") + 2); + String simpleName = className.substring(className.lastIndexOf('.') + 1); + result.put(simpleName + "." + field, Class.forName(className).getField(field).get(null)); + continue; + } String className = key.substring(0, key.indexOf('#')); String property = key.substring(key.indexOf('#') + 1); String simpleName = className.substring(className.lastIndexOf('.') + 1); diff --git a/tools/java-oracle/src/IpCheckCases.java b/tools/java-oracle/src/IpCheckCases.java new file mode 100644 index 0000000..421d13b --- /dev/null +++ b/tools/java-oracle/src/IpCheckCases.java @@ -0,0 +1,52 @@ +// Runs FILIUS's own IP.ipCheck on every case of a list and writes what Java +// returns — the reference for packages/fls/src/javaIp.ts. The cases file is +// ASCII, one case per line, with Java-style unicode escapes (backslash, u, +// four hex digits) for anything else (so no editor or +// hook can change a case by trimming or normalising it). +// +// Usage: java -cp filius.jar:lib/*:classes IpCheckCases + +import filius.software.vermittlungsschicht.IP; + +import java.io.FileOutputStream; +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.List; + +public class IpCheckCases { + + public static void main(String[] args) throws Exception { + List result = new ArrayList<>(); + for (String line : Files.readAllLines(Path.of(args[0]), StandardCharsets.US_ASCII)) { + String input = unescape(line); + String output; + try { + output = IP.ipCheck(input); + } catch (RuntimeException error) { + output = "!" + error.getClass().getSimpleName(); + } + result.add(Arrays.asList(line, output)); + } + try (FileOutputStream out = new FileOutputStream(args[1])) { + out.write((FlsOracle.Json.write(result, "") + "\n").getBytes(StandardCharsets.UTF_8)); + } + System.out.println("ip check cases: " + result.size()); + System.exit(0); + } + + static String unescape(String line) { + StringBuilder out = new StringBuilder(); + for (int index = 0; index < line.length(); index++) { + if (line.startsWith("\\u", index) && index + 6 <= line.length()) { + out.append((char) Integer.parseInt(line.substring(index + 2, index + 6), 16)); + index += 5; + } else { + out.append(line.charAt(index)); + } + } + return out.toString(); + } +}