# Lua4Frozen 仕様 Lua4Frozen は、Iceshrimp/FrozenFriendsYume 上で Lua プログラムを投稿・検索・閲覧・実行するためのブラウザ内サンドボックス実行環境です。 この仕様書は、現行実装に存在する Lua4Frozen 本体と関連機能をまとめます。 ## 1. 対象範囲 この文書に含める範囲は次の通りです。 - Lua4Frozen 投稿の判定条件 - Lua4Frozen 一覧・検索 - ノート本文からの Lua4Frozen 導線 - 投稿添付 `.lua` の実行画面 - ブラウザ内テストランナー - Lua 実行環境とサンドボックス - `ffy` API - 添付ファイル、仮想ファイル、Drive 連携 - 仮想キーボードとキーボード配列 JSON - エラー、ログ、セキュリティ制約 ## 2. 画面とルート Lua4Frozen には次のルートがあります。 | ルート | 機能 | | --- | --- | | `/lua4frozen` | Lua4Frozen 投稿一覧・検索 | | `/lua4frozen/:noteId` | 指定ノートの Lua プログラム実行 | | `/lua4frozen/spec` | アプリ内仕様説明ページ | | `/lua4frozen/test` | ブラウザ内テストランナー | ナビゲーションには `Lua4Frozen` 項目があり、トップ画面からテストランナーと仕様ページへ移動できます。 ## 3. Lua4Frozen 投稿 ノートが Lua4Frozen 投稿として扱われる条件は次の通りです。 1. ノートの可視性が `public` または `home` 2. ノートのタグに `lua4frozen` が含まれる 3. 添付ファイルに `.lua` 拡張子のファイルが 1 つ以上ある タグ判定は小文字化されたタグ配列に対して行われます。 本文中の `#Lua4Frozen` はタグ化され、内部的には `lua4frozen` として扱われます。 ### タイトルと説明 Lua4Frozen 一覧と実行画面では、ノート本文を次のように扱います。 - 1 行目: タイトル - 2 行目以降: 説明 - 1 行目が空の場合: `(no title)` 説明は実行画面では MFM として表示されます。 ## 4. ノートからの関連導線 通常のノート表示でも、次の条件を満たすと Lua4Frozen への導線が出ます。 1. ノートにファイルが 1 つ以上ある 2. 可視性が `public` または `home` 3. タグに `lua4frozen` が含まれる 4. `.lua` 拡張子の添付ファイルがある この場合、メディアサービス用リンクのラベルは `Lua4Frozen` になり、遷移先は `/lua4frozen/:noteId` です。 ## 5. 一覧・検索 API Lua4Frozen 一覧は `notes/lua4frozen-search` を使います。 ### エンドポイント `notes/lua4frozen-search` ### 認証 - 通常時: 認証不要 - Private mode 時: 認証が必要 ### パラメータ | 名前 | 型 | 既定値 | 制約 | | --- | --- | --- | --- | | `query` | string | `""` | 前後空白は除去 | | `limit` | integer | `20` | `1` 以上 `50` 以下 | | `offset` | integer | `0` | `0` 以上 | ### 検索条件 検索対象は次の通りです。 - ノート本文の部分一致 - 投稿者の `usernameLower` の部分一致 ### 抽出条件 API は以下の条件でノートを抽出します。 - `visibility` が `public` または `home` - `tags` に `lua4frozen` が含まれる - `fileIds` が空でない - pack 後に `.lua` ファイルを持つものだけ残す 並び順は `createdAt DESC` です。 ## 6. 投稿実行画面 `/lua4frozen/:noteId` は指定ノートを取得し、Lua4Frozen として実行します。 ### 読み込み時の検証 実行画面は次を検証します。 - ノートが存在すること - タグに `lua4frozen` が含まれること - 添付ファイルに `.lua` ファイルが 1 つ以上あること 条件を満たさない場合はエラー表示になります。 ### Lua ファイル選択 `.lua` ファイルが複数添付されている場合、実行画面の `Lua file` セレクトで実行対象を選べます。 選択された `.lua` の内容を取得して実行します。 ### 添付ファイルの事前読み込み 実行画面では、添付ファイルのうち次の条件を満たすものをテキストとして事前読み込みします。 - サイズが `512 KiB` 以下 - 拡張子が `.lua`, `.txt`, `.json`, `.csv` 読み込めない添付ファイルは無視されます。 ## 7. テストランナー `/lua4frozen/test` は投稿なしで Lua4Frozen を実行する開発・確認用画面です。 ### 主な機能 - Lua ソースの編集 - サンプルコード読み込み - 実行・停止 - 実行ログ表示 - Runtime error 表示 - Canvas 表示 - HTML 出力表示 - 仮想ファイルの追加、編集、削除 - 仮想キーボードの利用 - ユーザー定義キーボードの保存、削除 - プログラムからのキーボード配列登録 - `.lua` としてダウンロード - ブラウザ内下書き保存 ### サンプル 現在の実装では次のサンプルがあります。 - `Bouncing ball` - `Keyboard and mouse` - `Files and audio` ### ローカル保存 テストランナーはブラウザの `localStorage` を使います。 | キー | 用途 | | --- | --- | | `lua4frozen:test:source` | テストランナーの下書き Lua ソース | | `lua4frozen:userKeyboards` | ユーザー定義キーボード配列 | ## 8. Lua 実行環境 Lua 実行には `fengari` を使います。 ブラウザ内で Lua state を作成し、安全な標準ライブラリだけを開きます。 ### 開かれる標準ライブラリ - `_G` - `coroutine` - `table` - `string` - `utf8` - `math` ### 削除されるグローバル 安全性のため、次のグローバルは削除されます。 - `io` - `os` - `package` - `require` - `debug` - `dofile` - `loadfile` Lua から外部ネットワークへ直接通信する API は公開されません。 ## 9. 実行ライフサイクル 実行ボタンを押すと、次の順に処理します。 1. 既存の実行を停止 2. Runtime error とログを初期化 3. Drive 読み取り許可状態を `unknown` に戻す 4. Canvas を初期化 5. Lua state を作成 6. 安全な標準ライブラリを開く 7. 禁止グローバルを削除 8. `ffy` API を登録 9. Lua ソースを読み込み 10. 初期化コードを実行 11. `on_update` と `on_draw` の登録済み関数を取得 12. `requestAnimationFrame` でフレームループを開始 ### フレーム処理 各フレームでは次を実行します。 1. 前フレームからの経過秒数 `dt` を計算 2. `dt` は最大 `0.1` 秒に丸める 3. `on_update` 登録関数があれば `dt` を渡して呼ぶ 4. `on_draw` 登録関数があれば呼ぶ 5. エラーがあれば停止してログに出す ### 命令数制限 Lua の実行には hook による命令数制限があります。 | 処理 | 制限 | | --- | --- | | 初期化コード | `250000` 命令相当 | | `on_update` | `40000` 命令相当 | | `on_draw` | `40000` 命令相当 | 制限を超えると `Lua instruction limit exceeded` で停止します。 ## 10. グローバル関数 ### `print(...)` Lua の `print` は Lua4Frozen のログへ出力されます。 複数引数はタブ区切りで結合されます。 同じ関数は `ffy.print(...)` としても利用できます。 ## 11. `ffy` API Lua4Frozen はグローバルに `ffy` テーブルを公開します。 ### `ffy.on_update(fn)` 毎フレームの更新関数を登録します。 関数でない値を渡した場合は無視されます。 ```lua function update(dt) -- dt is seconds end ffy.on_update(update) ``` ### `ffy.on_draw(fn)` 毎フレームの描画関数を登録します。 関数でない値を渡した場合は無視されます。 ```lua function draw() ffy.gfx.clear("#111827") end ffy.on_draw(draw) ``` ## 12. `ffy.gfx` Canvas 描画 API です。Canvas の内部サイズは `800 x 450` です。 ### `ffy.gfx.size()` Canvas の幅と高さを返します。 ```lua w, h = ffy.gfx.size() ``` ### `ffy.gfx.color(color)` 現在の塗り色と線色を設定します。 `color` は CanvasRenderingContext2D の色指定として解釈されます。 ```lua ffy.gfx.color("#7dd3fc") ``` ### `ffy.gfx.clear(color?)` Canvas を消去します。 `color` が指定されている場合は、消去後に全体をその色で塗ります。 ```lua ffy.gfx.clear("#111827") ffy.gfx.clear() ``` ### `ffy.gfx.rect(x, y, w, h, fill?)` 矩形を描きます。 `fill` が省略または真なら塗り、偽なら線だけです。 ```lua ffy.gfx.rect(40, 40, 160, 80) ffy.gfx.rect(40, 40, 160, 80, false) ``` ### `ffy.gfx.circle(x, y, r, fill?)` 円を描きます。 `fill` が省略または真なら塗り、偽なら線だけです。 ```lua ffy.gfx.circle(120, 90, 24) ffy.gfx.circle(120, 90, 24, false) ``` ### `ffy.gfx.line(x1, y1, x2, y2)` 線を描きます。 ```lua ffy.gfx.line(10, 10, 300, 120) ``` ### `ffy.gfx.text(text, x, y, size?)` テキストを描きます。 `size` が未指定の場合は `18px` です。フォントは `sans-serif` です。 ```lua ffy.gfx.text("Hello", 40, 80, 24) ``` ## 12.5. `ffy.svg` SVG ベクタ表示 API です。SVG レイヤーは Canvas の 1 つ上に重なります。Canvas 描画機能はそのまま利用でき、SVG レイヤーだけを差し替えまたは消去できます。 ### `ffy.svg.set(markup)` SVG 全体、または SVG 要素断片を表示します。断片を渡した場合は `viewBox="0 0 800 450"` の `` に包まれます。 危険なタグ、イベント属性、外部 URL 参照、`javascript:` 参照は除去されます。 ```lua ffy.svg.set('') ``` ### `ffy.svg.clear()` SVG レイヤーを消去します。 ```lua ffy.svg.clear() ``` ## 13. `ffy.input` キーボードとマウスの状態を取得します。 Canvas にフォーカスがある間、実キーボード入力が反映されます。テストランナーでは仮想キーボード入力も反映されます。 ### `ffy.input.key(name)` 指定キーが押されているかを返します。 キー名は小文字化して比較されます。 ```lua if ffy.input.key("arrowleft") then x = x - 2 end ``` 主なキー名例: - `arrowleft` - `arrowright` - `arrowup` - `arrowdown` - `enter` - `escape` - `shift` - `control` - `alt` - `altgraph` - `meta` - `backspace` - `tab` - `delete` - ` ` ### `ffy.input.mouse()` マウス状態のテーブルを返します。 | フィールド | 型 | 内容 | | --- | --- | --- | | `x` | number | Canvas 内 X 座標 | | `y` | number | Canvas 内 Y 座標 | | `down` | boolean | マウスボタンが押されているか | | `button` | number | MouseEvent の `button` 値 | 座標は表示サイズではなく `800 x 450` の Canvas 座標へ変換されます。 ## 14. `ffy.html` Canvas 下の HTML 出力領域を操作します。 ### `ffy.html.set(html)` HTML 文字列を表示します。 ```lua ffy.html.set("Status Ready") ``` 表示前にサニタイズされます。 削除されるものは次の通りです。 - `script` - `iframe` - `object` - `embed` - `link` - `meta` - `form` - `on...` イベント属性 - `http://`, `https://`, `//` で始まる `src` または `href` ## 15. `ffy.audio` ### `ffy.audio.beep(frequency?, duration?)` ビープ音を鳴らします。 | 引数 | 既定値 | 内容 | | --- | --- | --- | | `frequency` | `440` | 周波数 Hz | | `duration` | `0.12` | 秒数 | 音量は固定で小さめに設定されます。 `duration` は最大 `2` 秒に丸められます。 ブラウザの自動再生制限により、ユーザー操作なしでは鳴らない場合があります。 ## 16. `ffy.fs` Lua4Frozen のファイル API です。 ### `ffy.fs.read(name)` 指定名のファイル内容を文字列で返します。 存在しない場合は空文字列を返します。 投稿実行画面では、添付ファイルと許可済み Drive ファイルが対象です。 テストランナーでは、仮想ファイルと許可済み Drive ファイルが対象です。 ### `ffy.fs.write(name, content)` Drive への保存を試みます。 保存前に確認ダイアログが表示されます。未ログインの場合は保存されません。 保存ファイル名は次の処理を受けます。 - `\ / : * ? " < > |` を `_` に置換 - 最大 120 文字に切り詰め - 空の場合は `lua4frozen.txt` - MIME type は `text/plain` `.jsondb`, `.ffdb`, `.db.json` は Lua4Frozen データベースファイルとして扱われます。 データベースファイルは Drive ファイルとして保存されますが、Drive 容量ではなく Lua4Frozen データベース容量を使用します。 ### `ffy.fs.list()` 見えているファイル名を Lua 配列として返します。 投稿実行画面では添付ファイル名と Drive ファイル名です。 テストランナーでは仮想ファイル名と Drive ファイル名です。 ## 17. Drive 連携 Drive 読み取りは初回 `ffy.fs.read` 時に必要に応じて確認されます。 実行 1 回ごとに許可状態は初期化されます。 ### 読み取り対象 Drive 読み取り許可後、最大 100 件の Drive ファイル一覧を取得します。 読み取り対象は次の条件を満たすものです。 - サイズが `512 KiB` 以下 - 拡張子が `.lua`, `.txt`, `.json`, `.csv`, `.jsondb`, `.ffdb`, `.db.json` 読み取りに失敗したファイルは無視されます。 ### 書き込み対象 書き込みは Drive へアップロードされます。 `ffy.fs.write` はメモリ上の Drive ファイル map も更新するため、同じ実行中に `ffy.fs.read` で読み返せます。 ### データベース容量 Lua4Frozen データベースファイルは、管理者がコントロールパネルで設定した Lua4Frozen データベース容量を使用します。 容量を超えた場合、新規書き込みは拒否されます。Drive 上の古いデータベースファイルを削除して空きを作ると、再び書き込めます。 ## 18. `ffy.db` / `ffy.sql` Lua4Frozen データベースファイルは JSON 形式の単純な key-value データベースとして扱われます。 ### `ffy.db.get(name, key)` 指定した DB ファイルから値を取得します。存在しない場合は空文字列を返します。 ### `ffy.db.set(name, key, value)` 指定した DB ファイルへ値を書き込みます。`name` に DB 拡張子がない場合は `.jsondb` が補われます。 ### `ffy.db.delete(name, key)` 指定したキーを削除します。 ### `ffy.db.list(name)` 指定した DB ファイルのキー一覧を Lua 配列として返します。 ### `ffy.sql.exec(name, sql)` 簡易 SQL を実行します。対応する構文は次の通りです。 - `CREATE TABLE ...` - `INSERT INTO table (key, value) VALUES ('key', 'value')` - `SELECT value FROM table WHERE key = 'key'` - `SELECT * FROM table` - `DELETE FROM table WHERE key = 'key'` ## 19. 仮想キーボード テストランナーには画面上の仮想キーボードがあります。 仮想キーボードは `ffy.input.key` の入力状態にも反映されます。 ### 標準配列 現在の標準配列は次の通りです。 - `jis` - `us-intl` - `uk` - `azerty-fr` - `custom-jis` - `custom-ansi` - `custom-iso` - `custom-azerty` `custom-*` は編集用の空配列テンプレートです。 ### 修飾キー 次のキーは修飾キーとして扱われます。 - `shift` - `control` - `alt` - `altgraph` - `meta` 修飾キーはタップ回数で状態を切り替えます。 | タップ回数 | 状態 | | --- | --- | | 0 | off | | 1 | latched | | 2 | locked | 通常キーを押すと、latched 状態の修飾キーは消費されます。 ### テキスト挿入 仮想キーに `text` があり、フォーカス中の要素がテストランナーの Lua エディタである場合、その文字列をエディタへ挿入します。 ## 20. キーボード配列 JSON キーボード配列は JSON で定義します。 ### レイアウト形式 ```json { "id": "my-layout", "name": "My Layout", "cols": 16, "rows": [ ["Esc", "1", "2", { "label": "Backspace", "w": 2 }], [{ "label": "Tab", "w": 2 }, "Q", "W", "E"], [{ "label": "Shift", "w": 2 }, "Z", "X", "C"] ] } ``` ### レイアウトフィールド | フィールド | 型 | 内容 | | --- | --- | --- | | `id` | string | 配列 ID | | `name` | string | 表示名 | | `cols` | number | グリッド列数。`1` から `24` に丸められる | | `rows` | array | キー行の配列 | | `keys` | array | 旧形式。`rows` がない場合に `cols` ごとに行へ変換 | ### キーフィールド キーは文字列またはオブジェクトで指定できます。 | フィールド | 型 | 内容 | | --- | --- | --- | | `label` | string | 表示ラベル | | `key` | string | `ffy.input.key` 用のキー名 | | `text` | string | エディタへ挿入する文字 | | `w` | number | 横幅。`1` から `6` に丸められる | | `h` | number | 高さ。`0.5` から `4` に丸められる | `key` が未指定の場合は、`label` からキー名が推定されます。 `text` が未指定かつ `label` が 1 文字の場合は、その文字が `text` になります。 ## 20. `ffy.keyboard` テストランナーでは、Lua プログラムから仮想キーボードを操作できます。 投稿実行画面には現在この API はありません。 ### `ffy.keyboard.set_layout_json(name, json)` プログラム由来のキーボード配列を登録し、選択します。 ```lua layout = [[ { "id": "gamepad", "name": "Gamepad", "cols": 8, "rows": [ ["ArrowUp"], ["ArrowLeft", "ArrowDown", "ArrowRight"] ] } ]] ffy.keyboard.set_layout_json("Gamepad", layout) ``` 登録された配列の ID は内部的に `program-` が付与されます。 ### `ffy.keyboard.select(id)` 既存のキーボード配列 ID を選択します。 ```lua ffy.keyboard.select("jis") ``` 存在しない ID を指定した場合は何もしません。 ## 21. ログ ログは画面上の `Log` に表示されます。 最大 200 行まで保持し、超えた分は古い行から削除されます。 ログに出る主な内容は次の通りです。 - `print` / `ffy.print` の出力 - Runtime error - Drive 読み取り許可・拒否 - Drive 書き込み許可・拒否・失敗 - 下書き保存 - キーボード配列保存・読み込み結果 ## 22. エラー処理 Lua の読み込み、初期実行、フレーム処理でエラーが起きると、Runtime error 表示とログ出力を行います。 フレーム処理中のエラーでは実行を停止します。 ## 23. セキュリティ仕様 Lua4Frozen はブラウザ内実行であり、以下の制約により危険な操作を抑制します。 - ファイルシステムの直接アクセス不可 - 任意モジュール読み込み不可 - `debug` API 不可 - OS コマンド実行不可 - Lua からの直接ネットワーク不可 - Drive 読み取りはユーザー確認が必要 - Drive 書き込みは保存ごとにユーザー確認が必要 - HTML 出力はサニタイズされる - Lua 命令数制限がある ## 24. 制限事項 現行仕様上の制限は次の通りです。 - Canvas は 2D のみ - Canvas 内部サイズは `800 x 450` - 画像読み込み API はない - 外部 URL 読み込み API はない - 永続ストレージは Drive 書き込みまたはテストランナーの `localStorage` に限られる - 投稿実行画面では仮想キーボード API は使えない - 添付ファイルの事前読み込みはテキスト系拡張子かつ `512 KiB` 以下のみ - Drive 読み取りは同期 XHR を使うため、実行時に一時的に UI が止まる可能性がある ## 25. 実装ファイル この仕様は主に以下の実装に対応しています。 - [lua4frozen.vue](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/pages/lua4frozen.vue) - [lua4frozen-test.vue](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/pages/lua4frozen-test.vue) - [lua4frozen-home.vue](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/pages/lua4frozen-home.vue) - [lua4frozen-spec.vue](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/pages/lua4frozen-spec.vue) - [lua4frozen-keyboards.json](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/pages/lua4frozen-keyboards.json) - [lua4frozen-search.ts](/home/mihkskhk/ドキュメント/iceshrimp/packages/backend/src/server/api/endpoints/notes/lua4frozen-search.ts) - [MkNote.vue](/home/mihkskhk/ドキュメント/iceshrimp/packages/client/src/components/MkNote.vue)