From fe9325c94a44fee9eee0270a64466e921bee274b Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Fri, 21 Nov 2025 18:10:22 -0600 Subject: [PATCH 01/22] feat: Create LiFS extension, version 1.0.5 (extensions.json wasn't updated yet) --- extensions/ohgodwhy2k/lithiumfs.js | 2283 ++++++++++++++++++++++++++++ 1 file changed, 2283 insertions(+) create mode 100644 extensions/ohgodwhy2k/lithiumfs.js diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js new file mode 100644 index 0000000000..7e5306a81f --- /dev/null +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -0,0 +1,2283 @@ +// Name: Lithium FS +// ID: lithiumFS +// Description: Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more. +// By: ohgodwhy2k +// Original: 0832 +// License: MIT + +Scratch.translate.setup({ + "de": { + "clean": "Dateisystem löschen", + "del": "Lösche [STR]", + "folder": "Setze [STR] auf [STR2]", + "folder_default": "LiFS ist gut!", + "in": "Dateisystem von [STR] importieren", + "list": "Alle Dateien unter [STR] auflisten", + "open": "Öffne [STR]", + "out": "Dateisystem exportieren", + "search": "Suche [STR]", + "start": "Erschaffe [STR]", + "sync": "Ändere die Position von [STR] zu [STR2]", + "listMenuAll": "alle", + "listMenuFiles": "dateien", + "listMenuDirs": "verzeichnisse", + "list_new": "liste [TYPE] unter [STR]", + "exists": "existiert [STR]?", + "isFile": "ist [STR] eine datei?", + "isDir": "ist [STR] ein verzeichnis?", + "copy": "kopiere [STR] nach [STR2]", + "fileName": "dateiname von [STR]", + "dirName": "verzeichnis von [STR]", + "sync_new": "benenne [STR] um in [STR2]", + "permSet": "[ACTION] [PERM] Berechtigung für [STR]", + "permAdd": "hinzufügen", + "permRemove": "entfernen", + "permCreate": "erstellen", + "permDelete": "löschen", + "permSee": "sehen", + "permRead": "lesen", + "permWrite": "schreiben", + "permList": "berechtigungen auflisten für [STR]", + "permControl": "kontrollieren", + "toggleLogging": "schalte [STATE] konsolen-logging", + "logOn": "an", + "logOff": "aus", + "setLimit": "setze größenlimit für [DIR] auf [BYTES] bytes", + "removeLimit": "entferne größenlimit für [DIR]", + "getLimit": "größenlimit von [DIR] (bytes)", + "getSize": "aktuelle größe von [DIR] (bytes)", + "getLastError": "letzter fehler", + "wasRead": "wurde gelesen?", + "wasWritten": "wurde geschrieben?", + "version": "Version", + "dateCreated": "erstellungsdatum von [STR]", + "dateModified": "änderungsdatum von [STR]", + "dateAccessed": "zugriffsdatum von [STR]" + }, + "es": { + "folder_default": "¡LiFS es bueno!", + "listMenuAll": "todo", + "listMenuFiles": "archivos", + "listMenuDirs": "directorios", + "list_new": "listar [TYPE] en [STR]", + "exists": "¿existe [STR]?", + "isFile": "¿es [STR] un archivo?", + "isDir": "¿es [STR] un directorio?", + "copy": "copiar [STR] a [STR2]", + "fileName": "nombre de archivo de [STR]", + "dirName": "directorio de [STR]", + "sync_new": "renombrar [STR] a [STR2]", + "permSet": "[ACTION] permiso de [PERM] a [STR]", + "permAdd": "añadir", + "permRemove": "quitar", + "permCreate": "crear", + "permDelete": "eliminar", + "permSee": "ver", + "permRead": "leer", + "permWrite": "escribir", + "permList": "listar permisos de [STR]", + "permControl": "controlar", + "toggleLogging": "[STATE] el registro de la consola", + "logOn": "activar", + "logOff": "desactivar", + "setLimit": "establecer límite de tamaño para [DIR] a [BYTES] bytes", + "removeLimit": "eliminar límite de tamaño para [DIR]", + "getLimit": "límite de tamaño de [DIR] (bytes)", + "getSize": "tamaño actual de [DIR] (bytes)", + "getLastError": "último error", + "wasRead": "¿fue leído?", + "wasWritten": "¿fue escrito?", + "version": "versión", + "dateCreated": "fecha de creación de [STR]", + "dateModified": "fecha de modificación de [STR]", + "dateAccessed": "fecha de acceso de [STR]" + }, + "fi": { + "clean": "tyhjennä tiedostojärjestelmä", + "del": "poista [STR]", + "folder": "aseta [STR] arvoon [STR2]", + "folder_default": "LiFS on hieno!", + "in": "tuo tiedostojärjestelmä kohteesta [STR]", + "list": "luettelo kaikista kohteessa [STR] sijaitsevista tiedostoista", + "open": "avaa [STR]", + "out": "vie tiedostojärjestelmä", + "search": "etsi [STR]", + "start": "luo [STR]", + "sync": "muuta kohteen [STR] sijainniksi [STR2]", + "listMenuAll": "kaikki", + "listMenuFiles": "tiedostot", + "listMenuDirs": "kansiot", + "list_new": "listaa [TYPE] polussa [STR]", + "exists": "onko [STR] olemassa?", + "isFile": "onko [STR] tiedosto?", + "isDir": "onko [STR] kansio?", + "copy": "kopioi [STR] kohteeseen [STR2]", + "fileName": "tiedostonimi [STR]", + "dirName": "kansio [STR]", + "sync_new": "nimeä [STR] uudelleen [STR2]", + "permSet": "[ACTION] [PERM] käyttöoikeuden kohteelle [STR]", + "permAdd": "lisää", + "permRemove": "poista", + "permCreate": "luo", + "permDelete": "poista", + "permSee": "nähdä", + "permRead": "lukea", + "permWrite": "kirjoittaa", + "permList": "listaa [STR] käyttöoikeudet", + "permControl": "hallita", + "toggleLogging": "laita [STATE] konsoliloki", + "logOn": "päälle", + "logOff": "pois päältä", + "getLastError": "viimeisin virhe", + "wasRead": "luettiinko?", + "wasWritten": "kirjoitettiinko?", + "version": "versio", + "dateCreated": "luontipäivä [STR]", + "dateModified": "muokkauspäivä [STR]", + "dateAccessed": "käyttöpäivä [STR]" + }, + "fr": { + "clean": "effacer le système de fichiers", + "del": "supprimer [STR]", + "folder": "mettre [STR] à [STR2]", + "folder_default": "LiFS est bon !", + "in": "importer le système de fichier depuis [STR]", + "list": "lister tous les fichiers sous [STR]", + "open": "ouvrir [STR]", + "out": "exporter le système de fichiers", + "search": "chercher [STR]", + "start": "créer [STR]", + "sync": "modifier l'emplacement de [STR] à [STR2]", + "listMenuAll": "tout", + "listMenuFiles": "fichiers", + "listMenuDirs": "dossiers", + "list_new": "lister [TYPE] sous [STR]", + "exists": "[STR] existe-t-il?", + "isFile": "[STR] est-il un fichier?", + "isDir": "[STR] est-il un dossier?", + "copy": "copier [STR] vers [STR2]", + "fileName": "nom de fichier de [STR]", + "dirName": "dossier de [STR]", + "sync_new": "renommer [STR] en [STR2]", + "permSet": "[ACTION] la permission [PERM] à [STR]", + "permAdd": "ajouter", + "permRemove": "supprimer", + "permCreate": "crer", + "permDelete": "supprimer", + "permSee": "voir", + "permRead": "lire", + "permWrite": "écrire", + "permList": "lister les permissions de [STR]", + "permControl": "contrôler", + "toggleLogging": "[STATE] la journalisation de la console", + "logOn": "activer", + "logOff": "désactiver", + "setLimit": "définir la limite de taille pour [DIR] à [BYTES] octets", + "removeLimit": "supprimer la limite de taille pour [DIR]", + "getLimit": "limite de taille de [DIR] (octets)", + "getSize": "taille actuelle de [DIR] (octets)", + "getLastError": "dernière erreur", + "wasRead": "lu ?", + "wasWritten": "écrit ?", + "version": "version", + "dateCreated": "date de création de [STR]", + "dateModified": "date de modification de [STR]", + "dateAccessed": "date d'accès de [STR]" + }, + "it": { + "clean": "svuota file system", + "del": "cancella [STR]", + "folder": "imposta [STR] a [STR2]", + "folder_default": "LiFS funziona!", + "in": "importa file system da [STR]", + "list": "elenca tutti i file in [STR]", + "open": "apri [STR]", + "out": "esporta file system", + "search": "cerca [STR]", + "start": "crea [STR]", + "sync": "cambia posizione di [STR] a [STR2]", + "listMenuAll": "tutti", + "listMenuFiles": "file", + "listMenuDirs": "directory", + "list_new": "elenca [TYPE] in [STR]", + "exists": "[STR] esiste?", + "isFile": "[STR] è un file?", + "isDir": "[STR] è una directory?", + "copy": "copia [STR] in [STR2]", + "fileName": "nome file di [STR]", + "dirName": "directory di [STR]", + "sync_new": "rinomina [STR] in [STR2]", + "permSet": "[ACTION] permesso [PERM] a [STR]", + "permAdd": "aggiungi", + "permRemove": "rimuovi", + "permCreate": "crea", + "permDelete": "elimina", + "permSee": "vedi", + "permRead": "leggi", + "permWrite": "scrivi", + "permList": "elenca permessi per [STR]", + "permControl": "controllare", + "toggleLogging": "[STATE] log console", + "logOn": "attiva", + "logOff": "disattiva", + "getLastError": "ultimo errore", + "wasRead": "è stato letto?", + "wasWritten": "è stato scritto?", + "version": "versione", + "dateCreated": "data creazione di [STR]", + "dateModified": "data modifica di [STR]", + "dateAccessed": "data accesso di [STR]" + }, + "ja": { + "clean": "ファイルシステムを削除する", + "del": "[STR]を削除", + "folder": "[STR]を[STR2]にセットする", + "folder_default": "LiFSは良い!", + "in": "[STR]からファイルシステムをインポートする", + "list": "[STR]直下のファイルをリスト化する", + "open": "[STR]を開く", + "out": "ファイルシステムをエクスポートする", + "search": "[STR]を検索", + "start": "[STR]を作成", + "sync": "[STR]のロケーションを[STR2]に変更する", + "listMenuAll": "すべて", + "listMenuFiles": "ファイル", + "listMenuDirs": "ディレクトリ", + "list_new": "[STR] の [TYPE] を一覧表示", + "exists": "[STR] は存在しますか?", + "isFile": "[STR] はファイルですか?", + "isDir": "[STR] はディレクトリですか?", + "copy": "[STR] を [STR2] にコピー", + "fileName": "[STR] のファイル名", + "dirName": "[STR] のディレクトリ", + "sync_new": "[STR] を [STR2] に名前変更", + "permSet": "[STR] の [PERM] 権限を [ACTION]", + "permAdd": "追加", + "permRemove": "削除", + "permCreate": "作成", + "permDelete": "削除", + "permSee": "表示", + "permRead": "読み取り", + "permWrite": "書き込み", + "permList": "[STR] の権限を一覧表示", + "permControl": "制御", + "toggleLogging": "コンソールログを [STATE] にする", + "logOn": "オン", + "logOff": "オフ", + "getLastError": "最後のエラー", + "wasRead": "読み込まれたか?", + "wasWritten": "書き込まれたか?", + "version": "バージョン", + "dateCreated": "[STR]の作成日時", + "dateModified": "[STR]の変更日時", + "dateAccessed": "[STR]のアクセス日時" + }, + "ko": { + "clean": "파일 システム 초기화하기", + "del": "[STR] 삭제하기", + "folder": "[STR]을(를) [STR2](으)로 정하기", + "folder_default": "LiFS 최고!", + "in": "[STR]에서 파일 システム 불러오기", + "list": "[STR] 안의 파일 목록", + "open": "[STR] 열기", + "out": "파일 システム 내보내기", + "search": "[STR] 검색하기", + "start": "[STR] 생성하기", + "sync": "[STR]의 경로를 [STR2](으)로 바꾸기", + "listMenuAll": "모두", + "listMenuFiles": "파일", + "listMenuDirs": "디렉터리", + "list_new": "[STR]의 [TYPE] 목록", + "exists": "[STR]이(가) 존재하나요?", + "isFile": "[STR]이(가) 파일인가요?", + "isDir": "[STR]이(가) 디렉터리인가요?", + "copy": "[STR]을(를) [STR2](으)로 복사하기", + "fileName": "[STR]의 파일 이름", + "dirName": "[STR]의 디렉터리", + "sync_new": "[STR]의 이름을 [STR2](으)로 바꾸기", + "permSet": "[STR]에 [PERM] 권한 [ACTION]", + "permAdd": "추가하기", + "permRemove": "제거하기", + "permCreate": "생성", + "permDelete": "삭제", + "permSee": "보기", + "permRead": "읽기", + "permWrite": "쓰기", + "permList": "[STR]의 권한 목록", + "permControl": "제어", + "toggleLogging": "콘솔 로깅 [STATE]", + "logOn": "켜기", + "logOff": "끄기", + "getLastError": "마지막 오류", + "wasRead": "읽었나요?", + "wasWritten": "작성했나요?", + "version": "버전", + "dateCreated": "[STR]의 생성 날짜", + "dateModified": "[STR]의 수정 날짜", + "dateAccessed": "[STR]의 접근 날짜" + }, + "nb": { + "folder_default": "LiFS er bra!", + "listMenuAll": "alle", + "listMenuFiles": "filer", + "listMenuDirs": "mapper", + "list_new": "list [TYPE] under [STR]", + "exists": "finnes [STR]?", + "isFile": "er [STR] en fil?", + "isDir": "er [STR] en mappe?", + "copy": "kopier [STR] til [STR2]", + "fileName": "filnavn til [STR]", + "dirName": "mappe til [STR]", + "sync_new": "gi [STR] nytt navn [STR2]", + "permSet": "[ACTION] [PERM] tillatelse til [STR]", + "permAdd": "legg til", + "permRemove": "fjern", + "permCreate": "opprett", + "permDelete": "slett", + "permSee": "se", + "permRead": "les", + "permWrite": "skriv", + "permList": "list tillatelser for [STR]", + "permControl": "kontroll", + "toggleLogging": "slå [STATE] konsolllogging", + "logOn": "på", + "logOff": "av", + "getLastError": "siste feil", + "wasRead": "ble lest?", + "wasWritten": "ble skrevet?", + "version": "versjon", + "dateCreated": "opprettelsesdato for [STR]", + "dateModified": "endringsdato for [STR]", + "dateAccessed": "tilgangsdato for [STR]" + }, + "nl": { + "clean": "wis het bestandssysteem", + "del": "verwijder [STR]", + "folder": "maak [STR] [STR2]", + "folder_default": "LiFS is geweldig!", + "in": "importeer bestandssysteem van [STR]", + "list": "alle bestanden onder [STR]", + "out": "exporteer bestandssysteem", + "search": "zoek [STR]", + "start": "creëer [STR]", + "sync": "verander locatie van [STR] naar [STR2]", + "listMenuAll": "alles", + "listMenuFiles": "bestanden", + "listMenuDirs": "mappen", + "list_new": "lijst [TYPE] onder [STR]", + "exists": "bestaat [STR]?", + "isFile": "is [STR] een bestand?", + "isDir": "is [STR] een map?", + "copy": "kopieer [STR] naar [STR2]", + "fileName": "bestandsnaam van [STR]", + "dirName": "map van [STR]", + "sync_new": "hernoem [STR] naar [STR2]", + "permSet": "[ACTION] [PERM] toestemming om [STR]", + "permAdd": "toevoegen", + "permRemove": "verwijderen", + "permCreate": "maken", + "permDelete": "verwijderen", + "permSee": "zien", + "permRead": "lezen", + "permWrite": "schrijven", + "permList": "lijst toestemmingen for [STR]", + "permControl": "beheren", + "toggleLogging": "zet console logging [STATE]", + "logOn": "aan", + "logOff": "uit", + "getLastError": "laatste fout", + "wasRead": "is gelezen?", + "wasWritten": "is geschreven?", + "version": "versie", + "dateCreated": "aanmaakdatum van [STR]", + "dateModified": "wijzigingsdatum van [STR]", + "dateAccessed": "toegangsdatum van [STR]" + }, + "pl": { + "del": "usuń [STR]", + "folder": "ustaw [STR] na [STR2]", + "open": "otwórz [STR]", + "search": "szukaj [STR]", + "listMenuAll": "wszystko", + "listMenuFiles": "pliki", + "listMenuDirs": "katalogi", + "list_new": "listuj [TYPE] w [STR]", + "exists": "czy [STR] istnieje?", + "isFile": "czy [STR] to plik?", + "isDir": "czy [STR] to katalog?", + "copy": "kopiej [STR] do [STR2]", + "fileName": "nazwa pliku [STR]", + "dirName": "katalog [STR]", + "sync_new": "zmień nazwę [STR] na [STR2]", + "permSet": "[ACTION] [PERM] uprawnienie do [STR]", + "permAdd": "dodaj", + "permRemove": "usuń", + "permCreate": "tworzenie", + "permDelete": "usuwanie", + "permSee": "przeglądanie", + "permRead": "czytanie", + "permWrite": "pisanie", + "permList": "listuj uprawnienia [STR]", + "permControl": "kontrola", + "toggleLogging": "włącz [STATE] logowanie konsoli", + "logOn": "włącz", + "logOff": "wyłącz", + "getLastError": "ostatni błąd", + "wasRead": "czytano?", + "wasWritten": "pisano?", + "version": "wersja", + "dateCreated": "data utworzenia [STR]", + "dateModified": "data modyfikacji [STR]", + "dateAccessed": "data dostępu [STR]" + }, + "ru": { + "clean": "очистить файловую систему", + "del": "удалить [STR]", + "folder": "задать [STR] значение [STR2]", + "folder_default": "LiFS это хорошо!", + "in": "импортировать файловую систему из [STR]", + "list": "перечислить все файлы под [STR]", + "open": "открыть [STR]", + "out": "экспортировать файловую систему", + "search": "поиск [STR]", + "start": "создать [STR]", + "sync": "изменить расположение [STR] на [STR2]", + "listMenuAll": "все", + "listMenuFiles": "файлы", + "listMenuDirs": "папки", + "list_new": "список [TYPE] в [STR]", + "exists": "[STR] существует?", + "isFile": "[STR] это файл?", + "isDir": "[STR] это папка?", + "copy": "копировать [STR] в [STR2]", + "fileName": "имя файла [STR]", + "dirName": "папка [STR]", + "sync_new": "переименовать [STR] в [STR2]", + "permSet": "[ACTION] [PERM] разрешение для [STR]", + "permAdd": "добавить", + "permRemove": "удалить", + "permCreate": "создать", + "permDelete": "удалить", + "permSee": "видеть", + "permRead": "читать", + "permWrite": "писать", + "permList": "список разрешений для [STR]", + "permControl": "управлять", + "toggleLogging": "[STATE] ведение журнала консоли", + "logOn": "включить", + "logOff": "выключить", + "getLastError": "последняя ошибка", + "wasRead": "было чтение?", + "wasWritten": "была запись?", + "version": "версия", + "dateCreated": "дата создания [STR]", + "dateModified": "дата изменения [STR]", + "dateAccessed": "дата доступа [STR]" + }, + "zh-cn": { + "clean": "清空文件System", + "del": "删除 [STR]", + "folder": "将[STR]设为[STR2]", + "folder_default": "LiFS 好用!", + "in": "从 [STR] 导入文件System", + "list": "列出 [STR] 下的所有文件", + "open": "打开 [STR]", + "out": "导出文件 system", + "search": "搜索 [STR]", + "start": "新建 [STR]", + "sync": "将 [STR] 的位置改为 [STR2]", + "listMenuAll": "所有", + "listMenuFiles": "文件", + "listMenuDirs": "目录", + "list_new": "列出 [STR] 下的 [TYPE]", + "exists": "[STR] 是否存在?", + "isFile": "[STR] 是文件吗?", + "isDir": "[STR] 是目录吗?", + "copy": "将 [STR] 复制到 [STR2]", + "fileName": "[STR] 的文件名", + "dirName": "[STR] 的目录", + "sync_new": "将 [STR] 重命名为 [STR2]", + "permSet": "[ACTION] [STR] 的 [PERM] 权限", + "permAdd": "添加", + "permRemove": "移除", + "permCreate": "创建", + "permDelete": "删除", + "permSee": "查看", + "permRead": "读取", + "permWrite": "写入", + "permList": "列出 [STR] 的权限", + "permControl": "控制", + "toggleLogging": "[STATE]控制台日志", + "logOn": "开启", + "logOff": "关闭", + "getLastError": "上一个错误", + "wasRead": "是否读取?", + "wasWritten": "是否写入?", + "version": "版本", + "dateCreated": "[STR] 的创建日期", + "dateModified": "[STR] 的修改日期", + "dateAccessed": "[STR] 的访问日期" + } +}); +(function(Scratch) { + "use strict"; + + const defaultPerms = { + create: true, + delete: true, + see: true, + read: true, + write: true, + control: true, + }; + + const extensionVersion = "1.0.5"; + + class LiFS { + constructor() { + + this.fs = new Map(); + this.liFSLogEnabled = false; + this.lastError = ""; + this.readActivity = false; + this.writeActivity = false; + + this._log("Initializing LiFS extension..."); + this._internalClean(); + } + + getInfo() { + return { + id: "lithiumFS", + + name: "Lithium FS", + color1: "#d52246", + color2: "#a61734", + color3: "#7f1026", + + description: "Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more.", + blocks: [ + + { + opcode: "start", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "start", + default: "create [STR]" + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "folder", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "folder", + default: "set [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate({ + id: "folder_default", + default: "LiFS is good!", + }), + }, + }, + }, + { + opcode: "open", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "open", + default: "open [STR]" + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "del", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "del", + default: "delete [STR]" + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "list", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "list_new", + default: "list [TYPE] under [STR]", + }), + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "LIST_TYPE_MENU", + defaultValue: "all", + }, + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + "---", + + { + opcode: "copy", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "copy", + default: "copy [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/copy_of_example.txt", + }, + }, + }, + { + opcode: "sync", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "sync_new", + default: "rename [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/new_example.txt", + }, + }, + }, + { + opcode: "exists", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "exists", + default: "does [STR] exist?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "isFile", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "isFile", + default: "is [STR] a file?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "isDir", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "isDir", + default: "is [STR] a directory?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "fileName", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "fileName", + default: "file name of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dirName", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dirName", + default: "directory of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + + { + opcode: "dateCreated", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateCreated", + default: "date created of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dateModified", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateModified", + default: "date modified of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dateAccessed", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateAccessed", + default: "date accessed of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + "---", + + { + opcode: "setLimit", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "setLimit", + default: "set size limit for [DIR] to [BYTES] bytes", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + BYTES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 8192, + }, + }, + }, + { + opcode: "removeLimit", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "removeLimit", + default: "remove size limit for [DIR]", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "getLimit", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getLimit", + default: "size limit of [DIR] (bytes)", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "getSize", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getSize", + default: "current size of [DIR] (bytes)", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "setPerm", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "permSet", + default: "[ACTION] [PERM] permission for [STR]", + }), + arguments: { + ACTION: { + type: Scratch.ArgumentType.STRING, + menu: "PERM_ACTION_MENU", + defaultValue: "remove", + }, + PERM: { + type: Scratch.ArgumentType.STRING, + menu: "PERM_TYPE_MENU", + defaultValue: "write", + }, + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "listPerms", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "permList", + default: "list permissions for [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + "---", + + { + opcode: "clean", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "clean", + default: "clear the file system", + }), + arguments: {}, + }, + { + opcode: "in", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "in", + default: "import file system from [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: '{"version":"1.0.5","fs":{}}', + }, + }, + }, + { + opcode: "out", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "out", + default: "export file system", + }), + arguments: {}, + }, + { + opcode: "wasRead", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "wasRead", + default: "was read?", + }), + }, + { + opcode: "wasWritten", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "wasWritten", + default: "was written?", + }), + }, + { + opcode: "getLastError", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getLastError", + default: "last error", + }), + }, + { + opcode: "toggleLogging", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "toggleLogging", + default: "turn [STATE] console logging", + }), + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "LOG_STATE_MENU", + defaultValue: "on", + }, + }, + }, + { + opcode: "getVersion", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "version", + default: "version" + }), + }, + ], + menus: { + LIST_TYPE_MENU: { + acceptReporters: true, + items: [{ + text: Scratch.translate({ + id: "listMenuAll", + default: "all" + }), + value: "all", + }, + { + text: Scratch.translate({ + id: "listMenuFiles", + default: "files", + }), + value: "files", + }, + { + text: Scratch.translate({ + id: "listMenuDirs", + default: "directories", + }), + value: "directories", + }, + ], + }, + PERM_ACTION_MENU: { + acceptReporters: true, + items: [{ + text: Scratch.translate({ + id: "permAdd", + default: "add" + }), + value: "add", + }, + { + text: Scratch.translate({ + id: "permRemove", + default: "remove", + }), + value: "remove", + }, + ], + }, + PERM_TYPE_MENU: { + acceptReporters: true, + items: [{ + text: Scratch.translate({ + id: "permCreate", + default: "create", + }), + value: "create", + }, + { + text: Scratch.translate({ + id: "permDelete", + default: "delete", + }), + value: "delete", + }, + { + text: Scratch.translate({ + id: "permSee", + default: "see" + }), + value: "see", + }, + { + text: Scratch.translate({ + id: "permRead", + default: "read" + }), + value: "read", + }, + { + text: Scratch.translate({ + id: "permWrite", + default: "write" + }), + value: "write", + }, + { + text: Scratch.translate({ + id: "permControl", + default: "control", + }), + value: "control", + }, + ], + }, + LOG_STATE_MENU: { + acceptReporters: true, + items: [{ + text: Scratch.translate({ + id: "logOn", + default: "on" + }), + value: "on", + }, + { + text: Scratch.translate({ + id: "logOff", + default: "off" + }), + value: "off", + }, + ], + }, + }, + }; + } + + _log(message, ...args) { + if (this.liFSLogEnabled) { + console.log(`[LiFS] ${message}`, ...args); + } + } + + _warn(message, ...args) { + if (this.liFSLogEnabled) { + console.warn(`[LiFS] ${message}`, ...args); + } + } + + _setError(message, ...args) { + this._warn(message, ...args); + this.lastError = message; + } + + _normalizePath(path) { + if (typeof path !== "string" || path.length === 0) { + return "/"; + } + + const hadTrailingSlash = path.length > 1 && path.endsWith("/"); + + if (path[0] !== "/") { + path = "/" + path; + } + + const segments = path.split("/"); + const newSegments = []; + + for (const segment of segments) { + + if (segment === "" || segment === ".") { + continue; + } + + if (segment === "..") { + if (newSegments.length > 0) { + newSegments.pop(); + } + } else { + + newSegments.push(segment); + } + } + + let newPath = "/" + newSegments.join("/"); + + if (newPath === "/") { + return "/"; + } + + if (hadTrailingSlash) { + newPath += "/"; + } + + return newPath; + } + + _isPathDir(path) { + + return path === "/" || path.endsWith("/"); + } + + _internalDirName(path) { + if (path === "/") { + return "/"; + } + + let procPath = this._isPathDir(path) ? + path.substring(0, path.length - 1) : + path; + + const lastSlash = procPath.lastIndexOf("/"); + if (lastSlash === 0) { + return "/"; + } + if (lastSlash === -1) { + return "/"; + } + + return procPath.substring(0, lastSlash + 1); + } + + _getStringSize(str) { + if (str === null || str === undefined) { + return 0; + } + + let length = 0; + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + if (charCode < 0x0080) { + length += 1; + } else if (charCode < 0x0800) { + length += 2; + } else if (charCode < 0xd800 || charCode > 0xdfff) { + length += 3; + } else { + + length += 4; + i++; + } + } + return length; + } + + _getDirectorySize(dirPath) { + let totalSize = 0; + + for (const [itemPath, entry] of this.fs.entries()) { + + if ( + !this._isPathDir(itemPath) && + itemPath.startsWith(dirPath) && + dirPath !== itemPath + ) { + totalSize += this._getStringSize(entry.content); + } + } + return totalSize; + } + + _canAccommodateChange(filePath, deltaSize) { + if (deltaSize <= 0) { + return true; + } + + let currentDir = this._internalDirName(filePath); + this._log(`Checking size change of ${deltaSize} bytes for ${filePath}`); + + while (true) { + const entry = this.fs.get(currentDir); + if (!entry) { + + this._warn(`Size check: Could not find parent dir ${currentDir}`); + break; + } + + const limit = entry.limit; + if (limit !== -1) { + + const currentSize = this._getDirectorySize(currentDir); + if (currentSize + deltaSize > limit) { + this._setError( + `Size limit exceeded for ${currentDir}: ${currentSize} + ${deltaSize} > ${limit}` + ); + return false; + } + } + + if (currentDir === "/") { + break; + } + currentDir = this._internalDirName(currentDir); + } + + return true; + } + + _internalCreate(path, content, parentDir) { + if (this.fs.has(path)) { + this._log("InternalCreate failed: Path already exists", path); + + return false; + } + + if (!this.hasPermission(parentDir, "create")) { + this._setError(`Create failed: No 'create' permission in ${parentDir}`); + return false; + } + + const deltaSize = this._getStringSize(content); + if (!this._canAccommodateChange(path, deltaSize)) { + + this._log("InternalCreate failed: Size limit exceeded"); + return false; + } + + let permsToInherit; + const parentEntry = this.fs.get(parentDir); + + if (parentEntry) { + permsToInherit = parentEntry.perms; + } else if (parentDir === "/") { + + permsToInherit = this.fs.get("/").perms; + } else { + + this._warn( + "InternalCreate: Parent not found, using default perms", + parentDir + ); + permsToInherit = defaultPerms; + } + + const now = Date.now(); + this.fs.set(path, { + content: content, + perms: JSON.parse(JSON.stringify(permsToInherit)), + limit: -1, + created: now, + modified: now, + accessed: now, + }); + this.writeActivity = true; + this._log("InternalCreate successful:", path); + return true; + } + + hasPermission(path, action) { + + const normPath = this._normalizePath(path); + this._log("Checking permission:", action, "on", normPath); + + const entry = this.fs.get(normPath); + + if (entry) { + + const result = entry.perms[action]; + this._log("Permission result:", result); + return result; + } + + if (action === "create") { + const parentDir = this._internalDirName(normPath); + const parentEntry = this.fs.get(parentDir); + + if (!parentEntry) { + + const result = parentDir === "/"; + this._log("Permission result (parent check, root):", result); + return result; + } + const result = parentEntry.perms.create; + this._log("Permission result (parent check):", result); + return result; + } + + this._log("Permission result (default fail):", false); + return false; + } + + _internalClean() { + this._log("Internal: Clearing file system..."); + const now = Date.now(); + this.fs.clear(); + this.fs.set("/", { + content: null, + perms: JSON.parse(JSON.stringify(defaultPerms)), + limit: -1, + created: now, + modified: now, + accessed: now, + }); + this._log("Internal: File system reset to root."); + this.writeActivity = true; + } + + clean() { + this.lastError = ""; + this._log("Block: clean"); + if (!this.hasPermission("/", "delete")) { + return this._setError("Clean failed: No 'delete' permission on /"); + } + this._internalClean(); + } + + sync({ + STR, + STR2 + }) { + this.lastError = ""; + const path1 = this._normalizePath(STR); + const path2 = this._normalizePath(STR2); + this._log("Block: rename", path1, "to", path2); + + if (!this.hasPermission(path1, "delete")) { + return this._setError(`Rename failed: No 'delete' permission on ${path1}`); + } + if (this.fs.has(path2)) { + return this._setError(`Rename failed: Destination ${path2} already exists`); + } + if (!this.hasPermission(path2, "create")) { + return this._setError(`Rename failed: No 'create' permission for ${path2}`); + } + + const entry = this.fs.get(path1); + if (!entry) { + return this._setError(`Rename failed: Source ${path1} not found`); + } + + const isDir = this._isPathDir(path1); + let deltaSize = 0; + if (isDir) { + deltaSize = this._getDirectorySize(path1); + } else { + deltaSize = this._getStringSize(entry.content); + } + + if (!this._canAccommodateChange(path2, deltaSize)) { + + return; + } + + const now = Date.now(); + + if (isDir) { + + this._log("Renaming directory and children..."); + + const toRename = []; + for (const [key, value] of this.fs.entries()) { + if (key.startsWith(path1)) { + toRename.push({ + oldKey: key, + value: value + }); + } + } + + const path1Length = path1.length; + for (const item of toRename) { + const remainder = item.oldKey.substring(path1Length); + const newChildPath = path2 + remainder; + + if (item.oldKey === path1) { + item.value.modified = now; + item.value.accessed = now; + } + + this.fs.set(newChildPath, item.value); + this.fs.delete(item.oldKey); + + this._log(`Renaming: ${item.oldKey} to ${newChildPath}`); + } + } else { + + this._log("Renaming single file..."); + entry.modified = now; + entry.accessed = now; + this.fs.set(path2, entry); + this.fs.delete(path1); + this._log("Rename successful"); + } + this.writeActivity = true; + } + + copy({ + STR, + STR2 + }) { + this.lastError = ""; + const path1 = this._normalizePath(STR); + const path2 = this._normalizePath(STR2); + this._log("Block: copy", path1, "to", path2); + + const entry = this.fs.get(path1); + if (!entry) { + return this._setError(`Copy failed: Source ${path1} not found`); + } + + if (!entry.perms.read) { + return this._setError(`Copy failed: No 'read' permission on ${path1}`); + } + if (this.fs.has(path2)) { + return this._setError(`Copy failed: Destination ${path2} already exists`); + } + if (!this.hasPermission(path2, "create")) { + return this._setError(`Copy failed: No 'create' permission for ${path2}`); + } + + this.readActivity = true; + const now = Date.now(); + entry.accessed = now; + + if (this._isPathDir(path1)) { + + const toCopy = []; + let totalDeltaSize = 0; + const path1Length = path1.length; + + for (const [key, value] of this.fs.entries()) { + if (key.startsWith(path1)) { + if (!this._isPathDir(key)) { + totalDeltaSize += this._getStringSize(value.content); + } + toCopy.push({ + key, + value + }); + } + } + + if (!this._canAccommodateChange(path2, totalDeltaSize)) { + + return; + } + + for (const item of toCopy) { + const remainder = item.key.substring(path1Length); + const newChildPath = path2 + remainder; + + this.fs.set(newChildPath, { + content: (item.value.content === null) ? null : ("" + item.value.content), + perms: JSON.parse(JSON.stringify(item.value.perms)), + limit: item.value.limit, + created: item.value.created, + modified: item.value.modified, + accessed: now + }); + this._log(`Copied ${item.key} to ${newChildPath}`); + } + this.writeActivity = true; + this._log("Recursive copy successful"); + + } else { + + const content = "" + entry.content; + const deltaSize = this._getStringSize(content); + if (!this._canAccommodateChange(path2, deltaSize)) { + + return; + } + + const destParentDir = this._internalDirName(path2); + const destParentEntry = this.fs.get(destParentDir); + let permsToInherit = defaultPerms; + + if (destParentEntry) { + permsToInherit = destParentEntry.perms; + } else if (destParentDir === "/") { + permsToInherit = this.fs.get("/").perms; + } else { + this._log( + `Copy: Could not find parent "${destParentDir}", using default perms.` + ); + } + + this.fs.set(path2, { + content: content, + perms: JSON.parse(JSON.stringify(permsToInherit)), + limit: -1, + created: now, + modified: now, + accessed: now + }); + this.writeActivity = true; + this._log("Copy successful"); + } + } + + start({ + STR + }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: create", path); + + if (this._isPathDir(path) && path.length > 1) { + const fileName = path.substring(0, path.length - 1).split("/").pop(); + if (fileName.includes(".")) { + this._warn( + `Path "${path}" looks like a file but is being treated as a directory due to the trailing slash.` + ); + } + } + + if (path === "/") { + return this._setError("Create failed: Cannot create root directory '/'"); + } + + if (this.fs.has(path)) { + return this._setError(`Create failed: ${path} already exists`); + } + + const parentDir = this._internalDirName(path); + if (parentDir !== "/" && !this.fs.has(parentDir)) { + this._log("Creating parent directory:", parentDir); + + if (!this.hasPermission(parentDir, "create")) { + return this._setError( + `Create failed: No 'create' permission in ${this._internalDirName(parentDir)}, aborting recursive create.` + ); + } + + this.start({ + STR: parentDir + }); + + if (this.lastError) { + this._log( + "Create failed: Parent creation failed (recursive call failed)." + ); + + return; + } + if (!this.fs.has(parentDir)) { + + return this._setError( + "Create failed: Parent creation failed, aborting." + ); + } + } + + const ok = this._internalCreate( + path, + this._isPathDir(path) ? null : "", + parentDir + ); + + if (!ok) { + this._log("Create failed: _internalCreate returned false."); + + if (!this.lastError) { + this._setError( + `Create failed: An internal error occurred for ${path}` + ); + } + return; + } + } + + open({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: open", path); + + const entry = this.fs.get(path); + this.readActivity = true; + + if (!entry) { + this._log("Result: (Not found)", ""); + return ""; + } + if (this._isPathDir(path)) { + this._log("Result: (Is a directory)", ""); + return ""; + } + + if (!entry.perms.read) { + this._warn(`Read permission denied for "${path}"`); + return ""; + } + + entry.accessed = Date.now(); + const content = entry.content; + this._log("Result:", content); + return content; + } + + del({ + STR + }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: delete", path); + + if (!this.hasPermission(path, "delete")) { + return this._setError(`Delete failed: No 'delete' permission on ${path}`); + } + + const isDir = this._isPathDir(path); + + const toDelete = []; + for (const currentPath of this.fs.keys()) { + if (isDir) { + if (currentPath.startsWith(path)) { + toDelete.push(currentPath); + } + } else { + if (currentPath === path) { + toDelete.push(currentPath); + break; + } + } + } + + for (const key of toDelete) { + this.fs.delete(key); + this._log("Deleted:", key); + } + + this.writeActivity = true; + this._log("Delete complete"); + } + + folder({ + STR, + STR2 + }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: set", path, "to", STR2); + + let entry = this.fs.get(path); + + if (!entry) { + this._log("Set: File not found, attempting to create..."); + this.start({ + STR: path + }); + entry = this.fs.get(path); + if (!entry) { + this._log("Set failed: Creation also failed"); + + return; + } + } + + if (this._isPathDir(path)) { + return this._setError("Set failed: Cannot set content of a directory"); + } + if (!entry.perms.write) { + return this._setError(`Set failed: No 'write' permission on ${path}`); + } + + const oldContent = entry.content || ""; + const deltaSize = + this._getStringSize(STR2) - this._getStringSize(oldContent); + + if (!this._canAccommodateChange(path, deltaSize)) { + + return; + } + + entry.content = STR2; + const now = Date.now(); + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("Set successful"); + } + + list({ + TYPE, + STR + }) { + + let path = this._normalizePath(STR); + if (!this._isPathDir(path)) { + path += "/"; + } + + this._log("Block: list", TYPE, "under", path); + this.readActivity = true; + const emptyList = []; + + const entry = this.fs.get(path); + if (!entry) { + this._log("List failed: Directory not found."); + return emptyList; + } + + if (!this.hasPermission(path, "see")) { + this._log("List failed: No see permission on directory"); + return emptyList; + } + + entry.accessed = Date.now(); + + let children = new Set(); + const pathLen = path.length; + + for (const itemPath of this.fs.keys()) { + if (itemPath === path || itemPath === "/") continue; + + if (itemPath.startsWith(path)) { + let remainder = itemPath.substring(pathLen); + let nextSlash = remainder.indexOf("/"); + let childName = ""; + let isDir = false; + + if (nextSlash === -1) { + childName = remainder; + isDir = false; + } else { + childName = remainder.substring(0, nextSlash + 1); + isDir = true; + } + + if (childName === "") continue; + + const childPath = `${path}${childName}`; + if (!this.hasPermission(childPath, "see")) { + this._log("List: Skipping item (no see perm):", childPath); + continue; + } + + if (TYPE === "all") children.add(childName); + else if (TYPE === "files" && !isDir) children.add(childName); + else if (TYPE === "directories" && isDir) children.add(childName); + } + } + + const childrenArray = Array.from(children); + this._log("List result (raw):", childrenArray); + return childrenArray; + } + + in({ + STR + }) { + this.lastError = ""; + this._log("Block: import"); + if (!this.hasPermission("/", "delete")) { + return this._setError("Import failed: No 'delete' permission on /"); + } + try { + const data = JSON.parse(STR); + + const version = data ? data.version : null; + if (!version) { + return this._setError("Import failed: Data invalid or missing version."); + } + + let migrationData = {}; + let needsMigration = false; + + if (version === "1.0.5") { + + if (!data.fs || typeof data.fs !== 'object' || Array.isArray(data.fs)) { + return this._setError("Import failed: v1.0.5 data is corrupt (missing 'fs' object)."); + } + migrationData = data.fs; + + } else if (version === "1.0.4" || version === "1.0.3" || version === "1.0.2") { + + this._log(`Import: Migrating v${version} save...`); + needsMigration = true; + if (!Array.isArray(data.sl)) { + this._log(`... adding 'sl' array.`); + data.sl = new Array(data.sy.length).fill(-1); + } + if (!Array.isArray(data.fi) || !Array.isArray(data.sy) || + !Array.isArray(data.pm) || !Array.isArray(data.sl) || + data.fi.length !== data.sy.length || + data.fi.length !== data.pm.length || + data.fi.length !== data.sl.length || + data.sy.indexOf("/") === -1) { + return this._setError("Import failed: Old version data arrays are corrupt or mismatched."); + } + + const now = Date.now(); + data.created = new Array(data.sy.length).fill(now); + data.modified = new Array(data.sy.length).fill(now); + data.accessed = new Array(data.sy.length).fill(now); + + } else { + return this._setError(`Import failed: Incompatible version "${version}". Expected "${extensionVersion}" or older.`); + } + + if (needsMigration) { + this.fs.clear(); + for (let i = 0; i < data.sy.length; i++) { + const perm = data.pm[i]; + const limit = data.sl[i]; + + if ( + typeof data.sy[i] !== "string" || + typeof perm !== "object" || perm === null || Array.isArray(perm) || + typeof limit !== "number" || + typeof perm.create !== "boolean" || typeof perm.delete !== "boolean" || + typeof perm.see !== "boolean" || typeof perm.read !== "boolean" || + typeof perm.write !== "boolean" || typeof perm.control !== "boolean" + ) { + this._setError("Import failed: Corrupt data found in legacy filesystem entries."); + this._internalClean(); + return; + } + this.fs.set(data.sy[i], { + content: data.fi[i], + perms: data.pm[i], + limit: data.sl[i], + created: data.created[i], + modified: data.modified[i], + accessed: data.accessed[i] + }); + } + } else { + + this.fs.clear(); + for (const path in data.fs) { + if (Object.prototype.hasOwnProperty.call(data.fs, path)) { + const entry = data.fs[path]; + + if (!entry || typeof entry.perms !== 'object' || typeof entry.limit !== 'number' || + typeof entry.created !== 'number' || typeof entry.modified !== 'number' || + typeof entry.accessed !== 'number') { + this._setError(`Import failed: Corrupt entry for path "${path}".`); + this._internalClean(); + return; + } + this.fs.set(path, entry); + } + } + if (!this.fs.has("/")) { + this._setError("Import failed: Rebuilt filesystem is missing root '/'."); + this._internalClean(); + return; + } + } + + this.writeActivity = true; + this._log("Import successful"); + } catch (e) { + this._setError( + `Import failed: JSON parse error. File system was not changed.` + ); + } + } + + out() { + this._log("Block: export"); + this.readActivity = true; + + const fsObject = {}; + for (const [path, entry] of this.fs.entries()) { + fsObject[path] = entry; + } + + const result = JSON.stringify({ + version: extensionVersion, + fs: fsObject, + }); + this._log("Export successful, size:", result.length); + return result; + } + + exists({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: exists", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + entry.accessed = Date.now(); + this._log("Result: true"); + return true; + } + + isFile({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: isFile", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + + entry.accessed = Date.now(); + const result = !this._isPathDir(path); + this._log("Result:", result); + return result; + } + + isDir({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: isDir", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + + entry.accessed = Date.now(); + const result = this._isPathDir(path); + this._log("Result:", result); + return result; + } + + setPerm({ + ACTION, + PERM, + STR + }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: setPerm", ACTION, PERM, "for", path); + + if (!this.hasPermission(path, "control")) { + return this._setError(`setPerm failed: No 'control' permission on ${path}`); + } + + const newValue = ACTION === "add"; + const isDir = this._isPathDir(path); + const now = Date.now(); + + this._log("Applying changes..."); + for (const [currentPath, entry] of this.fs.entries()) { + if ( + (isDir && currentPath.startsWith(path)) || + currentPath === path + ) { + entry.perms[PERM] = newValue; + entry.modified = now; + entry.accessed = now; + this._log("Changed perm for:", currentPath); + } + } + this.writeActivity = true; + this._log("setPerm complete"); + } + + listPerms({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: listPerms", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: {} (not found)"); + return JSON.stringify({}); + } + + if (!entry.perms.see) { + this._warn(`See permission denied for "${path}"`); + return JSON.stringify({}); + } + + entry.accessed = Date.now(); + const result = JSON.stringify(entry.perms); + this._log("Result:", result); + return result; + } + + fileName({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: fileName", path); + this.readActivity = true; + + if (!this.hasPermission(path, "see")) { + this._warn(`See permission denied for "${path}"`); + return ""; + } + + const entry = this.fs.get(path); + if (entry) entry.accessed = Date.now(); + + if (path === "/") { + this._log("Result: /"); + return "/"; + } + + let procPath = this._isPathDir(path) ? + path.substring(0, path.length - 1) : + path; + + const lastSlash = procPath.lastIndexOf("/"); + if (lastSlash === -1) { + this._log("Result (no slash):", procPath); + return procPath; + } + const file = procPath.substring(lastSlash + 1); + this._log("Result:", file); + return file; + } + + dirName({ + STR + }) { + const path = this._normalizePath(STR); + this._log("Block: dirName", path); + this.readActivity = true; + + if (!this.hasPermission(path, "see")) { + this._warn(`See permission denied for "${path}"`); + return ""; + } + + const entry = this.fs.get(path); + if (entry) entry.accessed = Date.now(); + + const parent = this._internalDirName(path); + this._log("Result:", parent); + return parent; + } + + toggleLogging({ + STATE + }) { + this.liFSLogEnabled = STATE === "on"; + this._log("Console logging turned", STATE); + } + + setLimit({ + DIR, + BYTES + }) { + this.lastError = ""; + const path = this._normalizePath(DIR); + this._log("Block: setLimit", path, "to", BYTES, "bytes"); + + if (!this._isPathDir(path)) { + return this._setError("setLimit failed: Path must be a directory (end with /)"); + } + if (!this.hasPermission(path, "control")) { + return this._setError(`setLimit failed: No 'control' permission on ${path}`); + } + const entry = this.fs.get(path); + if (!entry) { + return this._setError(`setLimit failed: Directory ${path} not found`); + } + + const limitInBytes = Math.max(-1, parseFloat(BYTES) || 0); + + if (limitInBytes !== -1) { + const currentSize = this._getDirectorySize(path); + if (currentSize > limitInBytes) { + return this._setError( + `setLimit failed: New limit (${limitInBytes} B) is smaller than current directory size (${currentSize} B)` + ); + } + } + + const now = Date.now(); + entry.limit = limitInBytes; + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("setLimit successful"); + } + + removeLimit({ + DIR + }) { + this.lastError = ""; + const path = this._normalizePath(DIR); + this._log("Block: removeLimit", path); + + if (!this._isPathDir(path)) { + return this._setError("removeLimit failed: Path must be a directory (end with /)"); + } + if (!this.hasPermission(path, "control")) { + return this._setError(`removeLimit failed: No 'control' permission on ${path}`); + } + const entry = this.fs.get(path); + if (!entry) { + return this._setError(`removeLimit failed: Directory ${path} not found`); + } + + const now = Date.now(); + entry.limit = -1; + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("removeLimit successful"); + } + + getLimit({ + DIR + }) { + const path = this._normalizePath(DIR); + this._log("Block: getLimit", path); + this.readActivity = true; + + if (!this._isPathDir(path)) { + this._warn("getLimit failed: Path must be a directory (end with /)"); + return -1; + } + if (!this.hasPermission(path, "see")) { + this._warn(`getLimit failed: No 'see' permission for "${path}"`); + return -1; + } + + const entry = this.fs.get(path); + if (!entry) { + this._warn(`getLimit failed: Directory ${path} not found`); + return -1; + } + + entry.accessed = Date.now(); + const limitInBytes = entry.limit; + this._log("getLimit result:", limitInBytes, "bytes"); + return limitInBytes; + } + + getSize({ + DIR + }) { + const path = this._normalizePath(DIR); + this._log("Block: getSize", path); + this.readActivity = true; + + if (!this._isPathDir(path)) { + this._warn("getSize failed: Path must be a directory (end with /)"); + return 0; + } + if (!this.hasPermission(path, "see")) { + this._warn(`getSize failed: No 'see' permission for "${path}"`); + return 0; + } + + const entry = this.fs.get(path); + if (!entry) { + this._warn(`getSize failed: Directory ${path} not found`); + return 0; + } + + entry.accessed = Date.now(); + const sizeInBytes = this._getDirectorySize(path); + this._log("getSize result:", sizeInBytes, "bytes"); + return sizeInBytes; + } + + _getTimestamp(path, type) { + this.readActivity = true; + const entry = this.fs.get(path); + if (!entry) { + this._warn(`Timestamp check failed: ${path} not found.`); + return ""; + } + if (!entry.perms.see) { + this._warn(`Timestamp check failed: No 'see' permission on ${path}.`); + return ""; + } + entry.accessed = Date.now(); + const timestamp = entry[type]; + return new Date(timestamp).toISOString(); + } + + dateCreated({ + STR + }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, 'created'); + } + + dateModified({ + STR + }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, 'modified'); + } + + dateAccessed({ + STR + }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, 'accessed'); + } + + getLastError() { + return this.lastError; + } + + wasRead() { + const val = this.readActivity; + this.readActivity = false; + return val; + } + + wasWritten() { + const val = this.writeActivity; + this.writeActivity = false; + return val; + } + + getVersion() { + return extensionVersion; + } + } + + Scratch.extensions.register(new LiFS()); +})(Scratch); From a41558bcc3a63123a5dde7a3d2666768f0e727a3 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Fri, 21 Nov 2025 18:12:17 -0600 Subject: [PATCH 02/22] fix: Update extensions.json with LiFS 1.0.5 --- extensions/extensions.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/extensions.json b/extensions/extensions.json index 442f300d59..5cfd4698b0 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -79,6 +79,7 @@ "CST1229/zip", "CST1229/images", "TheShovel/LZ-String", + "ohgodwhy2k/lithiumfs", "0832/rxFS2", "NexusKitten/sgrab", "NOname-awa/graphics2d", From e3891019d204b7eabea37059edfe643014657045 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 00:48:50 +0000 Subject: [PATCH 03/22] fix: Prettify lithiumfs.js --- extensions/ohgodwhy2k/lithiumfs.js | 4530 ++++++++++++++-------------- 1 file changed, 2257 insertions(+), 2273 deletions(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index 7e5306a81f..13db33916c 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -6,2278 +6,2262 @@ // License: MIT Scratch.translate.setup({ - "de": { - "clean": "Dateisystem löschen", - "del": "Lösche [STR]", - "folder": "Setze [STR] auf [STR2]", - "folder_default": "LiFS ist gut!", - "in": "Dateisystem von [STR] importieren", - "list": "Alle Dateien unter [STR] auflisten", - "open": "Öffne [STR]", - "out": "Dateisystem exportieren", - "search": "Suche [STR]", - "start": "Erschaffe [STR]", - "sync": "Ändere die Position von [STR] zu [STR2]", - "listMenuAll": "alle", - "listMenuFiles": "dateien", - "listMenuDirs": "verzeichnisse", - "list_new": "liste [TYPE] unter [STR]", - "exists": "existiert [STR]?", - "isFile": "ist [STR] eine datei?", - "isDir": "ist [STR] ein verzeichnis?", - "copy": "kopiere [STR] nach [STR2]", - "fileName": "dateiname von [STR]", - "dirName": "verzeichnis von [STR]", - "sync_new": "benenne [STR] um in [STR2]", - "permSet": "[ACTION] [PERM] Berechtigung für [STR]", - "permAdd": "hinzufügen", - "permRemove": "entfernen", - "permCreate": "erstellen", - "permDelete": "löschen", - "permSee": "sehen", - "permRead": "lesen", - "permWrite": "schreiben", - "permList": "berechtigungen auflisten für [STR]", - "permControl": "kontrollieren", - "toggleLogging": "schalte [STATE] konsolen-logging", - "logOn": "an", - "logOff": "aus", - "setLimit": "setze größenlimit für [DIR] auf [BYTES] bytes", - "removeLimit": "entferne größenlimit für [DIR]", - "getLimit": "größenlimit von [DIR] (bytes)", - "getSize": "aktuelle größe von [DIR] (bytes)", - "getLastError": "letzter fehler", - "wasRead": "wurde gelesen?", - "wasWritten": "wurde geschrieben?", - "version": "Version", - "dateCreated": "erstellungsdatum von [STR]", - "dateModified": "änderungsdatum von [STR]", - "dateAccessed": "zugriffsdatum von [STR]" - }, - "es": { - "folder_default": "¡LiFS es bueno!", - "listMenuAll": "todo", - "listMenuFiles": "archivos", - "listMenuDirs": "directorios", - "list_new": "listar [TYPE] en [STR]", - "exists": "¿existe [STR]?", - "isFile": "¿es [STR] un archivo?", - "isDir": "¿es [STR] un directorio?", - "copy": "copiar [STR] a [STR2]", - "fileName": "nombre de archivo de [STR]", - "dirName": "directorio de [STR]", - "sync_new": "renombrar [STR] a [STR2]", - "permSet": "[ACTION] permiso de [PERM] a [STR]", - "permAdd": "añadir", - "permRemove": "quitar", - "permCreate": "crear", - "permDelete": "eliminar", - "permSee": "ver", - "permRead": "leer", - "permWrite": "escribir", - "permList": "listar permisos de [STR]", - "permControl": "controlar", - "toggleLogging": "[STATE] el registro de la consola", - "logOn": "activar", - "logOff": "desactivar", - "setLimit": "establecer límite de tamaño para [DIR] a [BYTES] bytes", - "removeLimit": "eliminar límite de tamaño para [DIR]", - "getLimit": "límite de tamaño de [DIR] (bytes)", - "getSize": "tamaño actual de [DIR] (bytes)", - "getLastError": "último error", - "wasRead": "¿fue leído?", - "wasWritten": "¿fue escrito?", - "version": "versión", - "dateCreated": "fecha de creación de [STR]", - "dateModified": "fecha de modificación de [STR]", - "dateAccessed": "fecha de acceso de [STR]" - }, - "fi": { - "clean": "tyhjennä tiedostojärjestelmä", - "del": "poista [STR]", - "folder": "aseta [STR] arvoon [STR2]", - "folder_default": "LiFS on hieno!", - "in": "tuo tiedostojärjestelmä kohteesta [STR]", - "list": "luettelo kaikista kohteessa [STR] sijaitsevista tiedostoista", - "open": "avaa [STR]", - "out": "vie tiedostojärjestelmä", - "search": "etsi [STR]", - "start": "luo [STR]", - "sync": "muuta kohteen [STR] sijainniksi [STR2]", - "listMenuAll": "kaikki", - "listMenuFiles": "tiedostot", - "listMenuDirs": "kansiot", - "list_new": "listaa [TYPE] polussa [STR]", - "exists": "onko [STR] olemassa?", - "isFile": "onko [STR] tiedosto?", - "isDir": "onko [STR] kansio?", - "copy": "kopioi [STR] kohteeseen [STR2]", - "fileName": "tiedostonimi [STR]", - "dirName": "kansio [STR]", - "sync_new": "nimeä [STR] uudelleen [STR2]", - "permSet": "[ACTION] [PERM] käyttöoikeuden kohteelle [STR]", - "permAdd": "lisää", - "permRemove": "poista", - "permCreate": "luo", - "permDelete": "poista", - "permSee": "nähdä", - "permRead": "lukea", - "permWrite": "kirjoittaa", - "permList": "listaa [STR] käyttöoikeudet", - "permControl": "hallita", - "toggleLogging": "laita [STATE] konsoliloki", - "logOn": "päälle", - "logOff": "pois päältä", - "getLastError": "viimeisin virhe", - "wasRead": "luettiinko?", - "wasWritten": "kirjoitettiinko?", - "version": "versio", - "dateCreated": "luontipäivä [STR]", - "dateModified": "muokkauspäivä [STR]", - "dateAccessed": "käyttöpäivä [STR]" - }, - "fr": { - "clean": "effacer le système de fichiers", - "del": "supprimer [STR]", - "folder": "mettre [STR] à [STR2]", - "folder_default": "LiFS est bon !", - "in": "importer le système de fichier depuis [STR]", - "list": "lister tous les fichiers sous [STR]", - "open": "ouvrir [STR]", - "out": "exporter le système de fichiers", - "search": "chercher [STR]", - "start": "créer [STR]", - "sync": "modifier l'emplacement de [STR] à [STR2]", - "listMenuAll": "tout", - "listMenuFiles": "fichiers", - "listMenuDirs": "dossiers", - "list_new": "lister [TYPE] sous [STR]", - "exists": "[STR] existe-t-il?", - "isFile": "[STR] est-il un fichier?", - "isDir": "[STR] est-il un dossier?", - "copy": "copier [STR] vers [STR2]", - "fileName": "nom de fichier de [STR]", - "dirName": "dossier de [STR]", - "sync_new": "renommer [STR] en [STR2]", - "permSet": "[ACTION] la permission [PERM] à [STR]", - "permAdd": "ajouter", - "permRemove": "supprimer", - "permCreate": "crer", - "permDelete": "supprimer", - "permSee": "voir", - "permRead": "lire", - "permWrite": "écrire", - "permList": "lister les permissions de [STR]", - "permControl": "contrôler", - "toggleLogging": "[STATE] la journalisation de la console", - "logOn": "activer", - "logOff": "désactiver", - "setLimit": "définir la limite de taille pour [DIR] à [BYTES] octets", - "removeLimit": "supprimer la limite de taille pour [DIR]", - "getLimit": "limite de taille de [DIR] (octets)", - "getSize": "taille actuelle de [DIR] (octets)", - "getLastError": "dernière erreur", - "wasRead": "lu ?", - "wasWritten": "écrit ?", - "version": "version", - "dateCreated": "date de création de [STR]", - "dateModified": "date de modification de [STR]", - "dateAccessed": "date d'accès de [STR]" - }, - "it": { - "clean": "svuota file system", - "del": "cancella [STR]", - "folder": "imposta [STR] a [STR2]", - "folder_default": "LiFS funziona!", - "in": "importa file system da [STR]", - "list": "elenca tutti i file in [STR]", - "open": "apri [STR]", - "out": "esporta file system", - "search": "cerca [STR]", - "start": "crea [STR]", - "sync": "cambia posizione di [STR] a [STR2]", - "listMenuAll": "tutti", - "listMenuFiles": "file", - "listMenuDirs": "directory", - "list_new": "elenca [TYPE] in [STR]", - "exists": "[STR] esiste?", - "isFile": "[STR] è un file?", - "isDir": "[STR] è una directory?", - "copy": "copia [STR] in [STR2]", - "fileName": "nome file di [STR]", - "dirName": "directory di [STR]", - "sync_new": "rinomina [STR] in [STR2]", - "permSet": "[ACTION] permesso [PERM] a [STR]", - "permAdd": "aggiungi", - "permRemove": "rimuovi", - "permCreate": "crea", - "permDelete": "elimina", - "permSee": "vedi", - "permRead": "leggi", - "permWrite": "scrivi", - "permList": "elenca permessi per [STR]", - "permControl": "controllare", - "toggleLogging": "[STATE] log console", - "logOn": "attiva", - "logOff": "disattiva", - "getLastError": "ultimo errore", - "wasRead": "è stato letto?", - "wasWritten": "è stato scritto?", - "version": "versione", - "dateCreated": "data creazione di [STR]", - "dateModified": "data modifica di [STR]", - "dateAccessed": "data accesso di [STR]" - }, - "ja": { - "clean": "ファイルシステムを削除する", - "del": "[STR]を削除", - "folder": "[STR]を[STR2]にセットする", - "folder_default": "LiFSは良い!", - "in": "[STR]からファイルシステムをインポートする", - "list": "[STR]直下のファイルをリスト化する", - "open": "[STR]を開く", - "out": "ファイルシステムをエクスポートする", - "search": "[STR]を検索", - "start": "[STR]を作成", - "sync": "[STR]のロケーションを[STR2]に変更する", - "listMenuAll": "すべて", - "listMenuFiles": "ファイル", - "listMenuDirs": "ディレクトリ", - "list_new": "[STR] の [TYPE] を一覧表示", - "exists": "[STR] は存在しますか?", - "isFile": "[STR] はファイルですか?", - "isDir": "[STR] はディレクトリですか?", - "copy": "[STR] を [STR2] にコピー", - "fileName": "[STR] のファイル名", - "dirName": "[STR] のディレクトリ", - "sync_new": "[STR] を [STR2] に名前変更", - "permSet": "[STR] の [PERM] 権限を [ACTION]", - "permAdd": "追加", - "permRemove": "削除", - "permCreate": "作成", - "permDelete": "削除", - "permSee": "表示", - "permRead": "読み取り", - "permWrite": "書き込み", - "permList": "[STR] の権限を一覧表示", - "permControl": "制御", - "toggleLogging": "コンソールログを [STATE] にする", - "logOn": "オン", - "logOff": "オフ", - "getLastError": "最後のエラー", - "wasRead": "読み込まれたか?", - "wasWritten": "書き込まれたか?", - "version": "バージョン", - "dateCreated": "[STR]の作成日時", - "dateModified": "[STR]の変更日時", - "dateAccessed": "[STR]のアクセス日時" - }, - "ko": { - "clean": "파일 システム 초기화하기", - "del": "[STR] 삭제하기", - "folder": "[STR]을(를) [STR2](으)로 정하기", - "folder_default": "LiFS 최고!", - "in": "[STR]에서 파일 システム 불러오기", - "list": "[STR] 안의 파일 목록", - "open": "[STR] 열기", - "out": "파일 システム 내보내기", - "search": "[STR] 검색하기", - "start": "[STR] 생성하기", - "sync": "[STR]의 경로를 [STR2](으)로 바꾸기", - "listMenuAll": "모두", - "listMenuFiles": "파일", - "listMenuDirs": "디렉터리", - "list_new": "[STR]의 [TYPE] 목록", - "exists": "[STR]이(가) 존재하나요?", - "isFile": "[STR]이(가) 파일인가요?", - "isDir": "[STR]이(가) 디렉터리인가요?", - "copy": "[STR]을(를) [STR2](으)로 복사하기", - "fileName": "[STR]의 파일 이름", - "dirName": "[STR]의 디렉터리", - "sync_new": "[STR]의 이름을 [STR2](으)로 바꾸기", - "permSet": "[STR]에 [PERM] 권한 [ACTION]", - "permAdd": "추가하기", - "permRemove": "제거하기", - "permCreate": "생성", - "permDelete": "삭제", - "permSee": "보기", - "permRead": "읽기", - "permWrite": "쓰기", - "permList": "[STR]의 권한 목록", - "permControl": "제어", - "toggleLogging": "콘솔 로깅 [STATE]", - "logOn": "켜기", - "logOff": "끄기", - "getLastError": "마지막 오류", - "wasRead": "읽었나요?", - "wasWritten": "작성했나요?", - "version": "버전", - "dateCreated": "[STR]의 생성 날짜", - "dateModified": "[STR]의 수정 날짜", - "dateAccessed": "[STR]의 접근 날짜" - }, - "nb": { - "folder_default": "LiFS er bra!", - "listMenuAll": "alle", - "listMenuFiles": "filer", - "listMenuDirs": "mapper", - "list_new": "list [TYPE] under [STR]", - "exists": "finnes [STR]?", - "isFile": "er [STR] en fil?", - "isDir": "er [STR] en mappe?", - "copy": "kopier [STR] til [STR2]", - "fileName": "filnavn til [STR]", - "dirName": "mappe til [STR]", - "sync_new": "gi [STR] nytt navn [STR2]", - "permSet": "[ACTION] [PERM] tillatelse til [STR]", - "permAdd": "legg til", - "permRemove": "fjern", - "permCreate": "opprett", - "permDelete": "slett", - "permSee": "se", - "permRead": "les", - "permWrite": "skriv", - "permList": "list tillatelser for [STR]", - "permControl": "kontroll", - "toggleLogging": "slå [STATE] konsolllogging", - "logOn": "på", - "logOff": "av", - "getLastError": "siste feil", - "wasRead": "ble lest?", - "wasWritten": "ble skrevet?", - "version": "versjon", - "dateCreated": "opprettelsesdato for [STR]", - "dateModified": "endringsdato for [STR]", - "dateAccessed": "tilgangsdato for [STR]" - }, - "nl": { - "clean": "wis het bestandssysteem", - "del": "verwijder [STR]", - "folder": "maak [STR] [STR2]", - "folder_default": "LiFS is geweldig!", - "in": "importeer bestandssysteem van [STR]", - "list": "alle bestanden onder [STR]", - "out": "exporteer bestandssysteem", - "search": "zoek [STR]", - "start": "creëer [STR]", - "sync": "verander locatie van [STR] naar [STR2]", - "listMenuAll": "alles", - "listMenuFiles": "bestanden", - "listMenuDirs": "mappen", - "list_new": "lijst [TYPE] onder [STR]", - "exists": "bestaat [STR]?", - "isFile": "is [STR] een bestand?", - "isDir": "is [STR] een map?", - "copy": "kopieer [STR] naar [STR2]", - "fileName": "bestandsnaam van [STR]", - "dirName": "map van [STR]", - "sync_new": "hernoem [STR] naar [STR2]", - "permSet": "[ACTION] [PERM] toestemming om [STR]", - "permAdd": "toevoegen", - "permRemove": "verwijderen", - "permCreate": "maken", - "permDelete": "verwijderen", - "permSee": "zien", - "permRead": "lezen", - "permWrite": "schrijven", - "permList": "lijst toestemmingen for [STR]", - "permControl": "beheren", - "toggleLogging": "zet console logging [STATE]", - "logOn": "aan", - "logOff": "uit", - "getLastError": "laatste fout", - "wasRead": "is gelezen?", - "wasWritten": "is geschreven?", - "version": "versie", - "dateCreated": "aanmaakdatum van [STR]", - "dateModified": "wijzigingsdatum van [STR]", - "dateAccessed": "toegangsdatum van [STR]" - }, - "pl": { - "del": "usuń [STR]", - "folder": "ustaw [STR] na [STR2]", - "open": "otwórz [STR]", - "search": "szukaj [STR]", - "listMenuAll": "wszystko", - "listMenuFiles": "pliki", - "listMenuDirs": "katalogi", - "list_new": "listuj [TYPE] w [STR]", - "exists": "czy [STR] istnieje?", - "isFile": "czy [STR] to plik?", - "isDir": "czy [STR] to katalog?", - "copy": "kopiej [STR] do [STR2]", - "fileName": "nazwa pliku [STR]", - "dirName": "katalog [STR]", - "sync_new": "zmień nazwę [STR] na [STR2]", - "permSet": "[ACTION] [PERM] uprawnienie do [STR]", - "permAdd": "dodaj", - "permRemove": "usuń", - "permCreate": "tworzenie", - "permDelete": "usuwanie", - "permSee": "przeglądanie", - "permRead": "czytanie", - "permWrite": "pisanie", - "permList": "listuj uprawnienia [STR]", - "permControl": "kontrola", - "toggleLogging": "włącz [STATE] logowanie konsoli", - "logOn": "włącz", - "logOff": "wyłącz", - "getLastError": "ostatni błąd", - "wasRead": "czytano?", - "wasWritten": "pisano?", - "version": "wersja", - "dateCreated": "data utworzenia [STR]", - "dateModified": "data modyfikacji [STR]", - "dateAccessed": "data dostępu [STR]" - }, - "ru": { - "clean": "очистить файловую систему", - "del": "удалить [STR]", - "folder": "задать [STR] значение [STR2]", - "folder_default": "LiFS это хорошо!", - "in": "импортировать файловую систему из [STR]", - "list": "перечислить все файлы под [STR]", - "open": "открыть [STR]", - "out": "экспортировать файловую систему", - "search": "поиск [STR]", - "start": "создать [STR]", - "sync": "изменить расположение [STR] на [STR2]", - "listMenuAll": "все", - "listMenuFiles": "файлы", - "listMenuDirs": "папки", - "list_new": "список [TYPE] в [STR]", - "exists": "[STR] существует?", - "isFile": "[STR] это файл?", - "isDir": "[STR] это папка?", - "copy": "копировать [STR] в [STR2]", - "fileName": "имя файла [STR]", - "dirName": "папка [STR]", - "sync_new": "переименовать [STR] в [STR2]", - "permSet": "[ACTION] [PERM] разрешение для [STR]", - "permAdd": "добавить", - "permRemove": "удалить", - "permCreate": "создать", - "permDelete": "удалить", - "permSee": "видеть", - "permRead": "читать", - "permWrite": "писать", - "permList": "список разрешений для [STR]", - "permControl": "управлять", - "toggleLogging": "[STATE] ведение журнала консоли", - "logOn": "включить", - "logOff": "выключить", - "getLastError": "последняя ошибка", - "wasRead": "было чтение?", - "wasWritten": "была запись?", - "version": "версия", - "dateCreated": "дата создания [STR]", - "dateModified": "дата изменения [STR]", - "dateAccessed": "дата доступа [STR]" - }, - "zh-cn": { - "clean": "清空文件System", - "del": "删除 [STR]", - "folder": "将[STR]设为[STR2]", - "folder_default": "LiFS 好用!", - "in": "从 [STR] 导入文件System", - "list": "列出 [STR] 下的所有文件", - "open": "打开 [STR]", - "out": "导出文件 system", - "search": "搜索 [STR]", - "start": "新建 [STR]", - "sync": "将 [STR] 的位置改为 [STR2]", - "listMenuAll": "所有", - "listMenuFiles": "文件", - "listMenuDirs": "目录", - "list_new": "列出 [STR] 下的 [TYPE]", - "exists": "[STR] 是否存在?", - "isFile": "[STR] 是文件吗?", - "isDir": "[STR] 是目录吗?", - "copy": "将 [STR] 复制到 [STR2]", - "fileName": "[STR] 的文件名", - "dirName": "[STR] 的目录", - "sync_new": "将 [STR] 重命名为 [STR2]", - "permSet": "[ACTION] [STR] 的 [PERM] 权限", - "permAdd": "添加", - "permRemove": "移除", - "permCreate": "创建", - "permDelete": "删除", - "permSee": "查看", - "permRead": "读取", - "permWrite": "写入", - "permList": "列出 [STR] 的权限", - "permControl": "控制", - "toggleLogging": "[STATE]控制台日志", - "logOn": "开启", - "logOff": "关闭", - "getLastError": "上一个错误", - "wasRead": "是否读取?", - "wasWritten": "是否写入?", - "version": "版本", - "dateCreated": "[STR] 的创建日期", - "dateModified": "[STR] 的修改日期", - "dateAccessed": "[STR] 的访问日期" - } + de: { + clean: "Dateisystem löschen", + del: "Lösche [STR]", + folder: "Setze [STR] auf [STR2]", + folder_default: "LiFS ist gut!", + in: "Dateisystem von [STR] importieren", + list: "Alle Dateien unter [STR] auflisten", + open: "Öffne [STR]", + out: "Dateisystem exportieren", + search: "Suche [STR]", + start: "Erschaffe [STR]", + sync: "Ändere die Position von [STR] zu [STR2]", + listMenuAll: "alle", + listMenuFiles: "dateien", + listMenuDirs: "verzeichnisse", + list_new: "liste [TYPE] unter [STR]", + exists: "existiert [STR]?", + isFile: "ist [STR] eine datei?", + isDir: "ist [STR] ein verzeichnis?", + copy: "kopiere [STR] nach [STR2]", + fileName: "dateiname von [STR]", + dirName: "verzeichnis von [STR]", + sync_new: "benenne [STR] um in [STR2]", + permSet: "[ACTION] [PERM] Berechtigung für [STR]", + permAdd: "hinzufügen", + permRemove: "entfernen", + permCreate: "erstellen", + permDelete: "löschen", + permSee: "sehen", + permRead: "lesen", + permWrite: "schreiben", + permList: "berechtigungen auflisten für [STR]", + permControl: "kontrollieren", + toggleLogging: "schalte [STATE] konsolen-logging", + logOn: "an", + logOff: "aus", + setLimit: "setze größenlimit für [DIR] auf [BYTES] bytes", + removeLimit: "entferne größenlimit für [DIR]", + getLimit: "größenlimit von [DIR] (bytes)", + getSize: "aktuelle größe von [DIR] (bytes)", + getLastError: "letzter fehler", + wasRead: "wurde gelesen?", + wasWritten: "wurde geschrieben?", + version: "Version", + dateCreated: "erstellungsdatum von [STR]", + dateModified: "änderungsdatum von [STR]", + dateAccessed: "zugriffsdatum von [STR]", + }, + es: { + folder_default: "¡LiFS es bueno!", + listMenuAll: "todo", + listMenuFiles: "archivos", + listMenuDirs: "directorios", + list_new: "listar [TYPE] en [STR]", + exists: "¿existe [STR]?", + isFile: "¿es [STR] un archivo?", + isDir: "¿es [STR] un directorio?", + copy: "copiar [STR] a [STR2]", + fileName: "nombre de archivo de [STR]", + dirName: "directorio de [STR]", + sync_new: "renombrar [STR] a [STR2]", + permSet: "[ACTION] permiso de [PERM] a [STR]", + permAdd: "añadir", + permRemove: "quitar", + permCreate: "crear", + permDelete: "eliminar", + permSee: "ver", + permRead: "leer", + permWrite: "escribir", + permList: "listar permisos de [STR]", + permControl: "controlar", + toggleLogging: "[STATE] el registro de la consola", + logOn: "activar", + logOff: "desactivar", + setLimit: "establecer límite de tamaño para [DIR] a [BYTES] bytes", + removeLimit: "eliminar límite de tamaño para [DIR]", + getLimit: "límite de tamaño de [DIR] (bytes)", + getSize: "tamaño actual de [DIR] (bytes)", + getLastError: "último error", + wasRead: "¿fue leído?", + wasWritten: "¿fue escrito?", + version: "versión", + dateCreated: "fecha de creación de [STR]", + dateModified: "fecha de modificación de [STR]", + dateAccessed: "fecha de acceso de [STR]", + }, + fi: { + clean: "tyhjennä tiedostojärjestelmä", + del: "poista [STR]", + folder: "aseta [STR] arvoon [STR2]", + folder_default: "LiFS on hieno!", + in: "tuo tiedostojärjestelmä kohteesta [STR]", + list: "luettelo kaikista kohteessa [STR] sijaitsevista tiedostoista", + open: "avaa [STR]", + out: "vie tiedostojärjestelmä", + search: "etsi [STR]", + start: "luo [STR]", + sync: "muuta kohteen [STR] sijainniksi [STR2]", + listMenuAll: "kaikki", + listMenuFiles: "tiedostot", + listMenuDirs: "kansiot", + list_new: "listaa [TYPE] polussa [STR]", + exists: "onko [STR] olemassa?", + isFile: "onko [STR] tiedosto?", + isDir: "onko [STR] kansio?", + copy: "kopioi [STR] kohteeseen [STR2]", + fileName: "tiedostonimi [STR]", + dirName: "kansio [STR]", + sync_new: "nimeä [STR] uudelleen [STR2]", + permSet: "[ACTION] [PERM] käyttöoikeuden kohteelle [STR]", + permAdd: "lisää", + permRemove: "poista", + permCreate: "luo", + permDelete: "poista", + permSee: "nähdä", + permRead: "lukea", + permWrite: "kirjoittaa", + permList: "listaa [STR] käyttöoikeudet", + permControl: "hallita", + toggleLogging: "laita [STATE] konsoliloki", + logOn: "päälle", + logOff: "pois päältä", + getLastError: "viimeisin virhe", + wasRead: "luettiinko?", + wasWritten: "kirjoitettiinko?", + version: "versio", + dateCreated: "luontipäivä [STR]", + dateModified: "muokkauspäivä [STR]", + dateAccessed: "käyttöpäivä [STR]", + }, + fr: { + clean: "effacer le système de fichiers", + del: "supprimer [STR]", + folder: "mettre [STR] à [STR2]", + folder_default: "LiFS est bon !", + in: "importer le système de fichier depuis [STR]", + list: "lister tous les fichiers sous [STR]", + open: "ouvrir [STR]", + out: "exporter le système de fichiers", + search: "chercher [STR]", + start: "créer [STR]", + sync: "modifier l'emplacement de [STR] à [STR2]", + listMenuAll: "tout", + listMenuFiles: "fichiers", + listMenuDirs: "dossiers", + list_new: "lister [TYPE] sous [STR]", + exists: "[STR] existe-t-il?", + isFile: "[STR] est-il un fichier?", + isDir: "[STR] est-il un dossier?", + copy: "copier [STR] vers [STR2]", + fileName: "nom de fichier de [STR]", + dirName: "dossier de [STR]", + sync_new: "renommer [STR] en [STR2]", + permSet: "[ACTION] la permission [PERM] à [STR]", + permAdd: "ajouter", + permRemove: "supprimer", + permCreate: "crer", + permDelete: "supprimer", + permSee: "voir", + permRead: "lire", + permWrite: "écrire", + permList: "lister les permissions de [STR]", + permControl: "contrôler", + toggleLogging: "[STATE] la journalisation de la console", + logOn: "activer", + logOff: "désactiver", + setLimit: "définir la limite de taille pour [DIR] à [BYTES] octets", + removeLimit: "supprimer la limite de taille pour [DIR]", + getLimit: "limite de taille de [DIR] (octets)", + getSize: "taille actuelle de [DIR] (octets)", + getLastError: "dernière erreur", + wasRead: "lu ?", + wasWritten: "écrit ?", + version: "version", + dateCreated: "date de création de [STR]", + dateModified: "date de modification de [STR]", + dateAccessed: "date d'accès de [STR]", + }, + it: { + clean: "svuota file system", + del: "cancella [STR]", + folder: "imposta [STR] a [STR2]", + folder_default: "LiFS funziona!", + in: "importa file system da [STR]", + list: "elenca tutti i file in [STR]", + open: "apri [STR]", + out: "esporta file system", + search: "cerca [STR]", + start: "crea [STR]", + sync: "cambia posizione di [STR] a [STR2]", + listMenuAll: "tutti", + listMenuFiles: "file", + listMenuDirs: "directory", + list_new: "elenca [TYPE] in [STR]", + exists: "[STR] esiste?", + isFile: "[STR] è un file?", + isDir: "[STR] è una directory?", + copy: "copia [STR] in [STR2]", + fileName: "nome file di [STR]", + dirName: "directory di [STR]", + sync_new: "rinomina [STR] in [STR2]", + permSet: "[ACTION] permesso [PERM] a [STR]", + permAdd: "aggiungi", + permRemove: "rimuovi", + permCreate: "crea", + permDelete: "elimina", + permSee: "vedi", + permRead: "leggi", + permWrite: "scrivi", + permList: "elenca permessi per [STR]", + permControl: "controllare", + toggleLogging: "[STATE] log console", + logOn: "attiva", + logOff: "disattiva", + getLastError: "ultimo errore", + wasRead: "è stato letto?", + wasWritten: "è stato scritto?", + version: "versione", + dateCreated: "data creazione di [STR]", + dateModified: "data modifica di [STR]", + dateAccessed: "data accesso di [STR]", + }, + ja: { + clean: "ファイルシステムを削除する", + del: "[STR]を削除", + folder: "[STR]を[STR2]にセットする", + folder_default: "LiFSは良い!", + in: "[STR]からファイルシステムをインポートする", + list: "[STR]直下のファイルをリスト化する", + open: "[STR]を開く", + out: "ファイルシステムをエクスポートする", + search: "[STR]を検索", + start: "[STR]を作成", + sync: "[STR]のロケーションを[STR2]に変更する", + listMenuAll: "すべて", + listMenuFiles: "ファイル", + listMenuDirs: "ディレクトリ", + list_new: "[STR] の [TYPE] を一覧表示", + exists: "[STR] は存在しますか?", + isFile: "[STR] はファイルですか?", + isDir: "[STR] はディレクトリですか?", + copy: "[STR] を [STR2] にコピー", + fileName: "[STR] のファイル名", + dirName: "[STR] のディレクトリ", + sync_new: "[STR] を [STR2] に名前変更", + permSet: "[STR] の [PERM] 権限を [ACTION]", + permAdd: "追加", + permRemove: "削除", + permCreate: "作成", + permDelete: "削除", + permSee: "表示", + permRead: "読み取り", + permWrite: "書き込み", + permList: "[STR] の権限を一覧表示", + permControl: "制御", + toggleLogging: "コンソールログを [STATE] にする", + logOn: "オン", + logOff: "オフ", + getLastError: "最後のエラー", + wasRead: "読み込まれたか?", + wasWritten: "書き込まれたか?", + version: "バージョン", + dateCreated: "[STR]の作成日時", + dateModified: "[STR]の変更日時", + dateAccessed: "[STR]のアクセス日時", + }, + ko: { + clean: "파일 システム 초기화하기", + del: "[STR] 삭제하기", + folder: "[STR]을(를) [STR2](으)로 정하기", + folder_default: "LiFS 최고!", + in: "[STR]에서 파일 システム 불러오기", + list: "[STR] 안의 파일 목록", + open: "[STR] 열기", + out: "파일 システム 내보내기", + search: "[STR] 검색하기", + start: "[STR] 생성하기", + sync: "[STR]의 경로를 [STR2](으)로 바꾸기", + listMenuAll: "모두", + listMenuFiles: "파일", + listMenuDirs: "디렉터리", + list_new: "[STR]의 [TYPE] 목록", + exists: "[STR]이(가) 존재하나요?", + isFile: "[STR]이(가) 파일인가요?", + isDir: "[STR]이(가) 디렉터리인가요?", + copy: "[STR]을(를) [STR2](으)로 복사하기", + fileName: "[STR]의 파일 이름", + dirName: "[STR]의 디렉터리", + sync_new: "[STR]의 이름을 [STR2](으)로 바꾸기", + permSet: "[STR]에 [PERM] 권한 [ACTION]", + permAdd: "추가하기", + permRemove: "제거하기", + permCreate: "생성", + permDelete: "삭제", + permSee: "보기", + permRead: "읽기", + permWrite: "쓰기", + permList: "[STR]의 권한 목록", + permControl: "제어", + toggleLogging: "콘솔 로깅 [STATE]", + logOn: "켜기", + logOff: "끄기", + getLastError: "마지막 오류", + wasRead: "읽었나요?", + wasWritten: "작성했나요?", + version: "버전", + dateCreated: "[STR]의 생성 날짜", + dateModified: "[STR]의 수정 날짜", + dateAccessed: "[STR]의 접근 날짜", + }, + nb: { + folder_default: "LiFS er bra!", + listMenuAll: "alle", + listMenuFiles: "filer", + listMenuDirs: "mapper", + list_new: "list [TYPE] under [STR]", + exists: "finnes [STR]?", + isFile: "er [STR] en fil?", + isDir: "er [STR] en mappe?", + copy: "kopier [STR] til [STR2]", + fileName: "filnavn til [STR]", + dirName: "mappe til [STR]", + sync_new: "gi [STR] nytt navn [STR2]", + permSet: "[ACTION] [PERM] tillatelse til [STR]", + permAdd: "legg til", + permRemove: "fjern", + permCreate: "opprett", + permDelete: "slett", + permSee: "se", + permRead: "les", + permWrite: "skriv", + permList: "list tillatelser for [STR]", + permControl: "kontroll", + toggleLogging: "slå [STATE] konsolllogging", + logOn: "på", + logOff: "av", + getLastError: "siste feil", + wasRead: "ble lest?", + wasWritten: "ble skrevet?", + version: "versjon", + dateCreated: "opprettelsesdato for [STR]", + dateModified: "endringsdato for [STR]", + dateAccessed: "tilgangsdato for [STR]", + }, + nl: { + clean: "wis het bestandssysteem", + del: "verwijder [STR]", + folder: "maak [STR] [STR2]", + folder_default: "LiFS is geweldig!", + in: "importeer bestandssysteem van [STR]", + list: "alle bestanden onder [STR]", + out: "exporteer bestandssysteem", + search: "zoek [STR]", + start: "creëer [STR]", + sync: "verander locatie van [STR] naar [STR2]", + listMenuAll: "alles", + listMenuFiles: "bestanden", + listMenuDirs: "mappen", + list_new: "lijst [TYPE] onder [STR]", + exists: "bestaat [STR]?", + isFile: "is [STR] een bestand?", + isDir: "is [STR] een map?", + copy: "kopieer [STR] naar [STR2]", + fileName: "bestandsnaam van [STR]", + dirName: "map van [STR]", + sync_new: "hernoem [STR] naar [STR2]", + permSet: "[ACTION] [PERM] toestemming om [STR]", + permAdd: "toevoegen", + permRemove: "verwijderen", + permCreate: "maken", + permDelete: "verwijderen", + permSee: "zien", + permRead: "lezen", + permWrite: "schrijven", + permList: "lijst toestemmingen for [STR]", + permControl: "beheren", + toggleLogging: "zet console logging [STATE]", + logOn: "aan", + logOff: "uit", + getLastError: "laatste fout", + wasRead: "is gelezen?", + wasWritten: "is geschreven?", + version: "versie", + dateCreated: "aanmaakdatum van [STR]", + dateModified: "wijzigingsdatum van [STR]", + dateAccessed: "toegangsdatum van [STR]", + }, + pl: { + del: "usuń [STR]", + folder: "ustaw [STR] na [STR2]", + open: "otwórz [STR]", + search: "szukaj [STR]", + listMenuAll: "wszystko", + listMenuFiles: "pliki", + listMenuDirs: "katalogi", + list_new: "listuj [TYPE] w [STR]", + exists: "czy [STR] istnieje?", + isFile: "czy [STR] to plik?", + isDir: "czy [STR] to katalog?", + copy: "kopiej [STR] do [STR2]", + fileName: "nazwa pliku [STR]", + dirName: "katalog [STR]", + sync_new: "zmień nazwę [STR] na [STR2]", + permSet: "[ACTION] [PERM] uprawnienie do [STR]", + permAdd: "dodaj", + permRemove: "usuń", + permCreate: "tworzenie", + permDelete: "usuwanie", + permSee: "przeglądanie", + permRead: "czytanie", + permWrite: "pisanie", + permList: "listuj uprawnienia [STR]", + permControl: "kontrola", + toggleLogging: "włącz [STATE] logowanie konsoli", + logOn: "włącz", + logOff: "wyłącz", + getLastError: "ostatni błąd", + wasRead: "czytano?", + wasWritten: "pisano?", + version: "wersja", + dateCreated: "data utworzenia [STR]", + dateModified: "data modyfikacji [STR]", + dateAccessed: "data dostępu [STR]", + }, + ru: { + clean: "очистить файловую систему", + del: "удалить [STR]", + folder: "задать [STR] значение [STR2]", + folder_default: "LiFS это хорошо!", + in: "импортировать файловую систему из [STR]", + list: "перечислить все файлы под [STR]", + open: "открыть [STR]", + out: "экспортировать файловую систему", + search: "поиск [STR]", + start: "создать [STR]", + sync: "изменить расположение [STR] на [STR2]", + listMenuAll: "все", + listMenuFiles: "файлы", + listMenuDirs: "папки", + list_new: "список [TYPE] в [STR]", + exists: "[STR] существует?", + isFile: "[STR] это файл?", + isDir: "[STR] это папка?", + copy: "копировать [STR] в [STR2]", + fileName: "имя файла [STR]", + dirName: "папка [STR]", + sync_new: "переименовать [STR] в [STR2]", + permSet: "[ACTION] [PERM] разрешение для [STR]", + permAdd: "добавить", + permRemove: "удалить", + permCreate: "создать", + permDelete: "удалить", + permSee: "видеть", + permRead: "читать", + permWrite: "писать", + permList: "список разрешений для [STR]", + permControl: "управлять", + toggleLogging: "[STATE] ведение журнала консоли", + logOn: "включить", + logOff: "выключить", + getLastError: "последняя ошибка", + wasRead: "было чтение?", + wasWritten: "была запись?", + version: "версия", + dateCreated: "дата создания [STR]", + dateModified: "дата изменения [STR]", + dateAccessed: "дата доступа [STR]", + }, + "zh-cn": { + clean: "清空文件System", + del: "删除 [STR]", + folder: "将[STR]设为[STR2]", + folder_default: "LiFS 好用!", + in: "从 [STR] 导入文件System", + list: "列出 [STR] 下的所有文件", + open: "打开 [STR]", + out: "导出文件 system", + search: "搜索 [STR]", + start: "新建 [STR]", + sync: "将 [STR] 的位置改为 [STR2]", + listMenuAll: "所有", + listMenuFiles: "文件", + listMenuDirs: "目录", + list_new: "列出 [STR] 下的 [TYPE]", + exists: "[STR] 是否存在?", + isFile: "[STR] 是文件吗?", + isDir: "[STR] 是目录吗?", + copy: "将 [STR] 复制到 [STR2]", + fileName: "[STR] 的文件名", + dirName: "[STR] 的目录", + sync_new: "将 [STR] 重命名为 [STR2]", + permSet: "[ACTION] [STR] 的 [PERM] 权限", + permAdd: "添加", + permRemove: "移除", + permCreate: "创建", + permDelete: "删除", + permSee: "查看", + permRead: "读取", + permWrite: "写入", + permList: "列出 [STR] 的权限", + permControl: "控制", + toggleLogging: "[STATE]控制台日志", + logOn: "开启", + logOff: "关闭", + getLastError: "上一个错误", + wasRead: "是否读取?", + wasWritten: "是否写入?", + version: "版本", + dateCreated: "[STR] 的创建日期", + dateModified: "[STR] 的修改日期", + dateAccessed: "[STR] 的访问日期", + }, }); -(function(Scratch) { - "use strict"; - - const defaultPerms = { - create: true, - delete: true, - see: true, - read: true, - write: true, - control: true, - }; - - const extensionVersion = "1.0.5"; - - class LiFS { - constructor() { - - this.fs = new Map(); - this.liFSLogEnabled = false; - this.lastError = ""; - this.readActivity = false; - this.writeActivity = false; - - this._log("Initializing LiFS extension..."); - this._internalClean(); - } - - getInfo() { - return { - id: "lithiumFS", - - name: "Lithium FS", - color1: "#d52246", - color2: "#a61734", - color3: "#7f1026", - - description: "Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more.", - blocks: [ - - { - opcode: "start", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "start", - default: "create [STR]" - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "folder", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "folder", - default: "set [STR] to [STR2]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - STR2: { - type: Scratch.ArgumentType.STRING, - defaultValue: Scratch.translate({ - id: "folder_default", - default: "LiFS is good!", - }), - }, - }, - }, - { - opcode: "open", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "open", - default: "open [STR]" - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "del", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "del", - default: "delete [STR]" - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "list", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "list_new", - default: "list [TYPE] under [STR]", - }), - arguments: { - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "LIST_TYPE_MENU", - defaultValue: "all", - }, - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - "---", - - { - opcode: "copy", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "copy", - default: "copy [STR] to [STR2]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - STR2: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/copy_of_example.txt", - }, - }, - }, - { - opcode: "sync", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "sync_new", - default: "rename [STR] to [STR2]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - STR2: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/new_example.txt", - }, - }, - }, - { - opcode: "exists", - blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "exists", - default: "does [STR] exist?", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "isFile", - blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "isFile", - default: "is [STR] a file?", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "isDir", - blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "isDir", - default: "is [STR] a directory?", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - { - opcode: "fileName", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "fileName", - default: "file name of [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "dirName", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dirName", - default: "directory of [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - - { - opcode: "dateCreated", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateCreated", - default: "date created of [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "dateModified", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateModified", - default: "date modified of [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - { - opcode: "dateAccessed", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateAccessed", - default: "date accessed of [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", - }, - }, - }, - "---", - - { - opcode: "setLimit", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "setLimit", - default: "set size limit for [DIR] to [BYTES] bytes", - }), - arguments: { - DIR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - BYTES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 8192, - }, - }, - }, - { - opcode: "removeLimit", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "removeLimit", - default: "remove size limit for [DIR]", - }), - arguments: { - DIR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - { - opcode: "getLimit", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getLimit", - default: "size limit of [DIR] (bytes)", - }), - arguments: { - DIR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - { - opcode: "getSize", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getSize", - default: "current size of [DIR] (bytes)", - }), - arguments: { - DIR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - { - opcode: "setPerm", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "permSet", - default: "[ACTION] [PERM] permission for [STR]", - }), - arguments: { - ACTION: { - type: Scratch.ArgumentType.STRING, - menu: "PERM_ACTION_MENU", - defaultValue: "remove", - }, - PERM: { - type: Scratch.ArgumentType.STRING, - menu: "PERM_TYPE_MENU", - defaultValue: "write", - }, - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - { - opcode: "listPerms", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "permList", - default: "list permissions for [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", - }, - }, - }, - "---", - - { - opcode: "clean", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "clean", - default: "clear the file system", - }), - arguments: {}, - }, - { - opcode: "in", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "in", - default: "import file system from [STR]", - }), - arguments: { - STR: { - type: Scratch.ArgumentType.STRING, - defaultValue: '{"version":"1.0.5","fs":{}}', - }, - }, - }, - { - opcode: "out", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "out", - default: "export file system", - }), - arguments: {}, - }, - { - opcode: "wasRead", - blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "wasRead", - default: "was read?", - }), - }, - { - opcode: "wasWritten", - blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "wasWritten", - default: "was written?", - }), - }, - { - opcode: "getLastError", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getLastError", - default: "last error", - }), - }, - { - opcode: "toggleLogging", - blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "toggleLogging", - default: "turn [STATE] console logging", - }), - arguments: { - STATE: { - type: Scratch.ArgumentType.STRING, - menu: "LOG_STATE_MENU", - defaultValue: "on", - }, - }, - }, - { - opcode: "getVersion", - blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "version", - default: "version" - }), - }, - ], - menus: { - LIST_TYPE_MENU: { - acceptReporters: true, - items: [{ - text: Scratch.translate({ - id: "listMenuAll", - default: "all" - }), - value: "all", - }, - { - text: Scratch.translate({ - id: "listMenuFiles", - default: "files", - }), - value: "files", - }, - { - text: Scratch.translate({ - id: "listMenuDirs", - default: "directories", - }), - value: "directories", - }, - ], - }, - PERM_ACTION_MENU: { - acceptReporters: true, - items: [{ - text: Scratch.translate({ - id: "permAdd", - default: "add" - }), - value: "add", - }, - { - text: Scratch.translate({ - id: "permRemove", - default: "remove", - }), - value: "remove", - }, - ], - }, - PERM_TYPE_MENU: { - acceptReporters: true, - items: [{ - text: Scratch.translate({ - id: "permCreate", - default: "create", - }), - value: "create", - }, - { - text: Scratch.translate({ - id: "permDelete", - default: "delete", - }), - value: "delete", - }, - { - text: Scratch.translate({ - id: "permSee", - default: "see" - }), - value: "see", - }, - { - text: Scratch.translate({ - id: "permRead", - default: "read" - }), - value: "read", - }, - { - text: Scratch.translate({ - id: "permWrite", - default: "write" - }), - value: "write", - }, - { - text: Scratch.translate({ - id: "permControl", - default: "control", - }), - value: "control", - }, - ], - }, - LOG_STATE_MENU: { - acceptReporters: true, - items: [{ - text: Scratch.translate({ - id: "logOn", - default: "on" - }), - value: "on", - }, - { - text: Scratch.translate({ - id: "logOff", - default: "off" - }), - value: "off", - }, - ], - }, - }, - }; - } - - _log(message, ...args) { - if (this.liFSLogEnabled) { - console.log(`[LiFS] ${message}`, ...args); - } - } - - _warn(message, ...args) { - if (this.liFSLogEnabled) { - console.warn(`[LiFS] ${message}`, ...args); - } - } - - _setError(message, ...args) { - this._warn(message, ...args); - this.lastError = message; - } - - _normalizePath(path) { - if (typeof path !== "string" || path.length === 0) { - return "/"; - } - - const hadTrailingSlash = path.length > 1 && path.endsWith("/"); - - if (path[0] !== "/") { - path = "/" + path; - } - - const segments = path.split("/"); - const newSegments = []; - - for (const segment of segments) { - - if (segment === "" || segment === ".") { - continue; - } - - if (segment === "..") { - if (newSegments.length > 0) { - newSegments.pop(); - } - } else { - - newSegments.push(segment); - } - } - - let newPath = "/" + newSegments.join("/"); - - if (newPath === "/") { - return "/"; - } - - if (hadTrailingSlash) { - newPath += "/"; - } - - return newPath; - } - - _isPathDir(path) { - - return path === "/" || path.endsWith("/"); - } - - _internalDirName(path) { - if (path === "/") { - return "/"; - } - - let procPath = this._isPathDir(path) ? - path.substring(0, path.length - 1) : - path; - - const lastSlash = procPath.lastIndexOf("/"); - if (lastSlash === 0) { - return "/"; - } - if (lastSlash === -1) { - return "/"; - } - - return procPath.substring(0, lastSlash + 1); - } - - _getStringSize(str) { - if (str === null || str === undefined) { - return 0; - } - - let length = 0; - for (let i = 0; i < str.length; i++) { - const charCode = str.charCodeAt(i); - if (charCode < 0x0080) { - length += 1; - } else if (charCode < 0x0800) { - length += 2; - } else if (charCode < 0xd800 || charCode > 0xdfff) { - length += 3; - } else { - - length += 4; - i++; - } - } - return length; - } - - _getDirectorySize(dirPath) { - let totalSize = 0; - - for (const [itemPath, entry] of this.fs.entries()) { - - if ( - !this._isPathDir(itemPath) && - itemPath.startsWith(dirPath) && - dirPath !== itemPath - ) { - totalSize += this._getStringSize(entry.content); - } - } - return totalSize; - } - - _canAccommodateChange(filePath, deltaSize) { - if (deltaSize <= 0) { - return true; - } - - let currentDir = this._internalDirName(filePath); - this._log(`Checking size change of ${deltaSize} bytes for ${filePath}`); - - while (true) { - const entry = this.fs.get(currentDir); - if (!entry) { - - this._warn(`Size check: Could not find parent dir ${currentDir}`); - break; - } - - const limit = entry.limit; - if (limit !== -1) { - - const currentSize = this._getDirectorySize(currentDir); - if (currentSize + deltaSize > limit) { - this._setError( - `Size limit exceeded for ${currentDir}: ${currentSize} + ${deltaSize} > ${limit}` - ); - return false; - } - } - - if (currentDir === "/") { - break; - } - currentDir = this._internalDirName(currentDir); - } - - return true; - } - - _internalCreate(path, content, parentDir) { - if (this.fs.has(path)) { - this._log("InternalCreate failed: Path already exists", path); - - return false; - } - - if (!this.hasPermission(parentDir, "create")) { - this._setError(`Create failed: No 'create' permission in ${parentDir}`); - return false; - } - - const deltaSize = this._getStringSize(content); - if (!this._canAccommodateChange(path, deltaSize)) { - - this._log("InternalCreate failed: Size limit exceeded"); - return false; - } - - let permsToInherit; - const parentEntry = this.fs.get(parentDir); - - if (parentEntry) { - permsToInherit = parentEntry.perms; - } else if (parentDir === "/") { - - permsToInherit = this.fs.get("/").perms; - } else { - - this._warn( - "InternalCreate: Parent not found, using default perms", - parentDir - ); - permsToInherit = defaultPerms; - } - - const now = Date.now(); - this.fs.set(path, { - content: content, - perms: JSON.parse(JSON.stringify(permsToInherit)), - limit: -1, - created: now, - modified: now, - accessed: now, - }); - this.writeActivity = true; - this._log("InternalCreate successful:", path); - return true; - } - - hasPermission(path, action) { - - const normPath = this._normalizePath(path); - this._log("Checking permission:", action, "on", normPath); - - const entry = this.fs.get(normPath); - - if (entry) { - - const result = entry.perms[action]; - this._log("Permission result:", result); - return result; - } - - if (action === "create") { - const parentDir = this._internalDirName(normPath); - const parentEntry = this.fs.get(parentDir); - - if (!parentEntry) { - - const result = parentDir === "/"; - this._log("Permission result (parent check, root):", result); - return result; - } - const result = parentEntry.perms.create; - this._log("Permission result (parent check):", result); - return result; - } - - this._log("Permission result (default fail):", false); - return false; - } - - _internalClean() { - this._log("Internal: Clearing file system..."); - const now = Date.now(); - this.fs.clear(); - this.fs.set("/", { - content: null, - perms: JSON.parse(JSON.stringify(defaultPerms)), - limit: -1, - created: now, - modified: now, - accessed: now, - }); - this._log("Internal: File system reset to root."); - this.writeActivity = true; - } - - clean() { - this.lastError = ""; - this._log("Block: clean"); - if (!this.hasPermission("/", "delete")) { - return this._setError("Clean failed: No 'delete' permission on /"); - } - this._internalClean(); - } - - sync({ - STR, - STR2 - }) { - this.lastError = ""; - const path1 = this._normalizePath(STR); - const path2 = this._normalizePath(STR2); - this._log("Block: rename", path1, "to", path2); - - if (!this.hasPermission(path1, "delete")) { - return this._setError(`Rename failed: No 'delete' permission on ${path1}`); - } - if (this.fs.has(path2)) { - return this._setError(`Rename failed: Destination ${path2} already exists`); - } - if (!this.hasPermission(path2, "create")) { - return this._setError(`Rename failed: No 'create' permission for ${path2}`); - } - - const entry = this.fs.get(path1); - if (!entry) { - return this._setError(`Rename failed: Source ${path1} not found`); - } - - const isDir = this._isPathDir(path1); - let deltaSize = 0; - if (isDir) { - deltaSize = this._getDirectorySize(path1); - } else { - deltaSize = this._getStringSize(entry.content); - } - - if (!this._canAccommodateChange(path2, deltaSize)) { - - return; - } - - const now = Date.now(); - - if (isDir) { - - this._log("Renaming directory and children..."); - - const toRename = []; - for (const [key, value] of this.fs.entries()) { - if (key.startsWith(path1)) { - toRename.push({ - oldKey: key, - value: value - }); - } - } - - const path1Length = path1.length; - for (const item of toRename) { - const remainder = item.oldKey.substring(path1Length); - const newChildPath = path2 + remainder; - - if (item.oldKey === path1) { - item.value.modified = now; - item.value.accessed = now; - } - - this.fs.set(newChildPath, item.value); - this.fs.delete(item.oldKey); - - this._log(`Renaming: ${item.oldKey} to ${newChildPath}`); - } - } else { - - this._log("Renaming single file..."); - entry.modified = now; - entry.accessed = now; - this.fs.set(path2, entry); - this.fs.delete(path1); - this._log("Rename successful"); - } - this.writeActivity = true; - } - - copy({ - STR, - STR2 - }) { - this.lastError = ""; - const path1 = this._normalizePath(STR); - const path2 = this._normalizePath(STR2); - this._log("Block: copy", path1, "to", path2); - - const entry = this.fs.get(path1); - if (!entry) { - return this._setError(`Copy failed: Source ${path1} not found`); - } - - if (!entry.perms.read) { - return this._setError(`Copy failed: No 'read' permission on ${path1}`); - } - if (this.fs.has(path2)) { - return this._setError(`Copy failed: Destination ${path2} already exists`); - } - if (!this.hasPermission(path2, "create")) { - return this._setError(`Copy failed: No 'create' permission for ${path2}`); - } - - this.readActivity = true; - const now = Date.now(); - entry.accessed = now; - - if (this._isPathDir(path1)) { - - const toCopy = []; - let totalDeltaSize = 0; - const path1Length = path1.length; - - for (const [key, value] of this.fs.entries()) { - if (key.startsWith(path1)) { - if (!this._isPathDir(key)) { - totalDeltaSize += this._getStringSize(value.content); - } - toCopy.push({ - key, - value - }); - } - } - - if (!this._canAccommodateChange(path2, totalDeltaSize)) { - - return; - } - - for (const item of toCopy) { - const remainder = item.key.substring(path1Length); - const newChildPath = path2 + remainder; - - this.fs.set(newChildPath, { - content: (item.value.content === null) ? null : ("" + item.value.content), - perms: JSON.parse(JSON.stringify(item.value.perms)), - limit: item.value.limit, - created: item.value.created, - modified: item.value.modified, - accessed: now - }); - this._log(`Copied ${item.key} to ${newChildPath}`); - } - this.writeActivity = true; - this._log("Recursive copy successful"); - - } else { - - const content = "" + entry.content; - const deltaSize = this._getStringSize(content); - if (!this._canAccommodateChange(path2, deltaSize)) { - - return; - } - - const destParentDir = this._internalDirName(path2); - const destParentEntry = this.fs.get(destParentDir); - let permsToInherit = defaultPerms; - - if (destParentEntry) { - permsToInherit = destParentEntry.perms; - } else if (destParentDir === "/") { - permsToInherit = this.fs.get("/").perms; - } else { - this._log( - `Copy: Could not find parent "${destParentDir}", using default perms.` - ); - } - - this.fs.set(path2, { - content: content, - perms: JSON.parse(JSON.stringify(permsToInherit)), - limit: -1, - created: now, - modified: now, - accessed: now - }); - this.writeActivity = true; - this._log("Copy successful"); - } - } - - start({ - STR - }) { - this.lastError = ""; - const path = this._normalizePath(STR); - this._log("Block: create", path); - - if (this._isPathDir(path) && path.length > 1) { - const fileName = path.substring(0, path.length - 1).split("/").pop(); - if (fileName.includes(".")) { - this._warn( - `Path "${path}" looks like a file but is being treated as a directory due to the trailing slash.` - ); - } - } - - if (path === "/") { - return this._setError("Create failed: Cannot create root directory '/'"); - } - - if (this.fs.has(path)) { - return this._setError(`Create failed: ${path} already exists`); - } - - const parentDir = this._internalDirName(path); - if (parentDir !== "/" && !this.fs.has(parentDir)) { - this._log("Creating parent directory:", parentDir); - - if (!this.hasPermission(parentDir, "create")) { - return this._setError( - `Create failed: No 'create' permission in ${this._internalDirName(parentDir)}, aborting recursive create.` - ); - } - - this.start({ - STR: parentDir - }); - - if (this.lastError) { - this._log( - "Create failed: Parent creation failed (recursive call failed)." - ); - - return; - } - if (!this.fs.has(parentDir)) { - - return this._setError( - "Create failed: Parent creation failed, aborting." - ); - } - } - - const ok = this._internalCreate( - path, - this._isPathDir(path) ? null : "", - parentDir - ); - - if (!ok) { - this._log("Create failed: _internalCreate returned false."); - - if (!this.lastError) { - this._setError( - `Create failed: An internal error occurred for ${path}` - ); - } - return; - } - } - - open({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: open", path); - - const entry = this.fs.get(path); - this.readActivity = true; - - if (!entry) { - this._log("Result: (Not found)", ""); - return ""; - } - if (this._isPathDir(path)) { - this._log("Result: (Is a directory)", ""); - return ""; - } - - if (!entry.perms.read) { - this._warn(`Read permission denied for "${path}"`); - return ""; - } - - entry.accessed = Date.now(); - const content = entry.content; - this._log("Result:", content); - return content; - } - - del({ - STR - }) { - this.lastError = ""; - const path = this._normalizePath(STR); - this._log("Block: delete", path); - - if (!this.hasPermission(path, "delete")) { - return this._setError(`Delete failed: No 'delete' permission on ${path}`); - } - - const isDir = this._isPathDir(path); - - const toDelete = []; - for (const currentPath of this.fs.keys()) { - if (isDir) { - if (currentPath.startsWith(path)) { - toDelete.push(currentPath); - } - } else { - if (currentPath === path) { - toDelete.push(currentPath); - break; - } - } - } - - for (const key of toDelete) { - this.fs.delete(key); - this._log("Deleted:", key); - } - - this.writeActivity = true; - this._log("Delete complete"); - } - - folder({ - STR, - STR2 - }) { - this.lastError = ""; - const path = this._normalizePath(STR); - this._log("Block: set", path, "to", STR2); - - let entry = this.fs.get(path); - - if (!entry) { - this._log("Set: File not found, attempting to create..."); - this.start({ - STR: path - }); - entry = this.fs.get(path); - if (!entry) { - this._log("Set failed: Creation also failed"); - - return; - } - } - - if (this._isPathDir(path)) { - return this._setError("Set failed: Cannot set content of a directory"); - } - if (!entry.perms.write) { - return this._setError(`Set failed: No 'write' permission on ${path}`); - } - - const oldContent = entry.content || ""; - const deltaSize = - this._getStringSize(STR2) - this._getStringSize(oldContent); - - if (!this._canAccommodateChange(path, deltaSize)) { - - return; - } - - entry.content = STR2; - const now = Date.now(); - entry.modified = now; - entry.accessed = now; - this.writeActivity = true; - this._log("Set successful"); - } - - list({ - TYPE, - STR - }) { - - let path = this._normalizePath(STR); - if (!this._isPathDir(path)) { - path += "/"; - } - - this._log("Block: list", TYPE, "under", path); - this.readActivity = true; - const emptyList = []; - - const entry = this.fs.get(path); - if (!entry) { - this._log("List failed: Directory not found."); - return emptyList; - } - - if (!this.hasPermission(path, "see")) { - this._log("List failed: No see permission on directory"); - return emptyList; - } - - entry.accessed = Date.now(); - - let children = new Set(); - const pathLen = path.length; - - for (const itemPath of this.fs.keys()) { - if (itemPath === path || itemPath === "/") continue; - - if (itemPath.startsWith(path)) { - let remainder = itemPath.substring(pathLen); - let nextSlash = remainder.indexOf("/"); - let childName = ""; - let isDir = false; - - if (nextSlash === -1) { - childName = remainder; - isDir = false; - } else { - childName = remainder.substring(0, nextSlash + 1); - isDir = true; - } - - if (childName === "") continue; - - const childPath = `${path}${childName}`; - if (!this.hasPermission(childPath, "see")) { - this._log("List: Skipping item (no see perm):", childPath); - continue; - } - - if (TYPE === "all") children.add(childName); - else if (TYPE === "files" && !isDir) children.add(childName); - else if (TYPE === "directories" && isDir) children.add(childName); - } - } - - const childrenArray = Array.from(children); - this._log("List result (raw):", childrenArray); - return childrenArray; - } - - in({ - STR - }) { - this.lastError = ""; - this._log("Block: import"); - if (!this.hasPermission("/", "delete")) { - return this._setError("Import failed: No 'delete' permission on /"); - } - try { - const data = JSON.parse(STR); - - const version = data ? data.version : null; - if (!version) { - return this._setError("Import failed: Data invalid or missing version."); - } - - let migrationData = {}; - let needsMigration = false; - - if (version === "1.0.5") { - - if (!data.fs || typeof data.fs !== 'object' || Array.isArray(data.fs)) { - return this._setError("Import failed: v1.0.5 data is corrupt (missing 'fs' object)."); - } - migrationData = data.fs; - - } else if (version === "1.0.4" || version === "1.0.3" || version === "1.0.2") { - - this._log(`Import: Migrating v${version} save...`); - needsMigration = true; - if (!Array.isArray(data.sl)) { - this._log(`... adding 'sl' array.`); - data.sl = new Array(data.sy.length).fill(-1); - } - if (!Array.isArray(data.fi) || !Array.isArray(data.sy) || - !Array.isArray(data.pm) || !Array.isArray(data.sl) || - data.fi.length !== data.sy.length || - data.fi.length !== data.pm.length || - data.fi.length !== data.sl.length || - data.sy.indexOf("/") === -1) { - return this._setError("Import failed: Old version data arrays are corrupt or mismatched."); - } - - const now = Date.now(); - data.created = new Array(data.sy.length).fill(now); - data.modified = new Array(data.sy.length).fill(now); - data.accessed = new Array(data.sy.length).fill(now); - - } else { - return this._setError(`Import failed: Incompatible version "${version}". Expected "${extensionVersion}" or older.`); - } - - if (needsMigration) { - this.fs.clear(); - for (let i = 0; i < data.sy.length; i++) { - const perm = data.pm[i]; - const limit = data.sl[i]; - - if ( - typeof data.sy[i] !== "string" || - typeof perm !== "object" || perm === null || Array.isArray(perm) || - typeof limit !== "number" || - typeof perm.create !== "boolean" || typeof perm.delete !== "boolean" || - typeof perm.see !== "boolean" || typeof perm.read !== "boolean" || - typeof perm.write !== "boolean" || typeof perm.control !== "boolean" - ) { - this._setError("Import failed: Corrupt data found in legacy filesystem entries."); - this._internalClean(); - return; - } - this.fs.set(data.sy[i], { - content: data.fi[i], - perms: data.pm[i], - limit: data.sl[i], - created: data.created[i], - modified: data.modified[i], - accessed: data.accessed[i] - }); - } - } else { - - this.fs.clear(); - for (const path in data.fs) { - if (Object.prototype.hasOwnProperty.call(data.fs, path)) { - const entry = data.fs[path]; - - if (!entry || typeof entry.perms !== 'object' || typeof entry.limit !== 'number' || - typeof entry.created !== 'number' || typeof entry.modified !== 'number' || - typeof entry.accessed !== 'number') { - this._setError(`Import failed: Corrupt entry for path "${path}".`); - this._internalClean(); - return; - } - this.fs.set(path, entry); - } - } - if (!this.fs.has("/")) { - this._setError("Import failed: Rebuilt filesystem is missing root '/'."); - this._internalClean(); - return; - } - } - - this.writeActivity = true; - this._log("Import successful"); - } catch (e) { - this._setError( - `Import failed: JSON parse error. File system was not changed.` - ); - } - } - - out() { - this._log("Block: export"); - this.readActivity = true; - - const fsObject = {}; - for (const [path, entry] of this.fs.entries()) { - fsObject[path] = entry; - } - - const result = JSON.stringify({ - version: extensionVersion, - fs: fsObject, - }); - this._log("Export successful, size:", result.length); - return result; - } - - exists({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: exists", path); - this.readActivity = true; - - const entry = this.fs.get(path); - if (!entry) { - this._log("Result: false (not found)"); - return false; - } - if (!entry.perms.see) { - this._log("Result: false (no see perm)"); - return false; - } - entry.accessed = Date.now(); - this._log("Result: true"); - return true; - } - - isFile({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: isFile", path); - this.readActivity = true; - - const entry = this.fs.get(path); - if (!entry) { - this._log("Result: false (not found)"); - return false; - } - if (!entry.perms.see) { - this._log("Result: false (no see perm)"); - return false; - } - - entry.accessed = Date.now(); - const result = !this._isPathDir(path); - this._log("Result:", result); - return result; - } - - isDir({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: isDir", path); - this.readActivity = true; - - const entry = this.fs.get(path); - if (!entry) { - this._log("Result: false (not found)"); - return false; - } - if (!entry.perms.see) { - this._log("Result: false (no see perm)"); - return false; - } - - entry.accessed = Date.now(); - const result = this._isPathDir(path); - this._log("Result:", result); - return result; - } - - setPerm({ - ACTION, - PERM, - STR - }) { - this.lastError = ""; - const path = this._normalizePath(STR); - this._log("Block: setPerm", ACTION, PERM, "for", path); - - if (!this.hasPermission(path, "control")) { - return this._setError(`setPerm failed: No 'control' permission on ${path}`); - } - - const newValue = ACTION === "add"; - const isDir = this._isPathDir(path); - const now = Date.now(); - - this._log("Applying changes..."); - for (const [currentPath, entry] of this.fs.entries()) { - if ( - (isDir && currentPath.startsWith(path)) || - currentPath === path - ) { - entry.perms[PERM] = newValue; - entry.modified = now; - entry.accessed = now; - this._log("Changed perm for:", currentPath); - } - } - this.writeActivity = true; - this._log("setPerm complete"); - } - - listPerms({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: listPerms", path); - this.readActivity = true; - - const entry = this.fs.get(path); - if (!entry) { - this._log("Result: {} (not found)"); - return JSON.stringify({}); - } - - if (!entry.perms.see) { - this._warn(`See permission denied for "${path}"`); - return JSON.stringify({}); - } - - entry.accessed = Date.now(); - const result = JSON.stringify(entry.perms); - this._log("Result:", result); - return result; - } - - fileName({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: fileName", path); - this.readActivity = true; - - if (!this.hasPermission(path, "see")) { - this._warn(`See permission denied for "${path}"`); - return ""; - } - - const entry = this.fs.get(path); - if (entry) entry.accessed = Date.now(); - - if (path === "/") { - this._log("Result: /"); - return "/"; - } - - let procPath = this._isPathDir(path) ? - path.substring(0, path.length - 1) : - path; - - const lastSlash = procPath.lastIndexOf("/"); - if (lastSlash === -1) { - this._log("Result (no slash):", procPath); - return procPath; - } - const file = procPath.substring(lastSlash + 1); - this._log("Result:", file); - return file; - } - - dirName({ - STR - }) { - const path = this._normalizePath(STR); - this._log("Block: dirName", path); - this.readActivity = true; - - if (!this.hasPermission(path, "see")) { - this._warn(`See permission denied for "${path}"`); - return ""; - } - - const entry = this.fs.get(path); - if (entry) entry.accessed = Date.now(); - - const parent = this._internalDirName(path); - this._log("Result:", parent); - return parent; - } - - toggleLogging({ - STATE - }) { - this.liFSLogEnabled = STATE === "on"; - this._log("Console logging turned", STATE); - } - - setLimit({ - DIR, - BYTES - }) { - this.lastError = ""; - const path = this._normalizePath(DIR); - this._log("Block: setLimit", path, "to", BYTES, "bytes"); - - if (!this._isPathDir(path)) { - return this._setError("setLimit failed: Path must be a directory (end with /)"); - } - if (!this.hasPermission(path, "control")) { - return this._setError(`setLimit failed: No 'control' permission on ${path}`); - } - const entry = this.fs.get(path); - if (!entry) { - return this._setError(`setLimit failed: Directory ${path} not found`); - } - - const limitInBytes = Math.max(-1, parseFloat(BYTES) || 0); - - if (limitInBytes !== -1) { - const currentSize = this._getDirectorySize(path); - if (currentSize > limitInBytes) { - return this._setError( - `setLimit failed: New limit (${limitInBytes} B) is smaller than current directory size (${currentSize} B)` - ); - } - } - - const now = Date.now(); - entry.limit = limitInBytes; - entry.modified = now; - entry.accessed = now; - this.writeActivity = true; - this._log("setLimit successful"); - } - - removeLimit({ - DIR - }) { - this.lastError = ""; - const path = this._normalizePath(DIR); - this._log("Block: removeLimit", path); - - if (!this._isPathDir(path)) { - return this._setError("removeLimit failed: Path must be a directory (end with /)"); - } - if (!this.hasPermission(path, "control")) { - return this._setError(`removeLimit failed: No 'control' permission on ${path}`); - } - const entry = this.fs.get(path); - if (!entry) { - return this._setError(`removeLimit failed: Directory ${path} not found`); - } - - const now = Date.now(); - entry.limit = -1; - entry.modified = now; - entry.accessed = now; - this.writeActivity = true; - this._log("removeLimit successful"); - } - - getLimit({ - DIR - }) { - const path = this._normalizePath(DIR); - this._log("Block: getLimit", path); - this.readActivity = true; - - if (!this._isPathDir(path)) { - this._warn("getLimit failed: Path must be a directory (end with /)"); - return -1; - } - if (!this.hasPermission(path, "see")) { - this._warn(`getLimit failed: No 'see' permission for "${path}"`); - return -1; - } - - const entry = this.fs.get(path); - if (!entry) { - this._warn(`getLimit failed: Directory ${path} not found`); - return -1; - } - - entry.accessed = Date.now(); - const limitInBytes = entry.limit; - this._log("getLimit result:", limitInBytes, "bytes"); - return limitInBytes; - } - - getSize({ - DIR - }) { - const path = this._normalizePath(DIR); - this._log("Block: getSize", path); - this.readActivity = true; - - if (!this._isPathDir(path)) { - this._warn("getSize failed: Path must be a directory (end with /)"); - return 0; - } - if (!this.hasPermission(path, "see")) { - this._warn(`getSize failed: No 'see' permission for "${path}"`); - return 0; - } - - const entry = this.fs.get(path); - if (!entry) { - this._warn(`getSize failed: Directory ${path} not found`); - return 0; - } - - entry.accessed = Date.now(); - const sizeInBytes = this._getDirectorySize(path); - this._log("getSize result:", sizeInBytes, "bytes"); - return sizeInBytes; - } - - _getTimestamp(path, type) { - this.readActivity = true; - const entry = this.fs.get(path); - if (!entry) { - this._warn(`Timestamp check failed: ${path} not found.`); - return ""; - } - if (!entry.perms.see) { - this._warn(`Timestamp check failed: No 'see' permission on ${path}.`); - return ""; - } - entry.accessed = Date.now(); - const timestamp = entry[type]; - return new Date(timestamp).toISOString(); - } - - dateCreated({ - STR - }) { - const path = this._normalizePath(STR); - return this._getTimestamp(path, 'created'); - } - - dateModified({ - STR - }) { - const path = this._normalizePath(STR); - return this._getTimestamp(path, 'modified'); - } - - dateAccessed({ - STR - }) { - const path = this._normalizePath(STR); - return this._getTimestamp(path, 'accessed'); - } - - getLastError() { - return this.lastError; - } - - wasRead() { - const val = this.readActivity; - this.readActivity = false; - return val; - } - - wasWritten() { - const val = this.writeActivity; - this.writeActivity = false; - return val; - } - - getVersion() { - return extensionVersion; - } - } - - Scratch.extensions.register(new LiFS()); +(function (Scratch) { + "use strict"; + + const defaultPerms = { + create: true, + delete: true, + see: true, + read: true, + write: true, + control: true, + }; + + const extensionVersion = "1.0.5"; + + class LiFS { + constructor() { + this.fs = new Map(); + this.liFSLogEnabled = false; + this.lastError = ""; + this.readActivity = false; + this.writeActivity = false; + + this._log("Initializing LiFS extension..."); + this._internalClean(); + } + + getInfo() { + return { + id: "lithiumFS", + + name: "Lithium FS", + color1: "#d52246", + color2: "#a61734", + color3: "#7f1026", + + description: + "Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more.", + blocks: [ + { + opcode: "start", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "start", + default: "create [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "folder", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "folder", + default: "set [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate({ + id: "folder_default", + default: "LiFS is good!", + }), + }, + }, + }, + { + opcode: "open", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "open", + default: "open [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "del", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "del", + default: "delete [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "list", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "list_new", + default: "list [TYPE] under [STR]", + }), + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "LIST_TYPE_MENU", + defaultValue: "all", + }, + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + "---", + + { + opcode: "copy", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "copy", + default: "copy [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/copy_of_example.txt", + }, + }, + }, + { + opcode: "sync", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "sync_new", + default: "rename [STR] to [STR2]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/new_example.txt", + }, + }, + }, + { + opcode: "exists", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "exists", + default: "does [STR] exist?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "isFile", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "isFile", + default: "is [STR] a file?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "isDir", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "isDir", + default: "is [STR] a directory?", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "fileName", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "fileName", + default: "file name of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dirName", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dirName", + default: "directory of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + + { + opcode: "dateCreated", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateCreated", + default: "date created of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dateModified", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateModified", + default: "date modified of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + { + opcode: "dateAccessed", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "dateAccessed", + default: "date accessed of [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/example.txt", + }, + }, + }, + "---", + + { + opcode: "setLimit", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "setLimit", + default: "set size limit for [DIR] to [BYTES] bytes", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + BYTES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 8192, + }, + }, + }, + { + opcode: "removeLimit", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "removeLimit", + default: "remove size limit for [DIR]", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "getLimit", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getLimit", + default: "size limit of [DIR] (bytes)", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "getSize", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getSize", + default: "current size of [DIR] (bytes)", + }), + arguments: { + DIR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "setPerm", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "permSet", + default: "[ACTION] [PERM] permission for [STR]", + }), + arguments: { + ACTION: { + type: Scratch.ArgumentType.STRING, + menu: "PERM_ACTION_MENU", + defaultValue: "remove", + }, + PERM: { + type: Scratch.ArgumentType.STRING, + menu: "PERM_TYPE_MENU", + defaultValue: "write", + }, + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + { + opcode: "listPerms", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "permList", + default: "list permissions for [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/LiFS/", + }, + }, + }, + "---", + + { + opcode: "clean", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "clean", + default: "clear the file system", + }), + arguments: {}, + }, + { + opcode: "in", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "in", + default: "import file system from [STR]", + }), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: '{"version":"1.0.5","fs":{}}', + }, + }, + }, + { + opcode: "out", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "out", + default: "export file system", + }), + arguments: {}, + }, + { + opcode: "wasRead", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "wasRead", + default: "was read?", + }), + }, + { + opcode: "wasWritten", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate({ + id: "wasWritten", + default: "was written?", + }), + }, + { + opcode: "getLastError", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "getLastError", + default: "last error", + }), + }, + { + opcode: "toggleLogging", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate({ + id: "toggleLogging", + default: "turn [STATE] console logging", + }), + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "LOG_STATE_MENU", + defaultValue: "on", + }, + }, + }, + { + opcode: "getVersion", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate({ + id: "version", + default: "version", + }), + }, + ], + menus: { + LIST_TYPE_MENU: { + acceptReporters: true, + items: [ + { + text: Scratch.translate({ + id: "listMenuAll", + default: "all", + }), + value: "all", + }, + { + text: Scratch.translate({ + id: "listMenuFiles", + default: "files", + }), + value: "files", + }, + { + text: Scratch.translate({ + id: "listMenuDirs", + default: "directories", + }), + value: "directories", + }, + ], + }, + PERM_ACTION_MENU: { + acceptReporters: true, + items: [ + { + text: Scratch.translate({ + id: "permAdd", + default: "add", + }), + value: "add", + }, + { + text: Scratch.translate({ + id: "permRemove", + default: "remove", + }), + value: "remove", + }, + ], + }, + PERM_TYPE_MENU: { + acceptReporters: true, + items: [ + { + text: Scratch.translate({ + id: "permCreate", + default: "create", + }), + value: "create", + }, + { + text: Scratch.translate({ + id: "permDelete", + default: "delete", + }), + value: "delete", + }, + { + text: Scratch.translate({ + id: "permSee", + default: "see", + }), + value: "see", + }, + { + text: Scratch.translate({ + id: "permRead", + default: "read", + }), + value: "read", + }, + { + text: Scratch.translate({ + id: "permWrite", + default: "write", + }), + value: "write", + }, + { + text: Scratch.translate({ + id: "permControl", + default: "control", + }), + value: "control", + }, + ], + }, + LOG_STATE_MENU: { + acceptReporters: true, + items: [ + { + text: Scratch.translate({ + id: "logOn", + default: "on", + }), + value: "on", + }, + { + text: Scratch.translate({ + id: "logOff", + default: "off", + }), + value: "off", + }, + ], + }, + }, + }; + } + + _log(message, ...args) { + if (this.liFSLogEnabled) { + console.log(`[LiFS] ${message}`, ...args); + } + } + + _warn(message, ...args) { + if (this.liFSLogEnabled) { + console.warn(`[LiFS] ${message}`, ...args); + } + } + + _setError(message, ...args) { + this._warn(message, ...args); + this.lastError = message; + } + + _normalizePath(path) { + if (typeof path !== "string" || path.length === 0) { + return "/"; + } + + const hadTrailingSlash = path.length > 1 && path.endsWith("/"); + + if (path[0] !== "/") { + path = "/" + path; + } + + const segments = path.split("/"); + const newSegments = []; + + for (const segment of segments) { + if (segment === "" || segment === ".") { + continue; + } + + if (segment === "..") { + if (newSegments.length > 0) { + newSegments.pop(); + } + } else { + newSegments.push(segment); + } + } + + let newPath = "/" + newSegments.join("/"); + + if (newPath === "/") { + return "/"; + } + + if (hadTrailingSlash) { + newPath += "/"; + } + + return newPath; + } + + _isPathDir(path) { + return path === "/" || path.endsWith("/"); + } + + _internalDirName(path) { + if (path === "/") { + return "/"; + } + + let procPath = this._isPathDir(path) + ? path.substring(0, path.length - 1) + : path; + + const lastSlash = procPath.lastIndexOf("/"); + if (lastSlash === 0) { + return "/"; + } + if (lastSlash === -1) { + return "/"; + } + + return procPath.substring(0, lastSlash + 1); + } + + _getStringSize(str) { + if (str === null || str === undefined) { + return 0; + } + + let length = 0; + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + if (charCode < 0x0080) { + length += 1; + } else if (charCode < 0x0800) { + length += 2; + } else if (charCode < 0xd800 || charCode > 0xdfff) { + length += 3; + } else { + length += 4; + i++; + } + } + return length; + } + + _getDirectorySize(dirPath) { + let totalSize = 0; + + for (const [itemPath, entry] of this.fs.entries()) { + if ( + !this._isPathDir(itemPath) && + itemPath.startsWith(dirPath) && + dirPath !== itemPath + ) { + totalSize += this._getStringSize(entry.content); + } + } + return totalSize; + } + + _canAccommodateChange(filePath, deltaSize) { + if (deltaSize <= 0) { + return true; + } + + let currentDir = this._internalDirName(filePath); + this._log(`Checking size change of ${deltaSize} bytes for ${filePath}`); + + while (true) { + const entry = this.fs.get(currentDir); + if (!entry) { + this._warn(`Size check: Could not find parent dir ${currentDir}`); + break; + } + + const limit = entry.limit; + if (limit !== -1) { + const currentSize = this._getDirectorySize(currentDir); + if (currentSize + deltaSize > limit) { + this._setError( + `Size limit exceeded for ${currentDir}: ${currentSize} + ${deltaSize} > ${limit}` + ); + return false; + } + } + + if (currentDir === "/") { + break; + } + currentDir = this._internalDirName(currentDir); + } + + return true; + } + + _internalCreate(path, content, parentDir) { + if (this.fs.has(path)) { + this._log("InternalCreate failed: Path already exists", path); + + return false; + } + + if (!this.hasPermission(parentDir, "create")) { + this._setError(`Create failed: No 'create' permission in ${parentDir}`); + return false; + } + + const deltaSize = this._getStringSize(content); + if (!this._canAccommodateChange(path, deltaSize)) { + this._log("InternalCreate failed: Size limit exceeded"); + return false; + } + + let permsToInherit; + const parentEntry = this.fs.get(parentDir); + + if (parentEntry) { + permsToInherit = parentEntry.perms; + } else if (parentDir === "/") { + permsToInherit = this.fs.get("/").perms; + } else { + this._warn( + "InternalCreate: Parent not found, using default perms", + parentDir + ); + permsToInherit = defaultPerms; + } + + const now = Date.now(); + this.fs.set(path, { + content: content, + perms: JSON.parse(JSON.stringify(permsToInherit)), + limit: -1, + created: now, + modified: now, + accessed: now, + }); + this.writeActivity = true; + this._log("InternalCreate successful:", path); + return true; + } + + hasPermission(path, action) { + const normPath = this._normalizePath(path); + this._log("Checking permission:", action, "on", normPath); + + const entry = this.fs.get(normPath); + + if (entry) { + const result = entry.perms[action]; + this._log("Permission result:", result); + return result; + } + + if (action === "create") { + const parentDir = this._internalDirName(normPath); + const parentEntry = this.fs.get(parentDir); + + if (!parentEntry) { + const result = parentDir === "/"; + this._log("Permission result (parent check, root):", result); + return result; + } + const result = parentEntry.perms.create; + this._log("Permission result (parent check):", result); + return result; + } + + this._log("Permission result (default fail):", false); + return false; + } + + _internalClean() { + this._log("Internal: Clearing file system..."); + const now = Date.now(); + this.fs.clear(); + this.fs.set("/", { + content: null, + perms: JSON.parse(JSON.stringify(defaultPerms)), + limit: -1, + created: now, + modified: now, + accessed: now, + }); + this._log("Internal: File system reset to root."); + this.writeActivity = true; + } + + clean() { + this.lastError = ""; + this._log("Block: clean"); + if (!this.hasPermission("/", "delete")) { + return this._setError("Clean failed: No 'delete' permission on /"); + } + this._internalClean(); + } + + sync({ STR, STR2 }) { + this.lastError = ""; + const path1 = this._normalizePath(STR); + const path2 = this._normalizePath(STR2); + this._log("Block: rename", path1, "to", path2); + + if (!this.hasPermission(path1, "delete")) { + return this._setError( + `Rename failed: No 'delete' permission on ${path1}` + ); + } + if (this.fs.has(path2)) { + return this._setError( + `Rename failed: Destination ${path2} already exists` + ); + } + if (!this.hasPermission(path2, "create")) { + return this._setError( + `Rename failed: No 'create' permission for ${path2}` + ); + } + + const entry = this.fs.get(path1); + if (!entry) { + return this._setError(`Rename failed: Source ${path1} not found`); + } + + const isDir = this._isPathDir(path1); + let deltaSize = 0; + if (isDir) { + deltaSize = this._getDirectorySize(path1); + } else { + deltaSize = this._getStringSize(entry.content); + } + + if (!this._canAccommodateChange(path2, deltaSize)) { + return; + } + + const now = Date.now(); + + if (isDir) { + this._log("Renaming directory and children..."); + + const toRename = []; + for (const [key, value] of this.fs.entries()) { + if (key.startsWith(path1)) { + toRename.push({ + oldKey: key, + value: value, + }); + } + } + + const path1Length = path1.length; + for (const item of toRename) { + const remainder = item.oldKey.substring(path1Length); + const newChildPath = path2 + remainder; + + if (item.oldKey === path1) { + item.value.modified = now; + item.value.accessed = now; + } + + this.fs.set(newChildPath, item.value); + this.fs.delete(item.oldKey); + + this._log(`Renaming: ${item.oldKey} to ${newChildPath}`); + } + } else { + this._log("Renaming single file..."); + entry.modified = now; + entry.accessed = now; + this.fs.set(path2, entry); + this.fs.delete(path1); + this._log("Rename successful"); + } + this.writeActivity = true; + } + + copy({ STR, STR2 }) { + this.lastError = ""; + const path1 = this._normalizePath(STR); + const path2 = this._normalizePath(STR2); + this._log("Block: copy", path1, "to", path2); + + const entry = this.fs.get(path1); + if (!entry) { + return this._setError(`Copy failed: Source ${path1} not found`); + } + + if (!entry.perms.read) { + return this._setError(`Copy failed: No 'read' permission on ${path1}`); + } + if (this.fs.has(path2)) { + return this._setError( + `Copy failed: Destination ${path2} already exists` + ); + } + if (!this.hasPermission(path2, "create")) { + return this._setError( + `Copy failed: No 'create' permission for ${path2}` + ); + } + + this.readActivity = true; + const now = Date.now(); + entry.accessed = now; + + if (this._isPathDir(path1)) { + const toCopy = []; + let totalDeltaSize = 0; + const path1Length = path1.length; + + for (const [key, value] of this.fs.entries()) { + if (key.startsWith(path1)) { + if (!this._isPathDir(key)) { + totalDeltaSize += this._getStringSize(value.content); + } + toCopy.push({ + key, + value, + }); + } + } + + if (!this._canAccommodateChange(path2, totalDeltaSize)) { + return; + } + + for (const item of toCopy) { + const remainder = item.key.substring(path1Length); + const newChildPath = path2 + remainder; + + this.fs.set(newChildPath, { + content: + item.value.content === null ? null : "" + item.value.content, + perms: JSON.parse(JSON.stringify(item.value.perms)), + limit: item.value.limit, + created: item.value.created, + modified: item.value.modified, + accessed: now, + }); + this._log(`Copied ${item.key} to ${newChildPath}`); + } + this.writeActivity = true; + this._log("Recursive copy successful"); + } else { + const content = "" + entry.content; + const deltaSize = this._getStringSize(content); + if (!this._canAccommodateChange(path2, deltaSize)) { + return; + } + + const destParentDir = this._internalDirName(path2); + const destParentEntry = this.fs.get(destParentDir); + let permsToInherit = defaultPerms; + + if (destParentEntry) { + permsToInherit = destParentEntry.perms; + } else if (destParentDir === "/") { + permsToInherit = this.fs.get("/").perms; + } else { + this._log( + `Copy: Could not find parent "${destParentDir}", using default perms.` + ); + } + + this.fs.set(path2, { + content: content, + perms: JSON.parse(JSON.stringify(permsToInherit)), + limit: -1, + created: now, + modified: now, + accessed: now, + }); + this.writeActivity = true; + this._log("Copy successful"); + } + } + + start({ STR }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: create", path); + + if (this._isPathDir(path) && path.length > 1) { + const fileName = path + .substring(0, path.length - 1) + .split("/") + .pop(); + if (fileName.includes(".")) { + this._warn( + `Path "${path}" looks like a file but is being treated as a directory due to the trailing slash.` + ); + } + } + + if (path === "/") { + return this._setError( + "Create failed: Cannot create root directory '/'" + ); + } + + if (this.fs.has(path)) { + return this._setError(`Create failed: ${path} already exists`); + } + + const parentDir = this._internalDirName(path); + if (parentDir !== "/" && !this.fs.has(parentDir)) { + this._log("Creating parent directory:", parentDir); + + if (!this.hasPermission(parentDir, "create")) { + return this._setError( + `Create failed: No 'create' permission in ${this._internalDirName(parentDir)}, aborting recursive create.` + ); + } + + this.start({ + STR: parentDir, + }); + + if (this.lastError) { + this._log( + "Create failed: Parent creation failed (recursive call failed)." + ); + + return; + } + if (!this.fs.has(parentDir)) { + return this._setError( + "Create failed: Parent creation failed, aborting." + ); + } + } + + const ok = this._internalCreate( + path, + this._isPathDir(path) ? null : "", + parentDir + ); + + if (!ok) { + this._log("Create failed: _internalCreate returned false."); + + if (!this.lastError) { + this._setError( + `Create failed: An internal error occurred for ${path}` + ); + } + return; + } + } + + open({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: open", path); + + const entry = this.fs.get(path); + this.readActivity = true; + + if (!entry) { + this._log("Result: (Not found)", ""); + return ""; + } + if (this._isPathDir(path)) { + this._log("Result: (Is a directory)", ""); + return ""; + } + + if (!entry.perms.read) { + this._warn(`Read permission denied for "${path}"`); + return ""; + } + + entry.accessed = Date.now(); + const content = entry.content; + this._log("Result:", content); + return content; + } + + del({ STR }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: delete", path); + + if (!this.hasPermission(path, "delete")) { + return this._setError( + `Delete failed: No 'delete' permission on ${path}` + ); + } + + const isDir = this._isPathDir(path); + + const toDelete = []; + for (const currentPath of this.fs.keys()) { + if (isDir) { + if (currentPath.startsWith(path)) { + toDelete.push(currentPath); + } + } else { + if (currentPath === path) { + toDelete.push(currentPath); + break; + } + } + } + + for (const key of toDelete) { + this.fs.delete(key); + this._log("Deleted:", key); + } + + this.writeActivity = true; + this._log("Delete complete"); + } + + folder({ STR, STR2 }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: set", path, "to", STR2); + + let entry = this.fs.get(path); + + if (!entry) { + this._log("Set: File not found, attempting to create..."); + this.start({ + STR: path, + }); + entry = this.fs.get(path); + if (!entry) { + this._log("Set failed: Creation also failed"); + + return; + } + } + + if (this._isPathDir(path)) { + return this._setError("Set failed: Cannot set content of a directory"); + } + if (!entry.perms.write) { + return this._setError(`Set failed: No 'write' permission on ${path}`); + } + + const oldContent = entry.content || ""; + const deltaSize = + this._getStringSize(STR2) - this._getStringSize(oldContent); + + if (!this._canAccommodateChange(path, deltaSize)) { + return; + } + + entry.content = STR2; + const now = Date.now(); + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("Set successful"); + } + + list({ TYPE, STR }) { + let path = this._normalizePath(STR); + if (!this._isPathDir(path)) { + path += "/"; + } + + this._log("Block: list", TYPE, "under", path); + this.readActivity = true; + const emptyList = []; + + const entry = this.fs.get(path); + if (!entry) { + this._log("List failed: Directory not found."); + return emptyList; + } + + if (!this.hasPermission(path, "see")) { + this._log("List failed: No see permission on directory"); + return emptyList; + } + + entry.accessed = Date.now(); + + let children = new Set(); + const pathLen = path.length; + + for (const itemPath of this.fs.keys()) { + if (itemPath === path || itemPath === "/") continue; + + if (itemPath.startsWith(path)) { + let remainder = itemPath.substring(pathLen); + let nextSlash = remainder.indexOf("/"); + let childName = ""; + let isDir = false; + + if (nextSlash === -1) { + childName = remainder; + isDir = false; + } else { + childName = remainder.substring(0, nextSlash + 1); + isDir = true; + } + + if (childName === "") continue; + + const childPath = `${path}${childName}`; + if (!this.hasPermission(childPath, "see")) { + this._log("List: Skipping item (no see perm):", childPath); + continue; + } + + if (TYPE === "all") children.add(childName); + else if (TYPE === "files" && !isDir) children.add(childName); + else if (TYPE === "directories" && isDir) children.add(childName); + } + } + + const childrenArray = Array.from(children); + this._log("List result (raw):", childrenArray); + return childrenArray; + } + + in({ STR }) { + this.lastError = ""; + this._log("Block: import"); + if (!this.hasPermission("/", "delete")) { + return this._setError("Import failed: No 'delete' permission on /"); + } + try { + const data = JSON.parse(STR); + + const version = data ? data.version : null; + if (!version) { + return this._setError( + "Import failed: Data invalid or missing version." + ); + } + + let migrationData = {}; + let needsMigration = false; + + if (version === "1.0.5") { + if ( + !data.fs || + typeof data.fs !== "object" || + Array.isArray(data.fs) + ) { + return this._setError( + "Import failed: v1.0.5 data is corrupt (missing 'fs' object)." + ); + } + migrationData = data.fs; + } else if ( + version === "1.0.4" || + version === "1.0.3" || + version === "1.0.2" + ) { + this._log(`Import: Migrating v${version} save...`); + needsMigration = true; + if (!Array.isArray(data.sl)) { + this._log(`... adding 'sl' array.`); + data.sl = new Array(data.sy.length).fill(-1); + } + if ( + !Array.isArray(data.fi) || + !Array.isArray(data.sy) || + !Array.isArray(data.pm) || + !Array.isArray(data.sl) || + data.fi.length !== data.sy.length || + data.fi.length !== data.pm.length || + data.fi.length !== data.sl.length || + data.sy.indexOf("/") === -1 + ) { + return this._setError( + "Import failed: Old version data arrays are corrupt or mismatched." + ); + } + + const now = Date.now(); + data.created = new Array(data.sy.length).fill(now); + data.modified = new Array(data.sy.length).fill(now); + data.accessed = new Array(data.sy.length).fill(now); + } else { + return this._setError( + `Import failed: Incompatible version "${version}". Expected "${extensionVersion}" or older.` + ); + } + + if (needsMigration) { + this.fs.clear(); + for (let i = 0; i < data.sy.length; i++) { + const perm = data.pm[i]; + const limit = data.sl[i]; + + if ( + typeof data.sy[i] !== "string" || + typeof perm !== "object" || + perm === null || + Array.isArray(perm) || + typeof limit !== "number" || + typeof perm.create !== "boolean" || + typeof perm.delete !== "boolean" || + typeof perm.see !== "boolean" || + typeof perm.read !== "boolean" || + typeof perm.write !== "boolean" || + typeof perm.control !== "boolean" + ) { + this._setError( + "Import failed: Corrupt data found in legacy filesystem entries." + ); + this._internalClean(); + return; + } + this.fs.set(data.sy[i], { + content: data.fi[i], + perms: data.pm[i], + limit: data.sl[i], + created: data.created[i], + modified: data.modified[i], + accessed: data.accessed[i], + }); + } + } else { + this.fs.clear(); + for (const path in data.fs) { + if (Object.prototype.hasOwnProperty.call(data.fs, path)) { + const entry = data.fs[path]; + + if ( + !entry || + typeof entry.perms !== "object" || + typeof entry.limit !== "number" || + typeof entry.created !== "number" || + typeof entry.modified !== "number" || + typeof entry.accessed !== "number" + ) { + this._setError( + `Import failed: Corrupt entry for path "${path}".` + ); + this._internalClean(); + return; + } + this.fs.set(path, entry); + } + } + if (!this.fs.has("/")) { + this._setError( + "Import failed: Rebuilt filesystem is missing root '/'." + ); + this._internalClean(); + return; + } + } + + this.writeActivity = true; + this._log("Import successful"); + } catch (e) { + this._setError( + `Import failed: JSON parse error. File system was not changed.` + ); + } + } + + out() { + this._log("Block: export"); + this.readActivity = true; + + const fsObject = {}; + for (const [path, entry] of this.fs.entries()) { + fsObject[path] = entry; + } + + const result = JSON.stringify({ + version: extensionVersion, + fs: fsObject, + }); + this._log("Export successful, size:", result.length); + return result; + } + + exists({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: exists", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + entry.accessed = Date.now(); + this._log("Result: true"); + return true; + } + + isFile({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: isFile", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + + entry.accessed = Date.now(); + const result = !this._isPathDir(path); + this._log("Result:", result); + return result; + } + + isDir({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: isDir", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: false (not found)"); + return false; + } + if (!entry.perms.see) { + this._log("Result: false (no see perm)"); + return false; + } + + entry.accessed = Date.now(); + const result = this._isPathDir(path); + this._log("Result:", result); + return result; + } + + setPerm({ ACTION, PERM, STR }) { + this.lastError = ""; + const path = this._normalizePath(STR); + this._log("Block: setPerm", ACTION, PERM, "for", path); + + if (!this.hasPermission(path, "control")) { + return this._setError( + `setPerm failed: No 'control' permission on ${path}` + ); + } + + const newValue = ACTION === "add"; + const isDir = this._isPathDir(path); + const now = Date.now(); + + this._log("Applying changes..."); + for (const [currentPath, entry] of this.fs.entries()) { + if ((isDir && currentPath.startsWith(path)) || currentPath === path) { + entry.perms[PERM] = newValue; + entry.modified = now; + entry.accessed = now; + this._log("Changed perm for:", currentPath); + } + } + this.writeActivity = true; + this._log("setPerm complete"); + } + + listPerms({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: listPerms", path); + this.readActivity = true; + + const entry = this.fs.get(path); + if (!entry) { + this._log("Result: {} (not found)"); + return JSON.stringify({}); + } + + if (!entry.perms.see) { + this._warn(`See permission denied for "${path}"`); + return JSON.stringify({}); + } + + entry.accessed = Date.now(); + const result = JSON.stringify(entry.perms); + this._log("Result:", result); + return result; + } + + fileName({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: fileName", path); + this.readActivity = true; + + if (!this.hasPermission(path, "see")) { + this._warn(`See permission denied for "${path}"`); + return ""; + } + + const entry = this.fs.get(path); + if (entry) entry.accessed = Date.now(); + + if (path === "/") { + this._log("Result: /"); + return "/"; + } + + let procPath = this._isPathDir(path) + ? path.substring(0, path.length - 1) + : path; + + const lastSlash = procPath.lastIndexOf("/"); + if (lastSlash === -1) { + this._log("Result (no slash):", procPath); + return procPath; + } + const file = procPath.substring(lastSlash + 1); + this._log("Result:", file); + return file; + } + + dirName({ STR }) { + const path = this._normalizePath(STR); + this._log("Block: dirName", path); + this.readActivity = true; + + if (!this.hasPermission(path, "see")) { + this._warn(`See permission denied for "${path}"`); + return ""; + } + + const entry = this.fs.get(path); + if (entry) entry.accessed = Date.now(); + + const parent = this._internalDirName(path); + this._log("Result:", parent); + return parent; + } + + toggleLogging({ STATE }) { + this.liFSLogEnabled = STATE === "on"; + this._log("Console logging turned", STATE); + } + + setLimit({ DIR, BYTES }) { + this.lastError = ""; + const path = this._normalizePath(DIR); + this._log("Block: setLimit", path, "to", BYTES, "bytes"); + + if (!this._isPathDir(path)) { + return this._setError( + "setLimit failed: Path must be a directory (end with /)" + ); + } + if (!this.hasPermission(path, "control")) { + return this._setError( + `setLimit failed: No 'control' permission on ${path}` + ); + } + const entry = this.fs.get(path); + if (!entry) { + return this._setError(`setLimit failed: Directory ${path} not found`); + } + + const limitInBytes = Math.max(-1, parseFloat(BYTES) || 0); + + if (limitInBytes !== -1) { + const currentSize = this._getDirectorySize(path); + if (currentSize > limitInBytes) { + return this._setError( + `setLimit failed: New limit (${limitInBytes} B) is smaller than current directory size (${currentSize} B)` + ); + } + } + + const now = Date.now(); + entry.limit = limitInBytes; + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("setLimit successful"); + } + + removeLimit({ DIR }) { + this.lastError = ""; + const path = this._normalizePath(DIR); + this._log("Block: removeLimit", path); + + if (!this._isPathDir(path)) { + return this._setError( + "removeLimit failed: Path must be a directory (end with /)" + ); + } + if (!this.hasPermission(path, "control")) { + return this._setError( + `removeLimit failed: No 'control' permission on ${path}` + ); + } + const entry = this.fs.get(path); + if (!entry) { + return this._setError( + `removeLimit failed: Directory ${path} not found` + ); + } + + const now = Date.now(); + entry.limit = -1; + entry.modified = now; + entry.accessed = now; + this.writeActivity = true; + this._log("removeLimit successful"); + } + + getLimit({ DIR }) { + const path = this._normalizePath(DIR); + this._log("Block: getLimit", path); + this.readActivity = true; + + if (!this._isPathDir(path)) { + this._warn("getLimit failed: Path must be a directory (end with /)"); + return -1; + } + if (!this.hasPermission(path, "see")) { + this._warn(`getLimit failed: No 'see' permission for "${path}"`); + return -1; + } + + const entry = this.fs.get(path); + if (!entry) { + this._warn(`getLimit failed: Directory ${path} not found`); + return -1; + } + + entry.accessed = Date.now(); + const limitInBytes = entry.limit; + this._log("getLimit result:", limitInBytes, "bytes"); + return limitInBytes; + } + + getSize({ DIR }) { + const path = this._normalizePath(DIR); + this._log("Block: getSize", path); + this.readActivity = true; + + if (!this._isPathDir(path)) { + this._warn("getSize failed: Path must be a directory (end with /)"); + return 0; + } + if (!this.hasPermission(path, "see")) { + this._warn(`getSize failed: No 'see' permission for "${path}"`); + return 0; + } + + const entry = this.fs.get(path); + if (!entry) { + this._warn(`getSize failed: Directory ${path} not found`); + return 0; + } + + entry.accessed = Date.now(); + const sizeInBytes = this._getDirectorySize(path); + this._log("getSize result:", sizeInBytes, "bytes"); + return sizeInBytes; + } + + _getTimestamp(path, type) { + this.readActivity = true; + const entry = this.fs.get(path); + if (!entry) { + this._warn(`Timestamp check failed: ${path} not found.`); + return ""; + } + if (!entry.perms.see) { + this._warn(`Timestamp check failed: No 'see' permission on ${path}.`); + return ""; + } + entry.accessed = Date.now(); + const timestamp = entry[type]; + return new Date(timestamp).toISOString(); + } + + dateCreated({ STR }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, "created"); + } + + dateModified({ STR }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, "modified"); + } + + dateAccessed({ STR }) { + const path = this._normalizePath(STR); + return this._getTimestamp(path, "accessed"); + } + + getLastError() { + return this.lastError; + } + + wasRead() { + const val = this.readActivity; + this.readActivity = false; + return val; + } + + wasWritten() { + const val = this.writeActivity; + this.writeActivity = false; + return val; + } + + getVersion() { + return extensionVersion; + } + } + + Scratch.extensions.register(new LiFS()); })(Scratch); From d3a79cc4a44642dd83180f7b18c04fe8fa99b507 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 00:56:03 +0000 Subject: [PATCH 04/22] fix: Problems with JS --- extensions/ohgodwhy2k/lithiumfs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index 13db33916c..8e80d07517 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -273,7 +273,7 @@ Scratch.translate.setup({ dateAccessed: "[STR]のアクセス日時", }, ko: { - clean: "파일 システム 초기화하기", + clean: "파일 시스템 초기화하기", del: "[STR] 삭제하기", folder: "[STR]을(를) [STR2](으)로 정하기", folder_default: "LiFS 최고!", @@ -556,6 +556,7 @@ Scratch.translate.setup({ description: "Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more.", + /** @type {any} */ blocks: [ { opcode: "start", From e2ae7a5581b30b3b16de317e24bb826fe2a41555 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 01:02:05 +0000 Subject: [PATCH 05/22] fix: Remove Scratch.translate, and fix misc bugs --- extensions/ohgodwhy2k/lithiumfs.js | 521 +---------------------------- 1 file changed, 4 insertions(+), 517 deletions(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index 8e80d07517..7cf7c57401 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -5,520 +5,6 @@ // Original: 0832 // License: MIT -Scratch.translate.setup({ - de: { - clean: "Dateisystem löschen", - del: "Lösche [STR]", - folder: "Setze [STR] auf [STR2]", - folder_default: "LiFS ist gut!", - in: "Dateisystem von [STR] importieren", - list: "Alle Dateien unter [STR] auflisten", - open: "Öffne [STR]", - out: "Dateisystem exportieren", - search: "Suche [STR]", - start: "Erschaffe [STR]", - sync: "Ändere die Position von [STR] zu [STR2]", - listMenuAll: "alle", - listMenuFiles: "dateien", - listMenuDirs: "verzeichnisse", - list_new: "liste [TYPE] unter [STR]", - exists: "existiert [STR]?", - isFile: "ist [STR] eine datei?", - isDir: "ist [STR] ein verzeichnis?", - copy: "kopiere [STR] nach [STR2]", - fileName: "dateiname von [STR]", - dirName: "verzeichnis von [STR]", - sync_new: "benenne [STR] um in [STR2]", - permSet: "[ACTION] [PERM] Berechtigung für [STR]", - permAdd: "hinzufügen", - permRemove: "entfernen", - permCreate: "erstellen", - permDelete: "löschen", - permSee: "sehen", - permRead: "lesen", - permWrite: "schreiben", - permList: "berechtigungen auflisten für [STR]", - permControl: "kontrollieren", - toggleLogging: "schalte [STATE] konsolen-logging", - logOn: "an", - logOff: "aus", - setLimit: "setze größenlimit für [DIR] auf [BYTES] bytes", - removeLimit: "entferne größenlimit für [DIR]", - getLimit: "größenlimit von [DIR] (bytes)", - getSize: "aktuelle größe von [DIR] (bytes)", - getLastError: "letzter fehler", - wasRead: "wurde gelesen?", - wasWritten: "wurde geschrieben?", - version: "Version", - dateCreated: "erstellungsdatum von [STR]", - dateModified: "änderungsdatum von [STR]", - dateAccessed: "zugriffsdatum von [STR]", - }, - es: { - folder_default: "¡LiFS es bueno!", - listMenuAll: "todo", - listMenuFiles: "archivos", - listMenuDirs: "directorios", - list_new: "listar [TYPE] en [STR]", - exists: "¿existe [STR]?", - isFile: "¿es [STR] un archivo?", - isDir: "¿es [STR] un directorio?", - copy: "copiar [STR] a [STR2]", - fileName: "nombre de archivo de [STR]", - dirName: "directorio de [STR]", - sync_new: "renombrar [STR] a [STR2]", - permSet: "[ACTION] permiso de [PERM] a [STR]", - permAdd: "añadir", - permRemove: "quitar", - permCreate: "crear", - permDelete: "eliminar", - permSee: "ver", - permRead: "leer", - permWrite: "escribir", - permList: "listar permisos de [STR]", - permControl: "controlar", - toggleLogging: "[STATE] el registro de la consola", - logOn: "activar", - logOff: "desactivar", - setLimit: "establecer límite de tamaño para [DIR] a [BYTES] bytes", - removeLimit: "eliminar límite de tamaño para [DIR]", - getLimit: "límite de tamaño de [DIR] (bytes)", - getSize: "tamaño actual de [DIR] (bytes)", - getLastError: "último error", - wasRead: "¿fue leído?", - wasWritten: "¿fue escrito?", - version: "versión", - dateCreated: "fecha de creación de [STR]", - dateModified: "fecha de modificación de [STR]", - dateAccessed: "fecha de acceso de [STR]", - }, - fi: { - clean: "tyhjennä tiedostojärjestelmä", - del: "poista [STR]", - folder: "aseta [STR] arvoon [STR2]", - folder_default: "LiFS on hieno!", - in: "tuo tiedostojärjestelmä kohteesta [STR]", - list: "luettelo kaikista kohteessa [STR] sijaitsevista tiedostoista", - open: "avaa [STR]", - out: "vie tiedostojärjestelmä", - search: "etsi [STR]", - start: "luo [STR]", - sync: "muuta kohteen [STR] sijainniksi [STR2]", - listMenuAll: "kaikki", - listMenuFiles: "tiedostot", - listMenuDirs: "kansiot", - list_new: "listaa [TYPE] polussa [STR]", - exists: "onko [STR] olemassa?", - isFile: "onko [STR] tiedosto?", - isDir: "onko [STR] kansio?", - copy: "kopioi [STR] kohteeseen [STR2]", - fileName: "tiedostonimi [STR]", - dirName: "kansio [STR]", - sync_new: "nimeä [STR] uudelleen [STR2]", - permSet: "[ACTION] [PERM] käyttöoikeuden kohteelle [STR]", - permAdd: "lisää", - permRemove: "poista", - permCreate: "luo", - permDelete: "poista", - permSee: "nähdä", - permRead: "lukea", - permWrite: "kirjoittaa", - permList: "listaa [STR] käyttöoikeudet", - permControl: "hallita", - toggleLogging: "laita [STATE] konsoliloki", - logOn: "päälle", - logOff: "pois päältä", - getLastError: "viimeisin virhe", - wasRead: "luettiinko?", - wasWritten: "kirjoitettiinko?", - version: "versio", - dateCreated: "luontipäivä [STR]", - dateModified: "muokkauspäivä [STR]", - dateAccessed: "käyttöpäivä [STR]", - }, - fr: { - clean: "effacer le système de fichiers", - del: "supprimer [STR]", - folder: "mettre [STR] à [STR2]", - folder_default: "LiFS est bon !", - in: "importer le système de fichier depuis [STR]", - list: "lister tous les fichiers sous [STR]", - open: "ouvrir [STR]", - out: "exporter le système de fichiers", - search: "chercher [STR]", - start: "créer [STR]", - sync: "modifier l'emplacement de [STR] à [STR2]", - listMenuAll: "tout", - listMenuFiles: "fichiers", - listMenuDirs: "dossiers", - list_new: "lister [TYPE] sous [STR]", - exists: "[STR] existe-t-il?", - isFile: "[STR] est-il un fichier?", - isDir: "[STR] est-il un dossier?", - copy: "copier [STR] vers [STR2]", - fileName: "nom de fichier de [STR]", - dirName: "dossier de [STR]", - sync_new: "renommer [STR] en [STR2]", - permSet: "[ACTION] la permission [PERM] à [STR]", - permAdd: "ajouter", - permRemove: "supprimer", - permCreate: "crer", - permDelete: "supprimer", - permSee: "voir", - permRead: "lire", - permWrite: "écrire", - permList: "lister les permissions de [STR]", - permControl: "contrôler", - toggleLogging: "[STATE] la journalisation de la console", - logOn: "activer", - logOff: "désactiver", - setLimit: "définir la limite de taille pour [DIR] à [BYTES] octets", - removeLimit: "supprimer la limite de taille pour [DIR]", - getLimit: "limite de taille de [DIR] (octets)", - getSize: "taille actuelle de [DIR] (octets)", - getLastError: "dernière erreur", - wasRead: "lu ?", - wasWritten: "écrit ?", - version: "version", - dateCreated: "date de création de [STR]", - dateModified: "date de modification de [STR]", - dateAccessed: "date d'accès de [STR]", - }, - it: { - clean: "svuota file system", - del: "cancella [STR]", - folder: "imposta [STR] a [STR2]", - folder_default: "LiFS funziona!", - in: "importa file system da [STR]", - list: "elenca tutti i file in [STR]", - open: "apri [STR]", - out: "esporta file system", - search: "cerca [STR]", - start: "crea [STR]", - sync: "cambia posizione di [STR] a [STR2]", - listMenuAll: "tutti", - listMenuFiles: "file", - listMenuDirs: "directory", - list_new: "elenca [TYPE] in [STR]", - exists: "[STR] esiste?", - isFile: "[STR] è un file?", - isDir: "[STR] è una directory?", - copy: "copia [STR] in [STR2]", - fileName: "nome file di [STR]", - dirName: "directory di [STR]", - sync_new: "rinomina [STR] in [STR2]", - permSet: "[ACTION] permesso [PERM] a [STR]", - permAdd: "aggiungi", - permRemove: "rimuovi", - permCreate: "crea", - permDelete: "elimina", - permSee: "vedi", - permRead: "leggi", - permWrite: "scrivi", - permList: "elenca permessi per [STR]", - permControl: "controllare", - toggleLogging: "[STATE] log console", - logOn: "attiva", - logOff: "disattiva", - getLastError: "ultimo errore", - wasRead: "è stato letto?", - wasWritten: "è stato scritto?", - version: "versione", - dateCreated: "data creazione di [STR]", - dateModified: "data modifica di [STR]", - dateAccessed: "data accesso di [STR]", - }, - ja: { - clean: "ファイルシステムを削除する", - del: "[STR]を削除", - folder: "[STR]を[STR2]にセットする", - folder_default: "LiFSは良い!", - in: "[STR]からファイルシステムをインポートする", - list: "[STR]直下のファイルをリスト化する", - open: "[STR]を開く", - out: "ファイルシステムをエクスポートする", - search: "[STR]を検索", - start: "[STR]を作成", - sync: "[STR]のロケーションを[STR2]に変更する", - listMenuAll: "すべて", - listMenuFiles: "ファイル", - listMenuDirs: "ディレクトリ", - list_new: "[STR] の [TYPE] を一覧表示", - exists: "[STR] は存在しますか?", - isFile: "[STR] はファイルですか?", - isDir: "[STR] はディレクトリですか?", - copy: "[STR] を [STR2] にコピー", - fileName: "[STR] のファイル名", - dirName: "[STR] のディレクトリ", - sync_new: "[STR] を [STR2] に名前変更", - permSet: "[STR] の [PERM] 権限を [ACTION]", - permAdd: "追加", - permRemove: "削除", - permCreate: "作成", - permDelete: "削除", - permSee: "表示", - permRead: "読み取り", - permWrite: "書き込み", - permList: "[STR] の権限を一覧表示", - permControl: "制御", - toggleLogging: "コンソールログを [STATE] にする", - logOn: "オン", - logOff: "オフ", - getLastError: "最後のエラー", - wasRead: "読み込まれたか?", - wasWritten: "書き込まれたか?", - version: "バージョン", - dateCreated: "[STR]の作成日時", - dateModified: "[STR]の変更日時", - dateAccessed: "[STR]のアクセス日時", - }, - ko: { - clean: "파일 시스템 초기화하기", - del: "[STR] 삭제하기", - folder: "[STR]을(를) [STR2](으)로 정하기", - folder_default: "LiFS 최고!", - in: "[STR]에서 파일 システム 불러오기", - list: "[STR] 안의 파일 목록", - open: "[STR] 열기", - out: "파일 システム 내보내기", - search: "[STR] 검색하기", - start: "[STR] 생성하기", - sync: "[STR]의 경로를 [STR2](으)로 바꾸기", - listMenuAll: "모두", - listMenuFiles: "파일", - listMenuDirs: "디렉터리", - list_new: "[STR]의 [TYPE] 목록", - exists: "[STR]이(가) 존재하나요?", - isFile: "[STR]이(가) 파일인가요?", - isDir: "[STR]이(가) 디렉터리인가요?", - copy: "[STR]을(를) [STR2](으)로 복사하기", - fileName: "[STR]의 파일 이름", - dirName: "[STR]의 디렉터리", - sync_new: "[STR]의 이름을 [STR2](으)로 바꾸기", - permSet: "[STR]에 [PERM] 권한 [ACTION]", - permAdd: "추가하기", - permRemove: "제거하기", - permCreate: "생성", - permDelete: "삭제", - permSee: "보기", - permRead: "읽기", - permWrite: "쓰기", - permList: "[STR]의 권한 목록", - permControl: "제어", - toggleLogging: "콘솔 로깅 [STATE]", - logOn: "켜기", - logOff: "끄기", - getLastError: "마지막 오류", - wasRead: "읽었나요?", - wasWritten: "작성했나요?", - version: "버전", - dateCreated: "[STR]의 생성 날짜", - dateModified: "[STR]의 수정 날짜", - dateAccessed: "[STR]의 접근 날짜", - }, - nb: { - folder_default: "LiFS er bra!", - listMenuAll: "alle", - listMenuFiles: "filer", - listMenuDirs: "mapper", - list_new: "list [TYPE] under [STR]", - exists: "finnes [STR]?", - isFile: "er [STR] en fil?", - isDir: "er [STR] en mappe?", - copy: "kopier [STR] til [STR2]", - fileName: "filnavn til [STR]", - dirName: "mappe til [STR]", - sync_new: "gi [STR] nytt navn [STR2]", - permSet: "[ACTION] [PERM] tillatelse til [STR]", - permAdd: "legg til", - permRemove: "fjern", - permCreate: "opprett", - permDelete: "slett", - permSee: "se", - permRead: "les", - permWrite: "skriv", - permList: "list tillatelser for [STR]", - permControl: "kontroll", - toggleLogging: "slå [STATE] konsolllogging", - logOn: "på", - logOff: "av", - getLastError: "siste feil", - wasRead: "ble lest?", - wasWritten: "ble skrevet?", - version: "versjon", - dateCreated: "opprettelsesdato for [STR]", - dateModified: "endringsdato for [STR]", - dateAccessed: "tilgangsdato for [STR]", - }, - nl: { - clean: "wis het bestandssysteem", - del: "verwijder [STR]", - folder: "maak [STR] [STR2]", - folder_default: "LiFS is geweldig!", - in: "importeer bestandssysteem van [STR]", - list: "alle bestanden onder [STR]", - out: "exporteer bestandssysteem", - search: "zoek [STR]", - start: "creëer [STR]", - sync: "verander locatie van [STR] naar [STR2]", - listMenuAll: "alles", - listMenuFiles: "bestanden", - listMenuDirs: "mappen", - list_new: "lijst [TYPE] onder [STR]", - exists: "bestaat [STR]?", - isFile: "is [STR] een bestand?", - isDir: "is [STR] een map?", - copy: "kopieer [STR] naar [STR2]", - fileName: "bestandsnaam van [STR]", - dirName: "map van [STR]", - sync_new: "hernoem [STR] naar [STR2]", - permSet: "[ACTION] [PERM] toestemming om [STR]", - permAdd: "toevoegen", - permRemove: "verwijderen", - permCreate: "maken", - permDelete: "verwijderen", - permSee: "zien", - permRead: "lezen", - permWrite: "schrijven", - permList: "lijst toestemmingen for [STR]", - permControl: "beheren", - toggleLogging: "zet console logging [STATE]", - logOn: "aan", - logOff: "uit", - getLastError: "laatste fout", - wasRead: "is gelezen?", - wasWritten: "is geschreven?", - version: "versie", - dateCreated: "aanmaakdatum van [STR]", - dateModified: "wijzigingsdatum van [STR]", - dateAccessed: "toegangsdatum van [STR]", - }, - pl: { - del: "usuń [STR]", - folder: "ustaw [STR] na [STR2]", - open: "otwórz [STR]", - search: "szukaj [STR]", - listMenuAll: "wszystko", - listMenuFiles: "pliki", - listMenuDirs: "katalogi", - list_new: "listuj [TYPE] w [STR]", - exists: "czy [STR] istnieje?", - isFile: "czy [STR] to plik?", - isDir: "czy [STR] to katalog?", - copy: "kopiej [STR] do [STR2]", - fileName: "nazwa pliku [STR]", - dirName: "katalog [STR]", - sync_new: "zmień nazwę [STR] na [STR2]", - permSet: "[ACTION] [PERM] uprawnienie do [STR]", - permAdd: "dodaj", - permRemove: "usuń", - permCreate: "tworzenie", - permDelete: "usuwanie", - permSee: "przeglądanie", - permRead: "czytanie", - permWrite: "pisanie", - permList: "listuj uprawnienia [STR]", - permControl: "kontrola", - toggleLogging: "włącz [STATE] logowanie konsoli", - logOn: "włącz", - logOff: "wyłącz", - getLastError: "ostatni błąd", - wasRead: "czytano?", - wasWritten: "pisano?", - version: "wersja", - dateCreated: "data utworzenia [STR]", - dateModified: "data modyfikacji [STR]", - dateAccessed: "data dostępu [STR]", - }, - ru: { - clean: "очистить файловую систему", - del: "удалить [STR]", - folder: "задать [STR] значение [STR2]", - folder_default: "LiFS это хорошо!", - in: "импортировать файловую систему из [STR]", - list: "перечислить все файлы под [STR]", - open: "открыть [STR]", - out: "экспортировать файловую систему", - search: "поиск [STR]", - start: "создать [STR]", - sync: "изменить расположение [STR] на [STR2]", - listMenuAll: "все", - listMenuFiles: "файлы", - listMenuDirs: "папки", - list_new: "список [TYPE] в [STR]", - exists: "[STR] существует?", - isFile: "[STR] это файл?", - isDir: "[STR] это папка?", - copy: "копировать [STR] в [STR2]", - fileName: "имя файла [STR]", - dirName: "папка [STR]", - sync_new: "переименовать [STR] в [STR2]", - permSet: "[ACTION] [PERM] разрешение для [STR]", - permAdd: "добавить", - permRemove: "удалить", - permCreate: "создать", - permDelete: "удалить", - permSee: "видеть", - permRead: "читать", - permWrite: "писать", - permList: "список разрешений для [STR]", - permControl: "управлять", - toggleLogging: "[STATE] ведение журнала консоли", - logOn: "включить", - logOff: "выключить", - getLastError: "последняя ошибка", - wasRead: "было чтение?", - wasWritten: "была запись?", - version: "версия", - dateCreated: "дата создания [STR]", - dateModified: "дата изменения [STR]", - dateAccessed: "дата доступа [STR]", - }, - "zh-cn": { - clean: "清空文件System", - del: "删除 [STR]", - folder: "将[STR]设为[STR2]", - folder_default: "LiFS 好用!", - in: "从 [STR] 导入文件System", - list: "列出 [STR] 下的所有文件", - open: "打开 [STR]", - out: "导出文件 system", - search: "搜索 [STR]", - start: "新建 [STR]", - sync: "将 [STR] 的位置改为 [STR2]", - listMenuAll: "所有", - listMenuFiles: "文件", - listMenuDirs: "目录", - list_new: "列出 [STR] 下的 [TYPE]", - exists: "[STR] 是否存在?", - isFile: "[STR] 是文件吗?", - isDir: "[STR] 是目录吗?", - copy: "将 [STR] 复制到 [STR2]", - fileName: "[STR] 的文件名", - dirName: "[STR] 的目录", - sync_new: "将 [STR] 重命名为 [STR2]", - permSet: "[ACTION] [STR] 的 [PERM] 权限", - permAdd: "添加", - permRemove: "移除", - permCreate: "创建", - permDelete: "删除", - permSee: "查看", - permRead: "读取", - permWrite: "写入", - permList: "列出 [STR] 的权限", - permControl: "控制", - toggleLogging: "[STATE]控制台日志", - logOn: "开启", - logOff: "关闭", - getLastError: "上一个错误", - wasRead: "是否读取?", - wasWritten: "是否写入?", - version: "版本", - dateCreated: "[STR] 的创建日期", - dateModified: "[STR] 的修改日期", - dateAccessed: "[STR] 的访问日期", - }, -}); (function (Scratch) { "use strict"; @@ -549,7 +35,10 @@ Scratch.translate.setup({ return { id: "lithiumFS", - name: "Lithium FS", + name: Scratch.translate({ + id: "lithiumFS.name", + default: "Lithium FS", + }), color1: "#d52246", color2: "#a61734", color3: "#7f1026", @@ -1783,7 +1272,6 @@ Scratch.translate.setup({ ); } - let migrationData = {}; let needsMigration = false; if (version === "1.0.5") { @@ -1796,7 +1284,6 @@ Scratch.translate.setup({ "Import failed: v1.0.5 data is corrupt (missing 'fs' object)." ); } - migrationData = data.fs; } else if ( version === "1.0.4" || version === "1.0.3" || From faf25c581548363567d768bc476b7a535783e42b Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 01:04:34 +0000 Subject: [PATCH 06/22] fix: Real Scratch URLs! Made a Scratch account only for this purpose. --- extensions/ohgodwhy2k/lithiumfs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index 7cf7c57401..84d7f70c4f 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -1,8 +1,8 @@ // Name: Lithium FS // ID: lithiumFS // Description: Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more. -// By: ohgodwhy2k -// Original: 0832 +// By: ohgodwhy2k +// Original: 0832 // License: MIT (function (Scratch) { From 8ffa33cdad5956bc28a521ed654214fe9155a061 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 22:44:36 +0000 Subject: [PATCH 07/22] fix: Remove hallucinated description in getInfo() --- extensions/ohgodwhy2k/lithiumfs.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index 84d7f70c4f..bdf9fe0656 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -39,12 +39,11 @@ id: "lithiumFS.name", default: "Lithium FS", }), + color1: "#d52246", color2: "#a61734", color3: "#7f1026", - - description: - "Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more.", + /** @type {any} */ blocks: [ { From 456e20a612d60219eef66c187dcc9ba02bbbd658 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 22:45:53 +0000 Subject: [PATCH 08/22] fix: Prettify lithiumfs.json --- extensions/ohgodwhy2k/lithiumfs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/ohgodwhy2k/lithiumfs.js index bdf9fe0656..325306babb 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/ohgodwhy2k/lithiumfs.js @@ -39,11 +39,11 @@ id: "lithiumFS.name", default: "Lithium FS", }), - + color1: "#d52246", color2: "#a61734", color3: "#7f1026", - + /** @type {any} */ blocks: [ { From 09383efbb7949fd1497f1759ee08811df5cfb24f Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 23 Nov 2025 00:22:47 +0000 Subject: [PATCH 09/22] feat: Create banner for LiFS --- images/ohgodwhy2k/lithiumfs.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 images/ohgodwhy2k/lithiumfs.svg diff --git a/images/ohgodwhy2k/lithiumfs.svg b/images/ohgodwhy2k/lithiumfs.svg new file mode 100644 index 0000000000..31aafa773a --- /dev/null +++ b/images/ohgodwhy2k/lithiumfs.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7804e06b4ee85be961d76d44cd8e2d2f30c11c4a Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 19:29:49 -0600 Subject: [PATCH 10/22] fix: Update lithiumfs.js to use my new Scratch username NOTE: This breaks the banner! I'll rename the banner next, shouldn't take me a long time. --- extensions/{ohgodwhy2k => kx1bx1}/lithiumfs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename extensions/{ohgodwhy2k => kx1bx1}/lithiumfs.js (99%) diff --git a/extensions/ohgodwhy2k/lithiumfs.js b/extensions/kx1bx1/lithiumfs.js similarity index 99% rename from extensions/ohgodwhy2k/lithiumfs.js rename to extensions/kx1bx1/lithiumfs.js index 325306babb..9753e0d259 100644 --- a/extensions/ohgodwhy2k/lithiumfs.js +++ b/extensions/kx1bx1/lithiumfs.js @@ -1,7 +1,7 @@ // Name: Lithium FS // ID: lithiumFS // Description: Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more. -// By: ohgodwhy2k +// By: kx1bx1 // Original: 0832 // License: MIT From 495016f85c59658c2894f882fa3585a745d9a423 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sat, 22 Nov 2025 19:30:45 -0600 Subject: [PATCH 11/22] fix: Update LiFS banner to use new Scratch username Hopefully this one works(?) --- images/{ohgodwhy2k => kx1bx1}/lithiumfs.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename images/{ohgodwhy2k => kx1bx1}/lithiumfs.svg (99%) diff --git a/images/ohgodwhy2k/lithiumfs.svg b/images/kx1bx1/lithiumfs.svg similarity index 99% rename from images/ohgodwhy2k/lithiumfs.svg rename to images/kx1bx1/lithiumfs.svg index 31aafa773a..79c9c43fed 100644 --- a/images/ohgodwhy2k/lithiumfs.svg +++ b/images/kx1bx1/lithiumfs.svg @@ -1 +1 @@ - \ No newline at end of file + From f145e74ac1ecb60ab3c373444c343c7b4b814bb6 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 23 Nov 2025 01:38:01 +0000 Subject: [PATCH 12/22] fix: Update extensions.json to use new Scratch username --- extensions/extensions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/extensions.json b/extensions/extensions.json index 5cfd4698b0..6bd073db52 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -79,7 +79,7 @@ "CST1229/zip", "CST1229/images", "TheShovel/LZ-String", - "ohgodwhy2k/lithiumfs", + "kx1bx1/lithiumfs", "0832/rxFS2", "NexusKitten/sgrab", "NOname-awa/graphics2d", From b04761ea406106c70bb07b36ae9664cc63b0ac80 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 23 Nov 2025 05:41:18 +0000 Subject: [PATCH 13/22] fix: Match package-lock.json with package.json --- package-lock.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca9e99e66c..d660a0fcae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -289,7 +289,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -702,7 +701,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", From eae6ef0a3be1c6bd37e3e715a264c5cfa530c5f1 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 23 Nov 2025 05:43:02 +0000 Subject: [PATCH 14/22] fix: Scratch.translate calls not switched to strings --- extensions/kx1bx1/lithiumfs.js | 309 ++++++++++----------------------- 1 file changed, 96 insertions(+), 213 deletions(-) diff --git a/extensions/kx1bx1/lithiumfs.js b/extensions/kx1bx1/lithiumfs.js index 9753e0d259..b861d31399 100644 --- a/extensions/kx1bx1/lithiumfs.js +++ b/extensions/kx1bx1/lithiumfs.js @@ -34,11 +34,7 @@ getInfo() { return { id: "lithiumFS", - - name: Scratch.translate({ - id: "lithiumFS.name", - default: "Lithium FS", - }), + name: "Lithium FS", color1: "#d52246", color2: "#a61734", @@ -49,10 +45,7 @@ { opcode: "start", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "start", - default: "create [STR]", - }), + text: "create [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -63,10 +56,7 @@ { opcode: "folder", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "folder", - default: "set [STR] to [STR2]", - }), + text: "set [STR] to [STR2]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -74,20 +64,14 @@ }, STR2: { type: Scratch.ArgumentType.STRING, - defaultValue: Scratch.translate({ - id: "folder_default", - default: "LiFS is good!", - }), + defaultValue: "LiFS is good!", }, }, }, { opcode: "open", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "open", - default: "open [STR]", - }), + text: "open [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -98,10 +82,7 @@ { opcode: "del", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "del", - default: "delete [STR]", - }), + text: "delete [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -112,10 +93,7 @@ { opcode: "list", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "list_new", - default: "list [TYPE] under [STR]", - }), + text: "list [TYPE] under [STR]", arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -133,10 +111,7 @@ { opcode: "copy", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "copy", - default: "copy [STR] to [STR2]", - }), + text: "copy [STR] to [STR2]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -151,10 +126,7 @@ { opcode: "sync", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "sync_new", - default: "rename [STR] to [STR2]", - }), + text: "rename [STR] to [STR2]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -169,10 +141,7 @@ { opcode: "exists", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "exists", - default: "does [STR] exist?", - }), + text: "does [STR] exist?", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -183,10 +152,7 @@ { opcode: "isFile", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "isFile", - default: "is [STR] a file?", - }), + text: "is [STR] a file?", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -197,10 +163,7 @@ { opcode: "isDir", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "isDir", - default: "is [STR] a directory?", - }), + text: "is [STR] a directory?", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -211,10 +174,7 @@ { opcode: "fileName", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "fileName", - default: "file name of [STR]", - }), + text: "file name of [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -225,10 +185,7 @@ { opcode: "dirName", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dirName", - default: "directory of [STR]", - }), + text: "directory of [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -240,10 +197,7 @@ { opcode: "dateCreated", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateCreated", - default: "date created of [STR]", - }), + text: "date created of [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -254,10 +208,7 @@ { opcode: "dateModified", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateModified", - default: "date modified of [STR]", - }), + text: "date modified of [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -268,10 +219,7 @@ { opcode: "dateAccessed", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "dateAccessed", - default: "date accessed of [STR]", - }), + text: "date accessed of [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -284,10 +232,7 @@ { opcode: "setLimit", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "setLimit", - default: "set size limit for [DIR] to [BYTES] bytes", - }), + text: "set size limit for [DIR] to [BYTES] bytes", arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -302,10 +247,7 @@ { opcode: "removeLimit", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "removeLimit", - default: "remove size limit for [DIR]", - }), + text: "remove size limit for [DIR]", arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -316,10 +258,7 @@ { opcode: "getLimit", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getLimit", - default: "size limit of [DIR] (bytes)", - }), + text: "size limit of [DIR] (bytes)", arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -330,10 +269,7 @@ { opcode: "getSize", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getSize", - default: "current size of [DIR] (bytes)", - }), + text: "current size of [DIR] (bytes)", arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -344,10 +280,7 @@ { opcode: "setPerm", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "permSet", - default: "[ACTION] [PERM] permission for [STR]", - }), + text: "[ACTION] [PERM] permission for [STR]", arguments: { ACTION: { type: Scratch.ArgumentType.STRING, @@ -368,10 +301,7 @@ { opcode: "listPerms", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "permList", - default: "list permissions for [STR]", - }), + text: "list permissions for [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -384,19 +314,13 @@ { opcode: "clean", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "clean", - default: "clear the file system", - }), + text: "clear the file system", arguments: {}, }, { opcode: "in", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "in", - default: "import file system from [STR]", - }), + text: "import file system from [STR]", arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -407,43 +331,28 @@ { opcode: "out", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "out", - default: "export file system", - }), + text: "export file system", arguments: {}, }, { opcode: "wasRead", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "wasRead", - default: "was read?", - }), + text: "was read?", }, { opcode: "wasWritten", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate({ - id: "wasWritten", - default: "was written?", - }), + text: "was written?", }, { opcode: "getLastError", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "getLastError", - default: "last error", - }), + text: "last error", }, { opcode: "toggleLogging", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate({ - id: "toggleLogging", - default: "turn [STATE] console logging", - }), + text: "turn [STATE] console logging", arguments: { STATE: { type: Scratch.ArgumentType.STRING, @@ -455,10 +364,7 @@ { opcode: "getVersion", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate({ - id: "version", - default: "version", - }), + text: "version", }, ], menus: { @@ -466,24 +372,15 @@ acceptReporters: true, items: [ { - text: Scratch.translate({ - id: "listMenuAll", - default: "all", - }), + text: "all", value: "all", }, { - text: Scratch.translate({ - id: "listMenuFiles", - default: "files", - }), + text: "files", value: "files", }, { - text: Scratch.translate({ - id: "listMenuDirs", - default: "directories", - }), + text: "directories", value: "directories", }, ], @@ -492,17 +389,11 @@ acceptReporters: true, items: [ { - text: Scratch.translate({ - id: "permAdd", - default: "add", - }), + text: "add", value: "add", }, { - text: Scratch.translate({ - id: "permRemove", - default: "remove", - }), + text: "remove", value: "remove", }, ], @@ -511,45 +402,27 @@ acceptReporters: true, items: [ { - text: Scratch.translate({ - id: "permCreate", - default: "create", - }), + text: "create", value: "create", }, { - text: Scratch.translate({ - id: "permDelete", - default: "delete", - }), + text: "delete", value: "delete", }, { - text: Scratch.translate({ - id: "permSee", - default: "see", - }), + text: "see", value: "see", }, { - text: Scratch.translate({ - id: "permRead", - default: "read", - }), + text: "read", value: "read", }, { - text: Scratch.translate({ - id: "permWrite", - default: "write", - }), + text: "write", value: "write", }, { - text: Scratch.translate({ - id: "permControl", - default: "control", - }), + text: "control", value: "control", }, ], @@ -558,17 +431,11 @@ acceptReporters: true, items: [ { - text: Scratch.translate({ - id: "logOn", - default: "on", - }), + text: "on", value: "on", }, { - text: Scratch.translate({ - id: "logOff", - default: "off", - }), + text: "off", value: "off", }, ], @@ -877,9 +744,13 @@ if (isDir) { this._log("Renaming directory and children..."); + // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database + const path1Prefix = path1.endsWith("/") ? path1 : path1 + "/"; + const toRename = []; for (const [key, value] of this.fs.entries()) { - if (key.startsWith(path1)) { + // Check if it is the directory itself OR a child (checked via strict prefix) + if (key === path1 || key.startsWith(path1Prefix)) { toRename.push({ oldKey: key, value: value, @@ -947,8 +818,12 @@ let totalDeltaSize = 0; const path1Length = path1.length; + // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database + const path1Prefix = path1.endsWith("/") ? path1 : path1 + "/"; + for (const [key, value] of this.fs.entries()) { - if (key.startsWith(path1)) { + // Check if it is the directory itself OR a child (checked via strict prefix) + if (key === path1 || key.startsWith(path1Prefix)) { if (!this._isPathDir(key)) { totalDeltaSize += this._getStringSize(value.content); } @@ -1019,18 +894,6 @@ const path = this._normalizePath(STR); this._log("Block: create", path); - if (this._isPathDir(path) && path.length > 1) { - const fileName = path - .substring(0, path.length - 1) - .split("/") - .pop(); - if (fileName.includes(".")) { - this._warn( - `Path "${path}" looks like a file but is being treated as a directory due to the trailing slash.` - ); - } - } - if (path === "/") { return this._setError( "Create failed: Cannot create root directory '/'" @@ -1127,10 +990,14 @@ const isDir = this._isPathDir(path); + // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database + const pathPrefix = path.endsWith("/") ? path : path + "/"; + const toDelete = []; for (const currentPath of this.fs.keys()) { if (isDir) { - if (currentPath.startsWith(path)) { + // Check if it is the directory itself OR a child (checked via strict prefix) + if (currentPath === path || currentPath.startsWith(pathPrefix)) { toDelete.push(currentPath); } } else { @@ -1487,9 +1354,19 @@ const isDir = this._isPathDir(path); const now = Date.now(); + // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database + const pathPrefix = path.endsWith("/") ? path : path + "/"; + this._log("Applying changes..."); for (const [currentPath, entry] of this.fs.entries()) { - if ((isDir && currentPath.startsWith(path)) || currentPath === path) { + // Check if it is the directory itself OR a child (checked via strict prefix) + // If strict match: currentPath === path + // If child match: currentPath.startsWith(pathPrefix) + if ( + (isDir && + (currentPath === path || currentPath.startsWith(pathPrefix))) || + currentPath === path + ) { entry.perms[PERM] = newValue; entry.modified = now; entry.accessed = now; @@ -1579,14 +1456,15 @@ setLimit({ DIR, BYTES }) { this.lastError = ""; - const path = this._normalizePath(DIR); - this._log("Block: setLimit", path, "to", BYTES, "bytes"); + let path = this._normalizePath(DIR); + // POLISH: Automatically append slash if user forgot it, instead of erroring if (!this._isPathDir(path)) { - return this._setError( - "setLimit failed: Path must be a directory (end with /)" - ); + path += "/"; } + + this._log("Block: setLimit", path, "to", BYTES, "bytes"); + if (!this.hasPermission(path, "control")) { return this._setError( `setLimit failed: No 'control' permission on ${path}` @@ -1618,14 +1496,15 @@ removeLimit({ DIR }) { this.lastError = ""; - const path = this._normalizePath(DIR); - this._log("Block: removeLimit", path); + let path = this._normalizePath(DIR); + // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { - return this._setError( - "removeLimit failed: Path must be a directory (end with /)" - ); + path += "/"; } + + this._log("Block: removeLimit", path); + if (!this.hasPermission(path, "control")) { return this._setError( `removeLimit failed: No 'control' permission on ${path}` @@ -1647,14 +1526,16 @@ } getLimit({ DIR }) { - const path = this._normalizePath(DIR); - this._log("Block: getLimit", path); - this.readActivity = true; + let path = this._normalizePath(DIR); + // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { - this._warn("getLimit failed: Path must be a directory (end with /)"); - return -1; + path += "/"; } + + this._log("Block: getLimit", path); + this.readActivity = true; + if (!this.hasPermission(path, "see")) { this._warn(`getLimit failed: No 'see' permission for "${path}"`); return -1; @@ -1673,14 +1554,16 @@ } getSize({ DIR }) { - const path = this._normalizePath(DIR); - this._log("Block: getSize", path); - this.readActivity = true; + let path = this._normalizePath(DIR); + // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { - this._warn("getSize failed: Path must be a directory (end with /)"); - return 0; + path += "/"; } + + this._log("Block: getSize", path); + this.readActivity = true; + if (!this.hasPermission(path, "see")) { this._warn(`getSize failed: No 'see' permission for "${path}"`); return 0; From d0c2fdbf31ecde11b9b91245b2a1b550f6133b9c Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 23 Nov 2025 06:37:24 +0000 Subject: [PATCH 15/22] fix: Various unexplainable issues with Scratch.translate --- extensions/kx1bx1/lithiumfs.js | 79 ++++++++++++++-------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/extensions/kx1bx1/lithiumfs.js b/extensions/kx1bx1/lithiumfs.js index b861d31399..4dafd7fd67 100644 --- a/extensions/kx1bx1/lithiumfs.js +++ b/extensions/kx1bx1/lithiumfs.js @@ -34,18 +34,17 @@ getInfo() { return { id: "lithiumFS", - name: "Lithium FS", + name: Scratch.translate("Lithium FS"), color1: "#d52246", color2: "#a61734", color3: "#7f1026", - /** @type {any} */ blocks: [ { opcode: "start", blockType: Scratch.BlockType.COMMAND, - text: "create [STR]", + text: Scratch.translate("create [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -56,7 +55,7 @@ { opcode: "folder", blockType: Scratch.BlockType.COMMAND, - text: "set [STR] to [STR2]", + text: Scratch.translate("set [STR] to [STR2]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -71,7 +70,7 @@ { opcode: "open", blockType: Scratch.BlockType.REPORTER, - text: "open [STR]", + text: Scratch.translate("open [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -82,7 +81,7 @@ { opcode: "del", blockType: Scratch.BlockType.COMMAND, - text: "delete [STR]", + text: Scratch.translate("delete [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -93,7 +92,7 @@ { opcode: "list", blockType: Scratch.BlockType.REPORTER, - text: "list [TYPE] under [STR]", + text: Scratch.translate("list [TYPE] under [STR]"), arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -111,7 +110,7 @@ { opcode: "copy", blockType: Scratch.BlockType.COMMAND, - text: "copy [STR] to [STR2]", + text: Scratch.translate("copy [STR] to [STR2]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -126,7 +125,7 @@ { opcode: "sync", blockType: Scratch.BlockType.COMMAND, - text: "rename [STR] to [STR2]", + text: Scratch.translate("rename [STR] to [STR2]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -141,7 +140,7 @@ { opcode: "exists", blockType: Scratch.BlockType.BOOLEAN, - text: "does [STR] exist?", + text: Scratch.translate("does [STR] exist?"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -152,7 +151,7 @@ { opcode: "isFile", blockType: Scratch.BlockType.BOOLEAN, - text: "is [STR] a file?", + text: Scratch.translate("is [STR] a file?"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -163,7 +162,7 @@ { opcode: "isDir", blockType: Scratch.BlockType.BOOLEAN, - text: "is [STR] a directory?", + text: Scratch.translate("is [STR] a directory?"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -174,7 +173,7 @@ { opcode: "fileName", blockType: Scratch.BlockType.REPORTER, - text: "file name of [STR]", + text: Scratch.translate("file name of [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -185,7 +184,7 @@ { opcode: "dirName", blockType: Scratch.BlockType.REPORTER, - text: "directory of [STR]", + text: Scratch.translate("directory of [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -197,7 +196,7 @@ { opcode: "dateCreated", blockType: Scratch.BlockType.REPORTER, - text: "date created of [STR]", + text: Scratch.translate("date created of [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -208,7 +207,7 @@ { opcode: "dateModified", blockType: Scratch.BlockType.REPORTER, - text: "date modified of [STR]", + text: Scratch.translate("date modified of [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -219,7 +218,7 @@ { opcode: "dateAccessed", blockType: Scratch.BlockType.REPORTER, - text: "date accessed of [STR]", + text: Scratch.translate("date accessed of [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -232,7 +231,9 @@ { opcode: "setLimit", blockType: Scratch.BlockType.COMMAND, - text: "set size limit for [DIR] to [BYTES] bytes", + text: Scratch.translate( + "set size limit for [DIR] to [BYTES] bytes" + ), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -247,7 +248,7 @@ { opcode: "removeLimit", blockType: Scratch.BlockType.COMMAND, - text: "remove size limit for [DIR]", + text: Scratch.translate("remove size limit for [DIR]"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -258,7 +259,7 @@ { opcode: "getLimit", blockType: Scratch.BlockType.REPORTER, - text: "size limit of [DIR] (bytes)", + text: Scratch.translate("size limit of [DIR] (bytes)"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -269,7 +270,7 @@ { opcode: "getSize", blockType: Scratch.BlockType.REPORTER, - text: "current size of [DIR] (bytes)", + text: Scratch.translate("current size of [DIR] (bytes)"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -280,7 +281,7 @@ { opcode: "setPerm", blockType: Scratch.BlockType.COMMAND, - text: "[ACTION] [PERM] permission for [STR]", + text: Scratch.translate("[ACTION] [PERM] permission for [STR]"), arguments: { ACTION: { type: Scratch.ArgumentType.STRING, @@ -301,7 +302,7 @@ { opcode: "listPerms", blockType: Scratch.BlockType.REPORTER, - text: "list permissions for [STR]", + text: Scratch.translate("list permissions for [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -314,13 +315,13 @@ { opcode: "clean", blockType: Scratch.BlockType.COMMAND, - text: "clear the file system", + text: Scratch.translate("clear the file system"), arguments: {}, }, { opcode: "in", blockType: Scratch.BlockType.COMMAND, - text: "import file system from [STR]", + text: Scratch.translate("import file system from [STR]"), arguments: { STR: { type: Scratch.ArgumentType.STRING, @@ -331,28 +332,28 @@ { opcode: "out", blockType: Scratch.BlockType.REPORTER, - text: "export file system", + text: Scratch.translate("export file system"), arguments: {}, }, { opcode: "wasRead", blockType: Scratch.BlockType.BOOLEAN, - text: "was read?", + text: Scratch.translate("was read?"), }, { opcode: "wasWritten", blockType: Scratch.BlockType.BOOLEAN, - text: "was written?", + text: Scratch.translate("was written?"), }, { opcode: "getLastError", blockType: Scratch.BlockType.REPORTER, - text: "last error", + text: Scratch.translate("last error"), }, { opcode: "toggleLogging", blockType: Scratch.BlockType.COMMAND, - text: "turn [STATE] console logging", + text: Scratch.translate("turn [STATE] console logging"), arguments: { STATE: { type: Scratch.ArgumentType.STRING, @@ -364,7 +365,7 @@ { opcode: "getVersion", blockType: Scratch.BlockType.REPORTER, - text: "version", + text: Scratch.translate("version"), }, ], menus: { @@ -744,12 +745,10 @@ if (isDir) { this._log("Renaming directory and children..."); - // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database const path1Prefix = path1.endsWith("/") ? path1 : path1 + "/"; const toRename = []; for (const [key, value] of this.fs.entries()) { - // Check if it is the directory itself OR a child (checked via strict prefix) if (key === path1 || key.startsWith(path1Prefix)) { toRename.push({ oldKey: key, @@ -818,11 +817,9 @@ let totalDeltaSize = 0; const path1Length = path1.length; - // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database const path1Prefix = path1.endsWith("/") ? path1 : path1 + "/"; for (const [key, value] of this.fs.entries()) { - // Check if it is the directory itself OR a child (checked via strict prefix) if (key === path1 || key.startsWith(path1Prefix)) { if (!this._isPathDir(key)) { totalDeltaSize += this._getStringSize(value.content); @@ -990,13 +987,11 @@ const isDir = this._isPathDir(path); - // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database const pathPrefix = path.endsWith("/") ? path : path + "/"; const toDelete = []; for (const currentPath of this.fs.keys()) { if (isDir) { - // Check if it is the directory itself OR a child (checked via strict prefix) if (currentPath === path || currentPath.startsWith(pathPrefix)) { toDelete.push(currentPath); } @@ -1354,14 +1349,10 @@ const isDir = this._isPathDir(path); const now = Date.now(); - // CRITICAL FIX: Ensure prefix ends with "/" to avoid matching /data with /database const pathPrefix = path.endsWith("/") ? path : path + "/"; this._log("Applying changes..."); for (const [currentPath, entry] of this.fs.entries()) { - // Check if it is the directory itself OR a child (checked via strict prefix) - // If strict match: currentPath === path - // If child match: currentPath.startsWith(pathPrefix) if ( (isDir && (currentPath === path || currentPath.startsWith(pathPrefix))) || @@ -1458,7 +1449,6 @@ this.lastError = ""; let path = this._normalizePath(DIR); - // POLISH: Automatically append slash if user forgot it, instead of erroring if (!this._isPathDir(path)) { path += "/"; } @@ -1498,7 +1488,6 @@ this.lastError = ""; let path = this._normalizePath(DIR); - // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { path += "/"; } @@ -1528,7 +1517,6 @@ getLimit({ DIR }) { let path = this._normalizePath(DIR); - // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { path += "/"; } @@ -1556,7 +1544,6 @@ getSize({ DIR }) { let path = this._normalizePath(DIR); - // POLISH: Automatically append slash if user forgot it if (!this._isPathDir(path)) { path += "/"; } @@ -1633,5 +1620,5 @@ } } - Scratch.extensions.register(new LiFS()); + Scratch.extensions.register(/** @type {any} */ (new LiFS())); })(Scratch); From 440b82b1b1a32ae7ffd47bf69a760883ed087674 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Mon, 24 Nov 2025 18:54:52 +0000 Subject: [PATCH 16/22] fix: Update LiFS name to RubyFS --- extensions/extensions.json | 2 +- extensions/kx1bx1/{lithiumfs.js => rubyfs.js} | 78 ++++++++++--------- images/kx1bx1/lithiumfs.svg | 1 - images/kx1bx1/rubyfs.svg | 1 + 4 files changed, 42 insertions(+), 40 deletions(-) rename extensions/kx1bx1/{lithiumfs.js => rubyfs.js} (95%) delete mode 100644 images/kx1bx1/lithiumfs.svg create mode 100644 images/kx1bx1/rubyfs.svg diff --git a/extensions/extensions.json b/extensions/extensions.json index 6bd073db52..8ad15b3a03 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -79,7 +79,7 @@ "CST1229/zip", "CST1229/images", "TheShovel/LZ-String", - "kx1bx1/lithiumfs", + "kx1bx1/rubyfs", "0832/rxFS2", "NexusKitten/sgrab", "NOname-awa/graphics2d", diff --git a/extensions/kx1bx1/lithiumfs.js b/extensions/kx1bx1/rubyfs.js similarity index 95% rename from extensions/kx1bx1/lithiumfs.js rename to extensions/kx1bx1/rubyfs.js index 4dafd7fd67..0fd9f420f3 100644 --- a/extensions/kx1bx1/lithiumfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -1,10 +1,12 @@ -// Name: Lithium FS -// ID: lithiumFS -// Description: Advancement of rxFS. Blocks for interacting with an in-memory filesystem with permissions, size limits, and more. +// Name: RubyFS +// ID: rubyFS +// Description: A structured, in-memory file system for Scratch projects. (Previously RubyFS) // By: kx1bx1 // Original: 0832 // License: MIT +// Totally did NOT use Find & Replace... + (function (Scratch) { "use strict"; @@ -19,22 +21,22 @@ const extensionVersion = "1.0.5"; - class LiFS { + class RubyFS { constructor() { this.fs = new Map(); - this.liFSLogEnabled = false; + this.RubyFSLogEnabled = false; this.lastError = ""; this.readActivity = false; this.writeActivity = false; - this._log("Initializing LiFS extension..."); + this._log("Initializing RubyFS..."); this._internalClean(); } getInfo() { return { - id: "lithiumFS", - name: Scratch.translate("Lithium FS"), + id: "rubyFS", + name: Scratch.translate("RubyFS"), color1: "#d52246", color2: "#a61734", @@ -48,7 +50,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -59,11 +61,11 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, STR2: { type: Scratch.ArgumentType.STRING, - defaultValue: "LiFS is good!", + defaultValue: "RubyFS is good!", }, }, }, @@ -74,7 +76,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -85,7 +87,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -101,7 +103,7 @@ }, STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -114,11 +116,11 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, STR2: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/copy_of_example.txt", + defaultValue: "/RubyFS/copy_of_example.txt", }, }, }, @@ -129,11 +131,11 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, STR2: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/new_example.txt", + defaultValue: "/RubyFS/new_example.txt", }, }, }, @@ -144,7 +146,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -155,7 +157,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -166,7 +168,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -177,7 +179,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -188,7 +190,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -200,7 +202,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -211,7 +213,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -222,7 +224,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/example.txt", + defaultValue: "/RubyFS/example.txt", }, }, }, @@ -237,7 +239,7 @@ arguments: { DIR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, BYTES: { type: Scratch.ArgumentType.NUMBER, @@ -252,7 +254,7 @@ arguments: { DIR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -263,7 +265,7 @@ arguments: { DIR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -274,7 +276,7 @@ arguments: { DIR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -295,7 +297,7 @@ }, STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -306,7 +308,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "/LiFS/", + defaultValue: "/RubyFS/", }, }, }, @@ -446,14 +448,14 @@ } _log(message, ...args) { - if (this.liFSLogEnabled) { - console.log(`[LiFS] ${message}`, ...args); + if (this.RubyFSLogEnabled) { + console.log(`[RubyFS] ${message}`, ...args); } } _warn(message, ...args) { - if (this.liFSLogEnabled) { - console.warn(`[LiFS] ${message}`, ...args); + if (this.RubyFSLogEnabled) { + console.warn(`[RubyFS] ${message}`, ...args); } } @@ -1441,7 +1443,7 @@ } toggleLogging({ STATE }) { - this.liFSLogEnabled = STATE === "on"; + this.RubyFSLogEnabled = STATE === "on"; this._log("Console logging turned", STATE); } @@ -1620,5 +1622,5 @@ } } - Scratch.extensions.register(/** @type {any} */ (new LiFS())); + Scratch.extensions.register(/** @type {any} */ (new RubyFS())); })(Scratch); diff --git a/images/kx1bx1/lithiumfs.svg b/images/kx1bx1/lithiumfs.svg deleted file mode 100644 index 79c9c43fed..0000000000 --- a/images/kx1bx1/lithiumfs.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/images/kx1bx1/rubyfs.svg b/images/kx1bx1/rubyfs.svg new file mode 100644 index 0000000000..f5d40a4fff --- /dev/null +++ b/images/kx1bx1/rubyfs.svg @@ -0,0 +1 @@ + \ No newline at end of file From 6fefceaa7fc2460cf5dfe2aedd7ec3f8ac19a6bd Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Mon, 24 Nov 2025 18:59:37 +0000 Subject: [PATCH 17/22] fix: No punctuation in description --- extensions/kx1bx1/rubyfs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index 0fd9f420f3..6b01967bad 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -1,6 +1,6 @@ // Name: RubyFS // ID: rubyFS -// Description: A structured, in-memory file system for Scratch projects. (Previously RubyFS) +// Description: A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). // By: kx1bx1 // Original: 0832 // License: MIT From d34f701de80fa6f328948c7c6e5f60d5608b70b9 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Sun, 30 Nov 2025 23:32:05 -0600 Subject: [PATCH 18/22] feat: Bump RubyFS to v1.1.2 > This is guaranteed to fail validation because I didn't bother prettifying... if it does, I'll probably fix it tomorrow (it's 11 PM for me right now). --- extensions/kx1bx1/rubyfs.js | 774 +++++++++++++++++++++++++++++------- 1 file changed, 628 insertions(+), 146 deletions(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index 6b01967bad..267394a6fb 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -5,8 +5,6 @@ // Original: 0832 // License: MIT -// Totally did NOT use Find & Replace... - (function (Scratch) { "use strict"; @@ -19,7 +17,7 @@ control: true, }; - const extensionVersion = "1.0.5"; + const extensionVersion = "1.1.2"; class RubyFS { constructor() { @@ -28,6 +26,8 @@ this.lastError = ""; this.readActivity = false; this.writeActivity = false; + this.lastReadPath = ""; + this.lastWritePath = ""; this._log("Initializing RubyFS..."); this._internalClean(); @@ -42,7 +42,14 @@ color2: "#a61734", color3: "#7f1026", + description: Scratch.translate("A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). (Use 'turn on console logging' for debugging.)"), + blocks: [ + + { + blockType: Scratch.BlockType.LABEL, + text: "Core Operations", + }, { opcode: "start", blockType: Scratch.BlockType.COMMAND, @@ -94,7 +101,7 @@ { opcode: "list", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("list [TYPE] under [STR]"), + text: Scratch.translate("list [TYPE] under [STR] as JSON"), arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -107,8 +114,11 @@ }, }, }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "File & Directory Utilities", + }, { opcode: "copy", blockType: Scratch.BlockType.COMMAND, @@ -195,6 +205,10 @@ }, }, + { + blockType: Scratch.BlockType.LABEL, + text: "Timestamp Utilities", + }, { opcode: "dateCreated", blockType: Scratch.BlockType.REPORTER, @@ -228,8 +242,11 @@ }, }, }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "Permissions & Limits", + }, { opcode: "setLimit", blockType: Scratch.BlockType.COMMAND, @@ -312,8 +329,11 @@ }, }, }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "Import & Export", + }, { opcode: "clean", blockType: Scratch.BlockType.COMMAND, @@ -327,7 +347,7 @@ arguments: { STR: { type: Scratch.ArgumentType.STRING, - defaultValue: '{"version":"1.0.5","fs":{}}', + defaultValue: '{"version":"1.1.2","fs":{}}', }, }, }, @@ -337,6 +357,47 @@ text: Scratch.translate("export file system"), arguments: {}, }, + { + opcode: "exportFileBase64", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("export file [STR] as [FORMAT]"), + arguments: { + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/RubyFS/example.txt", + }, + FORMAT: { + type: Scratch.ArgumentType.STRING, + menu: "BASE64_FORMAT_MENU", + defaultValue: "base64", + }, + }, + }, + { + opcode: "importFileBase64", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("import [FORMAT] [STR] to file [STR2]"), + arguments: { + FORMAT: { + type: Scratch.ArgumentType.STRING, + menu: "BASE64_FORMAT_MENU", + defaultValue: "base64", + }, + STR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "UmVuZUZTIWlzZ29vZCE=", + }, + STR2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "/RubyFS/imported.txt", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Debugging & Activity", + }, { opcode: "wasRead", blockType: Scratch.BlockType.BOOLEAN, @@ -347,6 +408,16 @@ blockType: Scratch.BlockType.BOOLEAN, text: Scratch.translate("was written?"), }, + { + opcode: "getLastReadPath", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("last path read"), + }, + { + opcode: "getLastWritePath", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("last path written"), + }, { opcode: "getLastError", blockType: Scratch.BlockType.REPORTER, @@ -369,6 +440,11 @@ blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("version"), }, + { + opcode: "runIntegrityTest", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("run integrity test"), + }, ], menus: { LIST_TYPE_MENU: { @@ -443,6 +519,19 @@ }, ], }, + BASE64_FORMAT_MENU: { + acceptReporters: true, + items: [ + { + text: "Base64 String", + value: "base64", + }, + { + text: "Data URL", + value: "data_url", + }, + ], + }, }, }; } @@ -464,9 +553,55 @@ this.lastError = message; } + _encodeUTF8Base64(str) { + try { + + return btoa(str); + } catch (e) { + this._setError(`Base64 Encode Error: ${e.message}. (Note: Unicode text is not supported for export)`); + return ""; + } + } + + _decodeUTF8Base64(base64) { + + return atob(base64); + } + + _getMimeType(path) { + const extension = path.split('.').pop().toLowerCase(); + switch (extension) { + case 'txt': + return 'text/plain'; + case 'json': + return 'application/json'; + case 'svg': + return 'image/svg+xml'; + case 'png': + return 'image/png'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'gif': + return 'image/gif'; + case 'zip': + return 'application/zip'; + case 'sprite3': + return 'application/x-zip-compressed'; + case 'sb3': + return 'application/x-zip-compressed'; + case 'wav': + return 'audio/wav'; + case 'mp3': + return 'audio/mpeg'; + default: + return 'application/octet-stream'; + } + } + _normalizePath(path) { - if (typeof path !== "string" || path.length === 0) { - return "/"; + if (typeof path !== "string" || !path.trim()) { + return null; } const hadTrailingSlash = path.length > 1 && path.endsWith("/"); @@ -538,14 +673,15 @@ for (let i = 0; i < str.length; i++) { const charCode = str.charCodeAt(i); if (charCode < 0x0080) { - length += 1; + length += 1; } else if (charCode < 0x0800) { - length += 2; + length += 2; } else if (charCode < 0xd800 || charCode > 0xdfff) { - length += 3; + length += 3; } else { - length += 4; - i++; + + length += 4; + i++; } } return length; @@ -644,12 +780,17 @@ accessed: now, }); this.writeActivity = true; + this.lastWritePath = path; this._log("InternalCreate successful:", path); return true; } hasPermission(path, action) { const normPath = this._normalizePath(path); + if (!normPath) { + this._log("Permission check failed: Invalid path"); + return false; + } this._log("Checking permission:", action, "on", normPath); const entry = this.fs.get(normPath); @@ -692,6 +833,7 @@ }); this._log("Internal: File system reset to root."); this.writeActivity = true; + this.lastWritePath = "/"; } clean() { @@ -707,8 +849,15 @@ this.lastError = ""; const path1 = this._normalizePath(STR); const path2 = this._normalizePath(STR2); + if (!path1 || !path2) { + return this._setError("Invalid path provided."); + } this._log("Block: rename", path1, "to", path2); + if (path1 === "/") { + return this._setError("Rename failed: Root directory cannot be renamed"); + } + if (!this.hasPermission(path1, "delete")) { return this._setError( `Rename failed: No 'delete' permission on ${path1}` @@ -719,6 +868,17 @@ `Rename failed: Destination ${path2} already exists` ); } + + if (this._isPathDir(path2)) { + if (this.fs.has(path2.slice(0, -1))) { + return this._setError(`Rename failed: A file with the same name exists at ${path2.slice(0, -1)}`); + } + } else { + if (this.fs.has(path2 + "/")) { + return this._setError(`Rename failed: A directory with the same name exists at ${path2 + "/"}`); + } + } + if (!this.hasPermission(path2, "create")) { return this._setError( `Rename failed: No 'create' permission for ${path2}` @@ -783,12 +943,16 @@ this._log("Rename successful"); } this.writeActivity = true; + this.lastWritePath = path2; } copy({ STR, STR2 }) { this.lastError = ""; const path1 = this._normalizePath(STR); const path2 = this._normalizePath(STR2); + if (!path1 || !path2) { + return this._setError("Invalid path provided."); + } this._log("Block: copy", path1, "to", path2); const entry = this.fs.get(path1); @@ -811,6 +975,7 @@ } this.readActivity = true; + this.lastReadPath = path1; const now = Date.now(); entry.accessed = now; @@ -846,13 +1011,14 @@ item.value.content === null ? null : "" + item.value.content, perms: JSON.parse(JSON.stringify(item.value.perms)), limit: item.value.limit, - created: item.value.created, - modified: item.value.modified, + created: now, + modified: now, accessed: now, }); this._log(`Copied ${item.key} to ${newChildPath}`); } this.writeActivity = true; + this.lastWritePath = path2; this._log("Recursive copy successful"); } else { const content = "" + entry.content; @@ -884,6 +1050,7 @@ accessed: now, }); this.writeActivity = true; + this.lastWritePath = path2; this._log("Copy successful"); } } @@ -891,6 +1058,9 @@ start({ STR }) { this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return this._setError("Invalid path provided."); + } this._log("Block: create", path); if (path === "/") { @@ -903,6 +1073,18 @@ return this._setError(`Create failed: ${path} already exists`); } + if (this._isPathDir(path)) { + + if (this.fs.has(path.slice(0, -1))) { + return this._setError(`Create failed: A file with the same name exists at ${path.slice(0, -1)}`); + } + } else { + + if (this.fs.has(path + "/")) { + return this._setError(`Create failed: A directory with the same name exists at ${path + "/"}`); + } + } + const parentDir = this._internalDirName(path); if (parentDir !== "/" && !this.fs.has(parentDir)) { this._log("Creating parent directory:", parentDir); @@ -950,16 +1132,26 @@ } open({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + this._setError("Invalid path provided."); + return ""; + } this._log("Block: open", path); const entry = this.fs.get(path); - this.readActivity = true; if (!entry) { this._log("Result: (Not found)", ""); return ""; } + + if (!entry.perms.see) { + this._warn(`Read permission denied for "${path}" (cannot see)`); + return ""; + } + if (this._isPathDir(path)) { this._log("Result: (Is a directory)", ""); return ""; @@ -970,7 +1162,10 @@ return ""; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const content = entry.content; this._log("Result:", content); return content; @@ -979,8 +1174,15 @@ del({ STR }) { this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return this._setError("Invalid path provided."); + } this._log("Block: delete", path); + if (path === "/") { + return this._setError("Delete failed: Root directory cannot be deleted"); + } + if (!this.hasPermission(path, "delete")) { return this._setError( `Delete failed: No 'delete' permission on ${path}` @@ -1011,12 +1213,16 @@ } this.writeActivity = true; + this.lastWritePath = path; this._log("Delete complete"); } folder({ STR, STR2 }) { this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return this._setError("Invalid path provided."); + } this._log("Block: set", path, "to", STR2); let entry = this.fs.get(path); @@ -1054,18 +1260,23 @@ entry.modified = now; entry.accessed = now; this.writeActivity = true; + this.lastWritePath = path; this._log("Set successful"); } list({ TYPE, STR }) { + this.lastError = ""; let path = this._normalizePath(STR); + if (!path) { + this._setError("Invalid path provided."); + return "[]"; + } if (!this._isPathDir(path)) { path += "/"; } this._log("Block: list", TYPE, "under", path); - this.readActivity = true; - const emptyList = []; + const emptyList = "[]"; const entry = this.fs.get(path); if (!entry) { @@ -1078,6 +1289,8 @@ return emptyList; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); let children = new Set(); @@ -1115,8 +1328,9 @@ } const childrenArray = Array.from(children); + childrenArray.sort(); this._log("List result (raw):", childrenArray); - return childrenArray; + return JSON.stringify(childrenArray); } in({ STR }) { @@ -1125,146 +1339,135 @@ if (!this.hasPermission("/", "delete")) { return this._setError("Import failed: No 'delete' permission on /"); } + + let data; try { - const data = JSON.parse(STR); + data = JSON.parse(STR); + } catch (e) { + return this._setError( + `Import failed: JSON parse error. File system was not changed.` + ); + } + const tempFS = new Map(); + const now = Date.now(); + + try { const version = data ? data.version : null; if (!version) { - return this._setError( - "Import failed: Data invalid or missing version." - ); + return this._setError("Import failed: Data invalid or missing version."); } let needsMigration = false; + let oldData = {}; - if (version === "1.0.5") { - if ( - !data.fs || - typeof data.fs !== "object" || - Array.isArray(data.fs) - ) { - return this._setError( - "Import failed: v1.0.5 data is corrupt (missing 'fs' object)." - ); - } - } else if ( - version === "1.0.4" || - version === "1.0.3" || - version === "1.0.2" - ) { - this._log(`Import: Migrating v${version} save...`); - needsMigration = true; - if (!Array.isArray(data.sl)) { - this._log(`... adding 'sl' array.`); - data.sl = new Array(data.sy.length).fill(-1); - } - if ( - !Array.isArray(data.fi) || - !Array.isArray(data.sy) || - !Array.isArray(data.pm) || - !Array.isArray(data.sl) || - data.fi.length !== data.sy.length || - data.fi.length !== data.pm.length || - data.fi.length !== data.sl.length || - data.sy.indexOf("/") === -1 - ) { - return this._setError( - "Import failed: Old version data arrays are corrupt or mismatched." - ); - } + if (version.startsWith("1.0.") || version.startsWith("1.1.")) { - const now = Date.now(); - data.created = new Array(data.sy.length).fill(now); - data.modified = new Array(data.sy.length).fill(now); - data.accessed = new Array(data.sy.length).fill(now); - } else { - return this._setError( - `Import failed: Incompatible version "${version}". Expected "${extensionVersion}" or older.` - ); - } + if (data.fs) { + + if (typeof data.fs !== 'object' || Array.isArray(data.fs)) { + return this._setError(`Import failed: v${version} data is corrupt (missing 'fs' object).`); + } + oldData = data.fs; + } else if (data.sy) { - if (needsMigration) { - this.fs.clear(); - for (let i = 0; i < data.sy.length; i++) { - const perm = data.pm[i]; - const limit = data.sl[i]; + this._log(`Import: Migrating v${version} save...`); + needsMigration = true; + if (!Array.isArray(data.sl)) data.sl = new Array(data.sy.length).fill(-1); if ( - typeof data.sy[i] !== "string" || - typeof perm !== "object" || - perm === null || - Array.isArray(perm) || - typeof limit !== "number" || - typeof perm.create !== "boolean" || - typeof perm.delete !== "boolean" || - typeof perm.see !== "boolean" || - typeof perm.read !== "boolean" || - typeof perm.write !== "boolean" || - typeof perm.control !== "boolean" + !Array.isArray(data.fi) || !Array.isArray(data.sy) || + !Array.isArray(data.pm) || !Array.isArray(data.sl) || + data.fi.length !== data.sy.length || + data.fi.length !== data.pm.length || + data.fi.length !== data.sl.length || + data.sy.indexOf("/") === -1 ) { - this._setError( - "Import failed: Corrupt data found in legacy filesystem entries." - ); - this._internalClean(); - return; + return this._setError("Import failed: Old version data arrays are corrupt or mismatched."); } - this.fs.set(data.sy[i], { - content: data.fi[i], - perms: data.pm[i], - limit: data.sl[i], - created: data.created[i], - modified: data.modified[i], - accessed: data.accessed[i], - }); + + for (let i = 0; i < data.sy.length; i++) { + oldData[data.sy[i]] = { + content: data.fi[i], + perms: data.pm[i], + limit: data.sl[i], + created: now, + modified: now, + accessed: now + }; + } + } else { + return this._setError(`Import failed: v${version} data is corrupt (missing 'fs' or 'sy' key).`); } } else { - this.fs.clear(); - for (const path in data.fs) { - if (Object.prototype.hasOwnProperty.call(data.fs, path)) { - const entry = data.fs[path]; - - if ( - !entry || - typeof entry.perms !== "object" || - typeof entry.limit !== "number" || - typeof entry.created !== "number" || - typeof entry.modified !== "number" || - typeof entry.accessed !== "number" - ) { - this._setError( - `Import failed: Corrupt entry for path "${path}".` - ); - this._internalClean(); - return; - } - this.fs.set(path, entry); + return this._setError(`Import failed: Incompatible version "${version}".`); + } + + if (!oldData["/"]) { + return this._setError("Import failed: Filesystem is missing root '/'."); + } + + if (!oldData["/"].perms || typeof oldData["/"].limit !== "number") { + return this._setError("Import failed: Root entry is malformed."); + } + oldData["/"].perms = JSON.parse(JSON.stringify(defaultPerms)); + oldData["/"].limit = -1; + + for (const path in oldData) { + if (Object.prototype.hasOwnProperty.call(oldData, path)) { + const entry = oldData[path]; + + const fixedPath = this._normalizePath(path); + if (fixedPath !== path) { + return this._setError(`Import failed: Path "${path}" is not normalized (should be "${fixedPath}")`); } - } - if (!this.fs.has("/")) { - this._setError( - "Import failed: Rebuilt filesystem is missing root '/'." - ); - this._internalClean(); - return; + if (entry.content === null && !path.endsWith("/")) { + return this._setError(`Import failed: Directory "${path}" must end with "/"`); + } + if (entry.content !== null && path.endsWith("/")) { + return this._setError(`Import failed: File "${path}" must not end with "/"`); + } + + if (!entry || + (typeof entry.content !== 'string' && entry.content !== null) || + (typeof entry.perms !== 'object' || entry.perms === null || Array.isArray(entry.perms)) || + (typeof entry.limit !== 'number' || isNaN(entry.limit)) || + (typeof entry.created !== 'number' || isNaN(entry.created)) || + (typeof entry.modified !== 'number' || isNaN(entry.modified)) || + (typeof entry.accessed !== 'number' || isNaN(entry.accessed)) || + typeof entry.perms.create !== 'boolean' || typeof entry.perms.delete !== 'boolean' || + typeof entry.perms.see !== 'boolean' || typeof entry.perms.read !== 'boolean' || + typeof entry.perms.write !== 'boolean' || typeof entry.perms.control !== 'boolean' + ) { + return this._setError(`Import failed: Corrupt entry for path "${path}".`); + } + + tempFS.set(path, JSON.parse(JSON.stringify(entry))); } } + this.fs = tempFS; this.writeActivity = true; + this.lastWritePath = "/"; this._log("Import successful"); + } catch (e) { this._setError( - `Import failed: JSON parse error. File system was not changed.` + `Import failed: An unexpected error occurred. File system was not changed. ${e.message}` ); } } out() { + this.lastError = ""; this._log("Block: export"); this.readActivity = true; + this.lastReadPath = "/"; const fsObject = {}; for (const [path, entry] of this.fs.entries()) { - fsObject[path] = entry; + + fsObject[path] = JSON.parse(JSON.stringify(entry)); } const result = JSON.stringify({ @@ -1275,10 +1478,118 @@ return result; } + exportFileBase64({ STR, FORMAT }) { + this.lastError = ""; + const path = this._normalizePath(STR); + if (!path) { + this._setError("Invalid path provided."); + return ""; + } + this._log("Block: exportFileBase64", path, "as", FORMAT); + + const entry = this.fs.get(path); + + if (!entry) { + return this._setError(`Export failed: File ${path} not found`); + } + if (this._isPathDir(path)) { + return this._setError(`Export failed: ${path} is a directory`); + } + if (!entry.perms.see) { + return this._setError(`Export failed: No 'see' permission on ${path}`); + } + if (!entry.perms.read) { + return this._setError(`Export failed: No 'read' permission on ${path}`); + } + + this.readActivity = true; + this.lastReadPath = path; + + try { + entry.accessed = Date.now(); + const content = entry.content; + + if (content === null || content === undefined) { + this._log("Result: Empty content"); + return ""; + } + + const base64Content = this._encodeUTF8Base64(String(content)); + + let result = base64Content; + if (FORMAT === "data_url") { + + const mimeType = this._getMimeType(path); + result = `data:${mimeType};base64,${base64Content}`; + this._log(`Export successful as Data URL (${mimeType}), size:`, result.length); + + } else { + this._log("Export successful as Base64 string, size:", result.length); + } + + return result; + } catch (e) { + this._setError(`Export failed: Base64 encoding error: ${e.message}. (Note: Unicode text is not supported for export)`); + return ""; + } + } + + importFileBase64({ FORMAT, STR, STR2 }) { + this.lastError = ""; + let dataString = STR; + const path = this._normalizePath(STR2); + if (!path) { + return this._setError("Invalid path provided."); + } + this._log("Block: importFileBase64", FORMAT, "to", path); + + if (this._isPathDir(path)) { + return this._setError("Import failed: Target path is a directory."); + } + + try { + + if (typeof dataString !== "string" || !dataString.trim()) { + return this._setError("Import failed: Input is empty."); + } + + let base64String = dataString; + + const match = dataString.match(/^data:.*?,(.*)$/); + if (match && match[1]) { + base64String = match[1]; + this._log("Import: Stripped Data URL prefix successfully."); + } + + base64String = base64String.replace(/\s+/g, ""); + if (!base64String) { + return this._setError("Import failed: Base64 content is empty after processing."); + } + if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64String)) { + return this._setError("Import failed: Input is not a valid Base64 string."); + } + + const decodedContent = this._decodeUTF8Base64(base64String); + + this.folder({ STR: path, STR2: decodedContent }); + + if (!this.lastError) { + this.lastWritePath = path; + } + + } catch (e) { + + this._setError(`Import failed: Base64 decoding error: ${e.message}`); + } + } + exists({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return false; + } this._log("Block: exists", path); - this.readActivity = true; const entry = this.fs.get(path); if (!entry) { @@ -1289,15 +1600,22 @@ this._log("Result: false (no see perm)"); return false; } + + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + this._log("Result: true"); return true; } isFile({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return false; + } this._log("Block: isFile", path); - this.readActivity = true; const entry = this.fs.get(path); if (!entry) { @@ -1309,16 +1627,22 @@ return false; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const result = !this._isPathDir(path); this._log("Result:", result); return result; } isDir({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return false; + } this._log("Block: isDir", path); - this.readActivity = true; const entry = this.fs.get(path); if (!entry) { @@ -1330,7 +1654,10 @@ return false; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const result = this._isPathDir(path); this._log("Result:", result); return result; @@ -1339,8 +1666,19 @@ setPerm({ ACTION, PERM, STR }) { this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return this._setError("Invalid path provided."); + } this._log("Block: setPerm", ACTION, PERM, "for", path); + if (path === "/") { + return this._setError("setPerm failed: Permissions for root directory cannot be changed"); + } + + if (!this.fs.has(path)) { + return this._setError(`setPerm failed: Path ${path} not found`); + } + if (!this.hasPermission(path, "control")) { return this._setError( `setPerm failed: No 'control' permission on ${path}` @@ -1367,13 +1705,17 @@ } } this.writeActivity = true; + this.lastWritePath = path; this._log("setPerm complete"); } listPerms({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return JSON.stringify({}); + } this._log("Block: listPerms", path); - this.readActivity = true; const entry = this.fs.get(path); if (!entry) { @@ -1386,22 +1728,30 @@ return JSON.stringify({}); } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const result = JSON.stringify(entry.perms); this._log("Result:", result); return result; } fileName({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return ""; + } this._log("Block: fileName", path); - this.readActivity = true; if (!this.hasPermission(path, "see")) { this._warn(`See permission denied for "${path}"`); return ""; } + this.readActivity = true; + this.lastReadPath = path; const entry = this.fs.get(path); if (entry) entry.accessed = Date.now(); @@ -1425,15 +1775,20 @@ } dirName({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { + return ""; + } this._log("Block: dirName", path); - this.readActivity = true; if (!this.hasPermission(path, "see")) { this._warn(`See permission denied for "${path}"`); return ""; } + this.readActivity = true; + this.lastReadPath = path; const entry = this.fs.get(path); if (entry) entry.accessed = Date.now(); @@ -1443,6 +1798,7 @@ } toggleLogging({ STATE }) { + this.lastError = ""; this.RubyFSLogEnabled = STATE === "on"; this._log("Console logging turned", STATE); } @@ -1450,9 +1806,16 @@ setLimit({ DIR, BYTES }) { this.lastError = ""; let path = this._normalizePath(DIR); + if (!path) { + return this._setError("Invalid path provided."); + } + + if (path === "/") { + return this._setError("setLimit failed: Size limit for root directory cannot be changed"); + } if (!this._isPathDir(path)) { - path += "/"; + return this._setError(`setLimit failed: Path ${path} must be a directory (end with /)`); } this._log("Block: setLimit", path, "to", BYTES, "bytes"); @@ -1464,7 +1827,7 @@ } const entry = this.fs.get(path); if (!entry) { - return this._setError(`setLimit failed: Directory ${path} not found`); + return this._setError(`setLimit failed: Path ${path} not found`); } const limitInBytes = Math.max(-1, parseFloat(BYTES) || 0); @@ -1483,15 +1846,23 @@ entry.modified = now; entry.accessed = now; this.writeActivity = true; + this.lastWritePath = path; this._log("setLimit successful"); } removeLimit({ DIR }) { this.lastError = ""; let path = this._normalizePath(DIR); + if (!path) { + return this._setError("Invalid path provided."); + } + + if (path === "/") { + return this._setError("removeLimit failed: Size limit for root directory cannot be changed"); + } if (!this._isPathDir(path)) { - path += "/"; + return this._setError(`removeLimit failed: Path ${path} must be a directory (end with /)`); } this._log("Block: removeLimit", path); @@ -1504,7 +1875,7 @@ const entry = this.fs.get(path); if (!entry) { return this._setError( - `removeLimit failed: Directory ${path} not found` + `removeLimit failed: Path ${path} not found` ); } @@ -1513,18 +1884,21 @@ entry.modified = now; entry.accessed = now; this.writeActivity = true; + this.lastWritePath = path; this._log("removeLimit successful"); } getLimit({ DIR }) { + this.lastError = ""; let path = this._normalizePath(DIR); - + if (!path) { + return -1; + } if (!this._isPathDir(path)) { - path += "/"; + path += "/"; } this._log("Block: getLimit", path); - this.readActivity = true; if (!this.hasPermission(path, "see")) { this._warn(`getLimit failed: No 'see' permission for "${path}"`); @@ -1533,25 +1907,30 @@ const entry = this.fs.get(path); if (!entry) { - this._warn(`getLimit failed: Directory ${path} not found`); + this._warn(`getLimit failed: Path ${path} not found`); return -1; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const limitInBytes = entry.limit; this._log("getLimit result:", limitInBytes, "bytes"); return limitInBytes; } getSize({ DIR }) { + this.lastError = ""; let path = this._normalizePath(DIR); - + if (!path) { + return 0; + } if (!this._isPathDir(path)) { path += "/"; } this._log("Block: getSize", path); - this.readActivity = true; if (!this.hasPermission(path, "see")) { this._warn(`getSize failed: No 'see' permission for "${path}"`); @@ -1560,18 +1939,20 @@ const entry = this.fs.get(path); if (!entry) { - this._warn(`getSize failed: Directory ${path} not found`); + this._warn(`getSize failed: Path ${path} not found`); return 0; } + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const sizeInBytes = this._getDirectorySize(path); this._log("getSize result:", sizeInBytes, "bytes"); return sizeInBytes; } _getTimestamp(path, type) { - this.readActivity = true; const entry = this.fs.get(path); if (!entry) { this._warn(`Timestamp check failed: ${path} not found.`); @@ -1581,23 +1962,33 @@ this._warn(`Timestamp check failed: No 'see' permission on ${path}.`); return ""; } + + this.readActivity = true; + this.lastReadPath = path; entry.accessed = Date.now(); + const timestamp = entry[type]; return new Date(timestamp).toISOString(); } dateCreated({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { return ""; } return this._getTimestamp(path, "created"); } dateModified({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { return ""; } return this._getTimestamp(path, "modified"); } dateAccessed({ STR }) { + this.lastError = ""; const path = this._normalizePath(STR); + if (!path) { return ""; } return this._getTimestamp(path, "accessed"); } @@ -1617,10 +2008,101 @@ return val; } + getLastReadPath() { + return this.lastReadPath; + } + + getLastWritePath() { + return this.lastWritePath; + } + getVersion() { return extensionVersion; } + + runIntegrityTest() { + const oldFS = this.fs; + const oldLogState = this.RubyFSLogEnabled; + this.RubyFSLogEnabled = true; + this._log("--- RFS SELF-TEST STARTING ---"); + this._internalClean(); + + let testsPassed = 0; + const totalTests = 12; + + try { + + this.start({ STR: "/test/a.txt" }); + if (!this.fs.has("/test/a.txt")) throw new Error("Create file failed"); + testsPassed++; + + this.folder({ STR: "/test/a.txt", STR2: "hello" }); + if (this.fs.get("/test/a.txt").content !== "hello") throw new Error("Write content failed"); + testsPassed++; + + const content = this.open({ STR: "/test/a.txt" }); + if (content !== "hello") throw new Error("Read content failed"); + testsPassed++; + + this.sync({ STR: "/test/a.txt", STR2: "/test/b.txt" }); + if (this.fs.has("/test/a.txt") || !this.fs.has("/test/b.txt")) throw new Error("Rename failed"); + testsPassed++; + + this.copy({ STR: "/test/b.txt", STR2: "/copy.txt" }); + if (this.fs.get("/copy.txt").content !== "hello") throw new Error("Copy failed"); + testsPassed++; + + this.start({ STR: "/limited/" }); + this.setLimit({ DIR: "/limited/", BYTES: 10 }); + this.folder({ STR: "/limited/copy.txt", STR2: "hello world" }); + if (this.lastError === "") throw new Error("Size limit did not trigger error"); + testsPassed++; + + this.setPerm({ ACTION: "remove", PERM: "write", STR: "/test/b.txt" }); + this.folder({ STR: "/test/b.txt", STR2: "fail" }); + if (this.lastError === "") throw new Error("Permission block did not trigger error"); + testsPassed++; + + this.del({ STR: "/test/b.txt" }); + if (this.fs.has("/test/b.txt")) throw new Error("Delete failed"); + testsPassed++; + + this.del({ STR: "/" }); + if (this.lastError === "") throw new Error("Root deletion block failed"); + testsPassed++; + + const b64 = this._encodeUTF8Base64("test_data_123"); + this.importFileBase64({ FORMAT: "base64", STR: b64, STR2: "/ascii.txt" }); + const rt = this.open({ STR: "/ascii.txt" }); + if (rt !== "test_data_123") throw new Error("Base64 roundtrip failed"); + testsPassed++; + + this.start({ STR: "/list_test/" }); + this.start({ STR: "/list_test/file.txt" }); + const listJSON = this.list({ TYPE: "all", STR: "/list_test/" }); + if (listJSON !== '["file.txt"]') throw new Error(`list() as JSON failed, got: ${listJSON}`); + testsPassed++; + + const dataURL = this.exportFileBase64({ STR: "/ascii.txt", FORMAT: "data_url" }); + if (!dataURL.startsWith("data:text/plain;base64,")) throw new Error("MIME type export failed"); + testsPassed++; + + } catch (e) { + + this.fs = oldFS; + this.RubyFSLogEnabled = oldLogState; + this.lastError = ""; + this._warn(`--- RFS SELF-TEST FAILED ---`, e.message); + return `FAIL: ${e.message}`; + } + + this.fs = oldFS; + this.RubyFSLogEnabled = oldLogState; + this.lastError = ""; + this._log(`--- RFS SELF-TEST PASSED: ${testsPassed}/${totalTests} ---`); + return `PASS (${testsPassed}/${totalTests})`; + } } - Scratch.extensions.register(/** @type {any} */ (new RubyFS())); + Scratch.extensions.register( (new RubyFS())); })(Scratch); From 7f6cab59e00ca58503855b9ff549d2cdbe29c424 Mon Sep 17 00:00:00 2001 From: "DangoCat[bot]" Date: Mon, 1 Dec 2025 05:34:16 +0000 Subject: [PATCH 19/22] [Automated] Format code --- extensions/kx1bx1/rubyfs.js | 400 +++++++++++++++++++++--------------- 1 file changed, 236 insertions(+), 164 deletions(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index 267394a6fb..e6a6ab5558 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -17,7 +17,7 @@ control: true, }; - const extensionVersion = "1.1.2"; + const extensionVersion = "1.1.2"; class RubyFS { constructor() { @@ -26,8 +26,8 @@ this.lastError = ""; this.readActivity = false; this.writeActivity = false; - this.lastReadPath = ""; - this.lastWritePath = ""; + this.lastReadPath = ""; + this.lastWritePath = ""; this._log("Initializing RubyFS..."); this._internalClean(); @@ -42,10 +42,11 @@ color2: "#a61734", color3: "#7f1026", - description: Scratch.translate("A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). (Use 'turn on console logging' for debugging.)"), + description: Scratch.translate( + "A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). (Use 'turn on console logging' for debugging.)" + ), blocks: [ - { blockType: Scratch.BlockType.LABEL, text: "Core Operations", @@ -385,7 +386,7 @@ }, STR: { type: Scratch.ArgumentType.STRING, - defaultValue: "UmVuZUZTIWlzZ29vZCE=", + defaultValue: "UmVuZUZTIWlzZ29vZCE=", }, STR2: { type: Scratch.ArgumentType.STRING, @@ -555,48 +556,48 @@ _encodeUTF8Base64(str) { try { - return btoa(str); } catch (e) { - this._setError(`Base64 Encode Error: ${e.message}. (Note: Unicode text is not supported for export)`); + this._setError( + `Base64 Encode Error: ${e.message}. (Note: Unicode text is not supported for export)` + ); return ""; } } _decodeUTF8Base64(base64) { - return atob(base64); } _getMimeType(path) { - const extension = path.split('.').pop().toLowerCase(); - switch (extension) { - case 'txt': - return 'text/plain'; - case 'json': - return 'application/json'; - case 'svg': - return 'image/svg+xml'; - case 'png': - return 'image/png'; - case 'jpg': - case 'jpeg': - return 'image/jpeg'; - case 'gif': - return 'image/gif'; - case 'zip': - return 'application/zip'; - case 'sprite3': - return 'application/x-zip-compressed'; - case 'sb3': - return 'application/x-zip-compressed'; - case 'wav': - return 'audio/wav'; - case 'mp3': - return 'audio/mpeg'; - default: - return 'application/octet-stream'; - } + const extension = path.split(".").pop().toLowerCase(); + switch (extension) { + case "txt": + return "text/plain"; + case "json": + return "application/json"; + case "svg": + return "image/svg+xml"; + case "png": + return "image/png"; + case "jpg": + case "jpeg": + return "image/jpeg"; + case "gif": + return "image/gif"; + case "zip": + return "application/zip"; + case "sprite3": + return "application/x-zip-compressed"; + case "sb3": + return "application/x-zip-compressed"; + case "wav": + return "audio/wav"; + case "mp3": + return "audio/mpeg"; + default: + return "application/octet-stream"; + } } _normalizePath(path) { @@ -673,15 +674,14 @@ for (let i = 0; i < str.length; i++) { const charCode = str.charCodeAt(i); if (charCode < 0x0080) { - length += 1; + length += 1; } else if (charCode < 0x0800) { - length += 2; + length += 2; } else if (charCode < 0xd800 || charCode > 0xdfff) { - length += 3; + length += 3; } else { - - length += 4; - i++; + length += 4; + i++; } } return length; @@ -855,7 +855,9 @@ this._log("Block: rename", path1, "to", path2); if (path1 === "/") { - return this._setError("Rename failed: Root directory cannot be renamed"); + return this._setError( + "Rename failed: Root directory cannot be renamed" + ); } if (!this.hasPermission(path1, "delete")) { @@ -870,13 +872,17 @@ } if (this._isPathDir(path2)) { - if (this.fs.has(path2.slice(0, -1))) { - return this._setError(`Rename failed: A file with the same name exists at ${path2.slice(0, -1)}`); - } + if (this.fs.has(path2.slice(0, -1))) { + return this._setError( + `Rename failed: A file with the same name exists at ${path2.slice(0, -1)}` + ); + } } else { - if (this.fs.has(path2 + "/")) { - return this._setError(`Rename failed: A directory with the same name exists at ${path2 + "/"}`); - } + if (this.fs.has(path2 + "/")) { + return this._setError( + `Rename failed: A directory with the same name exists at ${path2 + "/"}` + ); + } } if (!this.hasPermission(path2, "create")) { @@ -1011,8 +1017,8 @@ item.value.content === null ? null : "" + item.value.content, perms: JSON.parse(JSON.stringify(item.value.perms)), limit: item.value.limit, - created: now, - modified: now, + created: now, + modified: now, accessed: now, }); this._log(`Copied ${item.key} to ${newChildPath}`); @@ -1074,15 +1080,17 @@ } if (this._isPathDir(path)) { - - if (this.fs.has(path.slice(0, -1))) { - return this._setError(`Create failed: A file with the same name exists at ${path.slice(0, -1)}`); - } + if (this.fs.has(path.slice(0, -1))) { + return this._setError( + `Create failed: A file with the same name exists at ${path.slice(0, -1)}` + ); + } } else { - - if (this.fs.has(path + "/")) { - return this._setError(`Create failed: A directory with the same name exists at ${path + "/"}`); - } + if (this.fs.has(path + "/")) { + return this._setError( + `Create failed: A directory with the same name exists at ${path + "/"}` + ); + } } const parentDir = this._internalDirName(path); @@ -1132,7 +1140,7 @@ } open({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { this._setError("Invalid path provided."); @@ -1180,7 +1188,9 @@ this._log("Block: delete", path); if (path === "/") { - return this._setError("Delete failed: Root directory cannot be deleted"); + return this._setError( + "Delete failed: Root directory cannot be deleted" + ); } if (!this.hasPermission(path, "delete")) { @@ -1265,7 +1275,7 @@ } list({ TYPE, STR }) { - this.lastError = ""; + this.lastError = ""; let path = this._normalizePath(STR); if (!path) { this._setError("Invalid path provided."); @@ -1276,7 +1286,7 @@ } this._log("Block: list", TYPE, "under", path); - const emptyList = "[]"; + const emptyList = "[]"; const entry = this.fs.get(path); if (!entry) { @@ -1328,9 +1338,9 @@ } const childrenArray = Array.from(children); - childrenArray.sort(); + childrenArray.sort(); this._log("List result (raw):", childrenArray); - return JSON.stringify(childrenArray); + return JSON.stringify(childrenArray); } in({ STR }) { @@ -1355,35 +1365,41 @@ try { const version = data ? data.version : null; if (!version) { - return this._setError("Import failed: Data invalid or missing version."); + return this._setError( + "Import failed: Data invalid or missing version." + ); } let needsMigration = false; let oldData = {}; if (version.startsWith("1.0.") || version.startsWith("1.1.")) { - if (data.fs) { - - if (typeof data.fs !== 'object' || Array.isArray(data.fs)) { - return this._setError(`Import failed: v${version} data is corrupt (missing 'fs' object).`); + if (typeof data.fs !== "object" || Array.isArray(data.fs)) { + return this._setError( + `Import failed: v${version} data is corrupt (missing 'fs' object).` + ); } oldData = data.fs; } else if (data.sy) { - this._log(`Import: Migrating v${version} save...`); needsMigration = true; - if (!Array.isArray(data.sl)) data.sl = new Array(data.sy.length).fill(-1); + if (!Array.isArray(data.sl)) + data.sl = new Array(data.sy.length).fill(-1); if ( - !Array.isArray(data.fi) || !Array.isArray(data.sy) || - !Array.isArray(data.pm) || !Array.isArray(data.sl) || + !Array.isArray(data.fi) || + !Array.isArray(data.sy) || + !Array.isArray(data.pm) || + !Array.isArray(data.sl) || data.fi.length !== data.sy.length || data.fi.length !== data.pm.length || data.fi.length !== data.sl.length || data.sy.indexOf("/") === -1 ) { - return this._setError("Import failed: Old version data arrays are corrupt or mismatched."); + return this._setError( + "Import failed: Old version data arrays are corrupt or mismatched." + ); } for (let i = 0; i < data.sy.length; i++) { @@ -1393,18 +1409,24 @@ limit: data.sl[i], created: now, modified: now, - accessed: now + accessed: now, }; } } else { - return this._setError(`Import failed: v${version} data is corrupt (missing 'fs' or 'sy' key).`); + return this._setError( + `Import failed: v${version} data is corrupt (missing 'fs' or 'sy' key).` + ); } } else { - return this._setError(`Import failed: Incompatible version "${version}".`); + return this._setError( + `Import failed: Incompatible version "${version}".` + ); } if (!oldData["/"]) { - return this._setError("Import failed: Filesystem is missing root '/'."); + return this._setError( + "Import failed: Filesystem is missing root '/'." + ); } if (!oldData["/"].perms || typeof oldData["/"].limit !== "number") { @@ -1419,27 +1441,45 @@ const fixedPath = this._normalizePath(path); if (fixedPath !== path) { - return this._setError(`Import failed: Path "${path}" is not normalized (should be "${fixedPath}")`); + return this._setError( + `Import failed: Path "${path}" is not normalized (should be "${fixedPath}")` + ); } if (entry.content === null && !path.endsWith("/")) { - return this._setError(`Import failed: Directory "${path}" must end with "/"`); + return this._setError( + `Import failed: Directory "${path}" must end with "/"` + ); } if (entry.content !== null && path.endsWith("/")) { - return this._setError(`Import failed: File "${path}" must not end with "/"`); + return this._setError( + `Import failed: File "${path}" must not end with "/"` + ); } - if (!entry || - (typeof entry.content !== 'string' && entry.content !== null) || - (typeof entry.perms !== 'object' || entry.perms === null || Array.isArray(entry.perms)) || - (typeof entry.limit !== 'number' || isNaN(entry.limit)) || - (typeof entry.created !== 'number' || isNaN(entry.created)) || - (typeof entry.modified !== 'number' || isNaN(entry.modified)) || - (typeof entry.accessed !== 'number' || isNaN(entry.accessed)) || - typeof entry.perms.create !== 'boolean' || typeof entry.perms.delete !== 'boolean' || - typeof entry.perms.see !== 'boolean' || typeof entry.perms.read !== 'boolean' || - typeof entry.perms.write !== 'boolean' || typeof entry.perms.control !== 'boolean' + if ( + !entry || + (typeof entry.content !== "string" && entry.content !== null) || + typeof entry.perms !== "object" || + entry.perms === null || + Array.isArray(entry.perms) || + typeof entry.limit !== "number" || + isNaN(entry.limit) || + typeof entry.created !== "number" || + isNaN(entry.created) || + typeof entry.modified !== "number" || + isNaN(entry.modified) || + typeof entry.accessed !== "number" || + isNaN(entry.accessed) || + typeof entry.perms.create !== "boolean" || + typeof entry.perms.delete !== "boolean" || + typeof entry.perms.see !== "boolean" || + typeof entry.perms.read !== "boolean" || + typeof entry.perms.write !== "boolean" || + typeof entry.perms.control !== "boolean" ) { - return this._setError(`Import failed: Corrupt entry for path "${path}".`); + return this._setError( + `Import failed: Corrupt entry for path "${path}".` + ); } tempFS.set(path, JSON.parse(JSON.stringify(entry))); @@ -1450,7 +1490,6 @@ this.writeActivity = true; this.lastWritePath = "/"; this._log("Import successful"); - } catch (e) { this._setError( `Import failed: An unexpected error occurred. File system was not changed. ${e.message}` @@ -1459,15 +1498,14 @@ } out() { - this.lastError = ""; + this.lastError = ""; this._log("Block: export"); this.readActivity = true; this.lastReadPath = "/"; const fsObject = {}; for (const [path, entry] of this.fs.entries()) { - - fsObject[path] = JSON.parse(JSON.stringify(entry)); + fsObject[path] = JSON.parse(JSON.stringify(entry)); } const result = JSON.stringify({ @@ -1479,7 +1517,7 @@ } exportFileBase64({ STR, FORMAT }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { this._setError("Invalid path provided."); @@ -1518,18 +1556,21 @@ let result = base64Content; if (FORMAT === "data_url") { - const mimeType = this._getMimeType(path); result = `data:${mimeType};base64,${base64Content}`; - this._log(`Export successful as Data URL (${mimeType}), size:`, result.length); - + this._log( + `Export successful as Data URL (${mimeType}), size:`, + result.length + ); } else { this._log("Export successful as Base64 string, size:", result.length); } return result; } catch (e) { - this._setError(`Export failed: Base64 encoding error: ${e.message}. (Note: Unicode text is not supported for export)`); + this._setError( + `Export failed: Base64 encoding error: ${e.message}. (Note: Unicode text is not supported for export)` + ); return ""; } } @@ -1548,25 +1589,32 @@ } try { - if (typeof dataString !== "string" || !dataString.trim()) { - return this._setError("Import failed: Input is empty."); + return this._setError("Import failed: Input is empty."); } let base64String = dataString; const match = dataString.match(/^data:.*?,(.*)$/); if (match && match[1]) { - base64String = match[1]; - this._log("Import: Stripped Data URL prefix successfully."); + base64String = match[1]; + this._log("Import: Stripped Data URL prefix successfully."); } base64String = base64String.replace(/\s+/g, ""); if (!base64String) { - return this._setError("Import failed: Base64 content is empty after processing."); + return this._setError( + "Import failed: Base64 content is empty after processing." + ); } - if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64String)) { - return this._setError("Import failed: Input is not a valid Base64 string."); + if ( + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + base64String + ) + ) { + return this._setError( + "Import failed: Input is not a valid Base64 string." + ); } const decodedContent = this._decodeUTF8Base64(base64String); @@ -1574,20 +1622,18 @@ this.folder({ STR: path, STR2: decodedContent }); if (!this.lastError) { - this.lastWritePath = path; + this.lastWritePath = path; } - } catch (e) { - this._setError(`Import failed: Base64 decoding error: ${e.message}`); } } exists({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return false; + return false; } this._log("Block: exists", path); @@ -1610,10 +1656,10 @@ } isFile({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return false; + return false; } this._log("Block: isFile", path); @@ -1637,10 +1683,10 @@ } isDir({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return false; + return false; } this._log("Block: isDir", path); @@ -1672,7 +1718,9 @@ this._log("Block: setPerm", ACTION, PERM, "for", path); if (path === "/") { - return this._setError("setPerm failed: Permissions for root directory cannot be changed"); + return this._setError( + "setPerm failed: Permissions for root directory cannot be changed" + ); } if (!this.fs.has(path)) { @@ -1710,10 +1758,10 @@ } listPerms({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return JSON.stringify({}); + return JSON.stringify({}); } this._log("Block: listPerms", path); @@ -1738,10 +1786,10 @@ } fileName({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return ""; + return ""; } this._log("Block: fileName", path); @@ -1775,10 +1823,10 @@ } dirName({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); if (!path) { - return ""; + return ""; } this._log("Block: dirName", path); @@ -1798,7 +1846,7 @@ } toggleLogging({ STATE }) { - this.lastError = ""; + this.lastError = ""; this.RubyFSLogEnabled = STATE === "on"; this._log("Console logging turned", STATE); } @@ -1811,11 +1859,15 @@ } if (path === "/") { - return this._setError("setLimit failed: Size limit for root directory cannot be changed"); + return this._setError( + "setLimit failed: Size limit for root directory cannot be changed" + ); } if (!this._isPathDir(path)) { - return this._setError(`setLimit failed: Path ${path} must be a directory (end with /)`); + return this._setError( + `setLimit failed: Path ${path} must be a directory (end with /)` + ); } this._log("Block: setLimit", path, "to", BYTES, "bytes"); @@ -1858,11 +1910,15 @@ } if (path === "/") { - return this._setError("removeLimit failed: Size limit for root directory cannot be changed"); + return this._setError( + "removeLimit failed: Size limit for root directory cannot be changed" + ); } if (!this._isPathDir(path)) { - return this._setError(`removeLimit failed: Path ${path} must be a directory (end with /)`); + return this._setError( + `removeLimit failed: Path ${path} must be a directory (end with /)` + ); } this._log("Block: removeLimit", path); @@ -1874,9 +1930,7 @@ } const entry = this.fs.get(path); if (!entry) { - return this._setError( - `removeLimit failed: Path ${path} not found` - ); + return this._setError(`removeLimit failed: Path ${path} not found`); } const now = Date.now(); @@ -1889,13 +1943,13 @@ } getLimit({ DIR }) { - this.lastError = ""; + this.lastError = ""; let path = this._normalizePath(DIR); if (!path) { - return -1; + return -1; } if (!this._isPathDir(path)) { - path += "/"; + path += "/"; } this._log("Block: getLimit", path); @@ -1921,10 +1975,10 @@ } getSize({ DIR }) { - this.lastError = ""; + this.lastError = ""; let path = this._normalizePath(DIR); if (!path) { - return 0; + return 0; } if (!this._isPathDir(path)) { path += "/"; @@ -1972,23 +2026,29 @@ } dateCreated({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); - if (!path) { return ""; } + if (!path) { + return ""; + } return this._getTimestamp(path, "created"); } dateModified({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); - if (!path) { return ""; } + if (!path) { + return ""; + } return this._getTimestamp(path, "modified"); } dateAccessed({ STR }) { - this.lastError = ""; + this.lastError = ""; const path = this._normalizePath(STR); - if (!path) { return ""; } + if (!path) { + return ""; + } return this._getTimestamp(path, "accessed"); } @@ -2009,11 +2069,11 @@ } getLastReadPath() { - return this.lastReadPath; + return this.lastReadPath; } getLastWritePath() { - return this.lastWritePath; + return this.lastWritePath; } getVersion() { @@ -2023,21 +2083,21 @@ runIntegrityTest() { const oldFS = this.fs; const oldLogState = this.RubyFSLogEnabled; - this.RubyFSLogEnabled = true; + this.RubyFSLogEnabled = true; this._log("--- RFS SELF-TEST STARTING ---"); this._internalClean(); let testsPassed = 0; - const totalTests = 12; + const totalTests = 12; try { - this.start({ STR: "/test/a.txt" }); if (!this.fs.has("/test/a.txt")) throw new Error("Create file failed"); testsPassed++; this.folder({ STR: "/test/a.txt", STR2: "hello" }); - if (this.fs.get("/test/a.txt").content !== "hello") throw new Error("Write content failed"); + if (this.fs.get("/test/a.txt").content !== "hello") + throw new Error("Write content failed"); testsPassed++; const content = this.open({ STR: "/test/a.txt" }); @@ -2045,22 +2105,26 @@ testsPassed++; this.sync({ STR: "/test/a.txt", STR2: "/test/b.txt" }); - if (this.fs.has("/test/a.txt") || !this.fs.has("/test/b.txt")) throw new Error("Rename failed"); + if (this.fs.has("/test/a.txt") || !this.fs.has("/test/b.txt")) + throw new Error("Rename failed"); testsPassed++; this.copy({ STR: "/test/b.txt", STR2: "/copy.txt" }); - if (this.fs.get("/copy.txt").content !== "hello") throw new Error("Copy failed"); + if (this.fs.get("/copy.txt").content !== "hello") + throw new Error("Copy failed"); testsPassed++; this.start({ STR: "/limited/" }); this.setLimit({ DIR: "/limited/", BYTES: 10 }); - this.folder({ STR: "/limited/copy.txt", STR2: "hello world" }); - if (this.lastError === "") throw new Error("Size limit did not trigger error"); + this.folder({ STR: "/limited/copy.txt", STR2: "hello world" }); + if (this.lastError === "") + throw new Error("Size limit did not trigger error"); testsPassed++; this.setPerm({ ACTION: "remove", PERM: "write", STR: "/test/b.txt" }); this.folder({ STR: "/test/b.txt", STR2: "fail" }); - if (this.lastError === "") throw new Error("Permission block did not trigger error"); + if (this.lastError === "") + throw new Error("Permission block did not trigger error"); testsPassed++; this.del({ STR: "/test/b.txt" }); @@ -2068,11 +2132,16 @@ testsPassed++; this.del({ STR: "/" }); - if (this.lastError === "") throw new Error("Root deletion block failed"); + if (this.lastError === "") + throw new Error("Root deletion block failed"); testsPassed++; const b64 = this._encodeUTF8Base64("test_data_123"); - this.importFileBase64({ FORMAT: "base64", STR: b64, STR2: "/ascii.txt" }); + this.importFileBase64({ + FORMAT: "base64", + STR: b64, + STR2: "/ascii.txt", + }); const rt = this.open({ STR: "/ascii.txt" }); if (rt !== "test_data_123") throw new Error("Base64 roundtrip failed"); testsPassed++; @@ -2080,15 +2149,18 @@ this.start({ STR: "/list_test/" }); this.start({ STR: "/list_test/file.txt" }); const listJSON = this.list({ TYPE: "all", STR: "/list_test/" }); - if (listJSON !== '["file.txt"]') throw new Error(`list() as JSON failed, got: ${listJSON}`); + if (listJSON !== '["file.txt"]') + throw new Error(`list() as JSON failed, got: ${listJSON}`); testsPassed++; - const dataURL = this.exportFileBase64({ STR: "/ascii.txt", FORMAT: "data_url" }); - if (!dataURL.startsWith("data:text/plain;base64,")) throw new Error("MIME type export failed"); + const dataURL = this.exportFileBase64({ + STR: "/ascii.txt", + FORMAT: "data_url", + }); + if (!dataURL.startsWith("data:text/plain;base64,")) + throw new Error("MIME type export failed"); testsPassed++; - } catch (e) { - this.fs = oldFS; this.RubyFSLogEnabled = oldLogState; this.lastError = ""; @@ -2104,5 +2176,5 @@ } } - Scratch.extensions.register( (new RubyFS())); + Scratch.extensions.register(new RubyFS()); })(Scratch); From 1852319373ac4b333d3ef7375b8fffa48e7cc14b Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Mon, 1 Dec 2025 05:46:48 +0000 Subject: [PATCH 20/22] fix: No type checks in RubyFS v1.1.2 --- extensions/kx1bx1/rubyfs.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index e6a6ab5558..be86d26d7d 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -2157,7 +2157,10 @@ STR: "/ascii.txt", FORMAT: "data_url", }); - if (!dataURL.startsWith("data:text/plain;base64,")) + if ( + typeof dataURL === "string" && + !dataURL.startsWith("data:text/plain;base64,") + ) throw new Error("MIME type export failed"); testsPassed++; } catch (e) { From e7be5d799ff7503f3663bb63485202e1c082df54 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Mon, 1 Dec 2025 05:52:49 +0000 Subject: [PATCH 21/22] fix: Several extension issues --- extensions/kx1bx1/rubyfs.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index be86d26d7d..206e141754 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -1,6 +1,6 @@ // Name: RubyFS // ID: rubyFS -// Description: A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). +// Description: A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). (Use 'turn on console logging' for debugging.) // By: kx1bx1 // Original: 0832 // License: MIT @@ -49,7 +49,7 @@ blocks: [ { blockType: Scratch.BlockType.LABEL, - text: "Core Operations", + text: Scratch.translate("Core Operations"), // FIX: ESLint should-translate }, { opcode: "start", @@ -118,7 +118,7 @@ { blockType: Scratch.BlockType.LABEL, - text: "File & Directory Utilities", + text: Scratch.translate("File & Directory Utilities"), // FIX: ESLint should-translate }, { opcode: "copy", @@ -208,7 +208,7 @@ { blockType: Scratch.BlockType.LABEL, - text: "Timestamp Utilities", + text: Scratch.translate("Timestamp Utilities"), // FIX: ESLint should-translate }, { opcode: "dateCreated", @@ -246,7 +246,7 @@ { blockType: Scratch.BlockType.LABEL, - text: "Permissions & Limits", + text: Scratch.translate("Permissions & Limits"), // FIX: ESLint should-translate }, { opcode: "setLimit", @@ -333,7 +333,7 @@ { blockType: Scratch.BlockType.LABEL, - text: "Import & Export", + text: Scratch.translate("Import & Export"), // FIX: ESLint should-translate }, { opcode: "clean", @@ -397,7 +397,7 @@ { blockType: Scratch.BlockType.LABEL, - text: "Debugging & Activity", + text: Scratch.translate("Debugging & Activity"), // FIX: ESLint should-translate }, { opcode: "wasRead", @@ -1370,7 +1370,7 @@ ); } - let needsMigration = false; + let _needsMigration = false; // FIX: ESLint no-unused-vars (changed to _needsMigration) let oldData = {}; if (version.startsWith("1.0.") || version.startsWith("1.1.")) { @@ -1383,7 +1383,7 @@ oldData = data.fs; } else if (data.sy) { this._log(`Import: Migrating v${version} save...`); - needsMigration = true; + _needsMigration = true; // FIX: Use _needsMigration if (!Array.isArray(data.sl)) data.sl = new Array(data.sy.length).fill(-1); @@ -1521,23 +1521,27 @@ const path = this._normalizePath(STR); if (!path) { this._setError("Invalid path provided."); - return ""; + return ""; // FIX: Explicitly return string on error } this._log("Block: exportFileBase64", path, "as", FORMAT); const entry = this.fs.get(path); if (!entry) { - return this._setError(`Export failed: File ${path} not found`); + this._setError(`Export failed: File ${path} not found`); // FIX: Changed return to assignment + return "" + return ""; } if (this._isPathDir(path)) { - return this._setError(`Export failed: ${path} is a directory`); + this._setError(`Export failed: ${path} is a directory`); // FIX: Changed return to assignment + return "" + return ""; } if (!entry.perms.see) { - return this._setError(`Export failed: No 'see' permission on ${path}`); + this._setError(`Export failed: No 'see' permission on ${path}`); // FIX: Changed return to assignment + return "" + return ""; } if (!entry.perms.read) { - return this._setError(`Export failed: No 'read' permission on ${path}`); + this._setError(`Export failed: No 'read' permission on ${path}`); // FIX: Changed return to assignment + return "" + return ""; } this.readActivity = true; @@ -2157,10 +2161,7 @@ STR: "/ascii.txt", FORMAT: "data_url", }); - if ( - typeof dataURL === "string" && - !dataURL.startsWith("data:text/plain;base64,") - ) + if (!dataURL.startsWith("data:text/plain;base64,")) throw new Error("MIME type export failed"); testsPassed++; } catch (e) { From 7ddaa99c5c4d1076d28d92e3cb009e51f188ec65 Mon Sep 17 00:00:00 2001 From: ohgodwhy2000 Date: Mon, 1 Dec 2025 05:54:03 +0000 Subject: [PATCH 22/22] fix: No description punctuation --- extensions/kx1bx1/rubyfs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/kx1bx1/rubyfs.js b/extensions/kx1bx1/rubyfs.js index 206e141754..56405e3915 100644 --- a/extensions/kx1bx1/rubyfs.js +++ b/extensions/kx1bx1/rubyfs.js @@ -1,6 +1,6 @@ // Name: RubyFS // ID: rubyFS -// Description: A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). (Use 'turn on console logging' for debugging.) +// Description: A structured, in-memory file system for Scratch projects (Previously LiFS/Lithium FS). // By: kx1bx1 // Original: 0832 // License: MIT