feat(core): the file explorer, text editor and image viewer's file operations after Java, measured against Java

FileCases does what the windows of FILIUS' file explorer and text editor
do to a host's files: new folder, delete, copy, cut and paste, rename,
import, save and save as. The core's filePrograms.ts does the same on the
node's file system; the file system renames an entry in its place, as
Java's tree does.

Departures from Java: a folder name that comes to no name makes nothing,
a paste onto a name there already is refused, a rename to a name with a
slash or blanks at its ends is refused and one of the case only is done,
save as and import onto a folder's name are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:59:11 +00:00
co-authored by Claude Opus 5.5
parent f622959ecc
commit 9d42b90157
13 changed files with 7653 additions and 3 deletions
+34
View File
@@ -870,6 +870,40 @@ Gemessen (02.10.2026):
ebenso eine unter Last (andere Sitzungen auf dem Rechner: die erste
ARP-Runde dauert dann knapp 300 statt 225 ms).
## Datei-Explorer, Texteditor, Bildbetrachter (F3.5b)
`FileCases` tut, was die Fenster dieser Programme mit den Dateien eines
Rechners tun (`GUIApplicationFileExplorerWindow`,
`GUIApplicationTextEditorWindow`; der Bildbetrachter liest nur), Schritt für
Schritt aus `file-programs.cases.txt`, ohne laufendes Netz, ab den Dateien
von `TerminalCases`, in beiden Sprachen. Geschrieben werden je Schritt die
Rückgabe (Meldung, Ausnahme) und alle Dateien, dazu die Meldungen des
Explorers.
Gemessen (02.10.2026):
* Neuer Ordner (`erstelleVerzeichnis`): der Name ist ein Pfad (`a/b` legt b
in a an, `/x` unter der Wurzel, `../x` darüber); ein vorhandener Name
ändert nichts; `..` an der Wurzel legt einen Ordner ohne Namen an.
* Einfügen: eine tiefe Kopie ans Ende des Ordners; ein zweites Einfügen
desselben Namens legt eine zweite Datei gleichen Namens an; nicht in das
Kopierte selbst oder darunter (`fileexplorer_msg19`); Ausschneiden und in
denselben Ordner einfügen schiebt den Eintrag ans Ende.
* Umbenennen: der Eintrag behält seine Stelle; ein leerer oder vorhandener
Name ändert nichts, auch eine reine Änderung der Groß-/Kleinschreibung
(Java findet den Eintrag selbst); `a/b` wird eine Datei mit `/` im Namen;
Leerzeichen am Rand bleiben.
* Import (`FileExplorer.addFile`): der Typ nach dem Namen der lokalen Datei
(`config/filetypes.txt`; ohne Endung Text, unbekannte Endung kein Typ);
Text Zeile für Zeile, jede mit CR LF (auch die letzte), UTF-8; anderes als
Base64; über 150 000 Byte abgelehnt; ohne Namen `noName`; auf einen
vorhandenen Namen Inhalt und Typ ersetzt, Schreibweise bleibt; auf einen
Ordnernamen eine Ausnahme.
* Texteditor: „Speichern“ ersetzt den Inhalt der offenen Datei, „Speichern
unter“ (`speicherDatei`) legt eine Datei vom Typ `text` an oder ersetzt
Inhalt und Typ einer gleichnamigen (auch eines Bildes); auf einen
Ordnernamen eine Ausnahme, in einem fehlenden Ordner nichts.
## Befehlszeile, traceroute und tcpdump (F3.4c)
Weitere Abläufe von `TerminalRun` auf `terminal-net`. Gemessen (02.10.2026):
+11 -1
View File
@@ -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" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.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" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.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.
@@ -286,6 +286,16 @@ for language in de en; do
test -s "$output"
done
# What the file explorer and the text editor do to a host's files, without a
# running network, in both languages (FileCases; packages/core/test/filePrograms.test.ts).
for language in de en; do
output="$root/packages/core/test/file-programs.$language.java.json"
rm -f "$output"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" FileCases "$language" \
"$root/packages/core/test/scenarios/router-web.scenario.json" "$root/packages/core/test/file-programs.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
test -s "$output"
done
# The command line's network commands on terminal-net, typed at set times
# (TerminalRun; packages/core/test/terminalJava.test.ts), in the German locale.
for run in pingLan:8000 pingRouted:13000 pingNobody:49000 pingUnreachable:45000 pingBroadcast:27000 pingSelf:17000 \
+182
View File
@@ -0,0 +1,182 @@
// Feeds what FILIUS' file explorer, text editor and image viewer do to a
// host's files, as their windows call FILIUS (GUIApplicationFileExplorerWindow,
// GUIApplicationTextEditorWindow; the image viewer only reads), without a
// running network, and writes after each step what it gave back (a message,
// an exception) and every file and directory of the host, and the explorer's
// messages as Java loads them: the reference for the core's file programs
// (packages/core/src/runtime/apps/filePrograms.ts).
// Each case starts on host 10.0.0.2 of a scenario with TerminalCases' files.
// The cases file is ASCII, with Java-style unicode escapes; a step is an
// operation and its fields, separated by tabs:
//
// mkdir <folder> <name> the explorer's "new folder" (an empty name: nothing)
// delete <folder> <name> the explorer's "delete" of an entry of the folder
// copy|cut <folder> <name> the explorer's clipboard
// paste <folder> the explorer's "paste" into the folder
// rename <folder> <name> <new> the explorer's "rename"
// import <folder> <new> <local name> <content>
// the explorer's import of a local file (FileExplorer.addFile);
// content @image (TerminalCases' image), @binary (bytes 0 to 255),
// @bytes:N (N times 'a'), else the text
// save <path> <text> the editor's "save" of the open file
// saveas <folder> <name> <text> the editor's "save as"
//
// Usage: java -cp filius.jar:lib/*:classes FileCases <de|en> <scenario.json> <cases txt> <output json>
import filius.rahmenprogramm.Base64;
import filius.rahmenprogramm.Information;
import filius.software.lokal.FileExplorer;
import filius.software.system.Datei;
import filius.software.system.Dateisystem;
import filius.software.system.InternetKnotenBetriebssystem;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.swing.tree.DefaultMutableTreeNode;
public class FileCases {
static DefaultMutableTreeNode clipboard;
static boolean cut;
public static void main(String[] args) throws Exception {
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
ScenarioNetwork network = ScenarioNetwork.read(args[1]);
InternetKnotenBetriebssystem system = network.system(TerminalCases.HOST);
system.installApp("filius.software.lokal.FileExplorer", false);
FileExplorer explorer = (FileExplorer) system.holeSoftware("filius.software.lokal.FileExplorer");
explorer.setSystemSoftware(system);
Dateisystem files = system.getDateisystem();
Path local = Files.createTempDirectory("filecases");
List<Object> cases = new ArrayList<>();
List<Object> steps = null;
for (String line : Files.readAllLines(Path.of(args[2]), StandardCharsets.US_ASCII)) {
if (line.startsWith("#")) continue;
if (line.startsWith("case ")) {
Map<String, Object> current = new LinkedHashMap<>();
steps = new ArrayList<>();
current.put("name", line.substring("case ".length()));
current.put("steps", steps);
cases.add(current);
TerminalCases.files(files);
clipboard = null;
current.put("start", TerminalCases.tree(files));
continue;
}
String[] fields = line.split("\t", -1);
for (int index = 0; index < fields.length; index++) fields[index] = IpCheckCases.unescape(fields[index]);
Map<String, Object> step = new LinkedHashMap<>();
step.put("step", Arrays.asList(fields));
Object returned;
try {
returned = run(fields, files, explorer, local);
} catch (Exception error) {
returned = "!" + error.getClass().getSimpleName();
}
step.put("returned", returned == null ? null : returned.toString());
step.put("files", TerminalCases.tree(files));
steps.add(step);
}
Map<String, Object> out = new LinkedHashMap<>();
out.put("cases", cases);
// The messages the explorer shows (its import's, and its paste's refusal), as Java loads them.
Map<String, Object> texts = new LinkedHashMap<>();
for (String key : new String[] { "sw_fileexplorer_msg1", "sw_fileexplorer_msg2", "sw_fileexplorer_msg3", "fileexplorer_msg19" }) {
texts.put(key, explorer.messages.getString(key));
}
out.put("texts", texts);
try (FileOutputStream stream = new FileOutputStream(args[3])) {
stream.write((FlsOracle.Json.write(out, "") + "\n").getBytes(StandardCharsets.UTF_8));
}
System.out.println("file cases (" + args[0] + "): " + cases.size());
System.exit(0);
}
static Object run(String[] fields, Dateisystem files, FileExplorer explorer, Path local) throws Exception {
DefaultMutableTreeNode folder = files.verzeichnisKnoten(fields[1]);
switch (fields[0]) {
case "mkdir":
// "Neuer Ordner": the name asked for, nothing when it is empty.
if (fields[2].equals("")) return null;
return files.erstelleVerzeichnis(folder, fields[2]);
case "delete":
// "Loeschen": the selected entry, found by its name, removed from the folder.
folder.remove(Dateisystem.verzeichnisKnoten(folder, fields[2]));
return null;
case "copy":
case "cut":
clipboard = Dateisystem.verzeichnisKnoten(folder, fields[2]);
cut = fields[0].equals("cut");
return null;
case "paste":
// "Einfuegen": not into the copied entry itself or below it; a deep copy added, a cut one removed.
if (clipboard.isNodeDescendant(folder)) return "fileexplorer_msg19";
folder.add(deepCopy(clipboard));
if (cut) {
((DefaultMutableTreeNode) clipboard.getParent()).remove(clipboard);
clipboard = null;
}
return null;
case "rename": {
// "Umbenennen": a name that is not blank and not there yet in the folder.
String name = fields[3];
if (name.trim().isEmpty() || files.dateiVorhanden(folder, name)) return null;
DefaultMutableTreeNode node = Dateisystem.verzeichnisKnoten(folder, fields[2]);
if (node.getUserObject() instanceof Datei) ((Datei) node.getUserObject()).setName(name);
else node.setUserObject(name);
return null;
}
case "import": {
Path file = local.resolve(fields[3]);
Files.write(file, content(fields[4]));
return explorer.addFile(local.toString() + "/", fields[3], folder, fields[2]);
}
case "save":
// "Speichern": the open file's content.
files.holeDatei(fields[1]).setDateiInhalt(fields[2]);
return null;
case "saveas":
// "Speichern unter": a new text file (texteditor_msg8) stored in the folder chosen.
return files.speicherDatei(fields[1], new Datei(fields[2], explorer.messages.getString("texteditor_msg8"), fields[3]));
default:
throw new IllegalArgumentException("unknown step " + fields[0]);
}
}
static byte[] content(String spec) throws Exception {
if (spec.equals("@image")) return Base64.decode(TerminalCases.IMAGE);
if (spec.equals("@binary")) {
byte[] bytes = new byte[256];
for (int index = 0; index < bytes.length; index++) bytes[index] = (byte) index;
return bytes;
}
if (spec.startsWith("@bytes:")) {
byte[] bytes = new byte[Integer.parseInt(spec.substring("@bytes:".length()))];
Arrays.fill(bytes, (byte) 'a');
return bytes;
}
return spec.getBytes(StandardCharsets.UTF_8);
}
/** GUIApplicationFileExplorerWindow.tiefesKopieren: the node with all below it, serialized and read back. */
static DefaultMutableTreeNode deepCopy(DefaultMutableTreeNode original) throws Exception {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(buffer)) {
out.writeObject(original);
}
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
return (DefaultMutableTreeNode) in.readObject();
}
}
}