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
+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();
}
}
}