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:
@@ -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):
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user