From 6dffa3d31ab353f73bcd7f908f7f00a95eb1f5ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 7 Jul 2026 10:15:40 +0900 Subject: [PATCH 1/4] feat(score): score PDF storage + IPC (clearfolio viewer contract, PR 1/3) Add three Tauri commands backing the upcoming score viewer: - attach_score_pdf(project_id, song_id): OS file dialog (.pdf only), validates %PDF- magic bytes and a 25MB size cap, copies the file into the app-owned //scores/.pdf and returns {scoreId, fileName, fileSizeBytes}. - read_score_pdf(project_id, score_id): strict lowercase-UUID allowlist, path rebuilt locally, canonicalize + prefix guard against traversal, returns file bytes. - remove_score_pdf(project_id, score_id): same guards, idempotent delete. Project ids are validated against the exact shape minted by next_project_id (project--) before any path join. Pure helpers are separated from the command wrappers and covered by unit tests (uuid guard, traversal/symlink rejection, magic-byte and size checks). Drive-by fix: import_youtube_url, save_project, and load_project were registered in the invoke handler but missing from the build.rs app manifest and capabilities/main.json, so their permissions were never generated or granted. Added them alongside the new score commands. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RjGVapDZ3k7V7zKYk16P4C --- apps/desktop/src-tauri/Cargo.lock | 1 + apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/build.rs | 6 + apps/desktop/src-tauri/capabilities/main.json | 8 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 72 ++++ .../src-tauri/gen/schemas/macOS-schema.json | 72 ++++ .../autogenerated/attach_score_pdf.toml | 11 + .../autogenerated/import_youtube_url.toml | 11 + .../autogenerated/load_project.toml | 11 + .../autogenerated/read_score_pdf.toml | 11 + .../autogenerated/remove_score_pdf.toml | 11 + .../autogenerated/save_project.toml | 11 + apps/desktop/src-tauri/src/main.rs | 354 +++++++++++++++++- 15 files changed, 580 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/attach_score_pdf.toml create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/import_youtube_url.toml create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/load_project.toml create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/read_score_pdf.toml create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/remove_score_pdf.toml create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/save_project.toml diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 0df254ea..9dbf7bf1 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -79,6 +79,7 @@ dependencies = [ "time", "tokio", "url", + "uuid", ] [[package]] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index de282982..f0369488 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -14,6 +14,7 @@ tauri = { version = "2.11.1", default-features = false, features = ["wry"] } time = { version = "0.3", features = ["formatting", "macros"] } tokio = { version = "1.50.0", features = ["time"] } url = "2.5.8" +uuid = { version = "1", features = ["v4"] } [features] default = [] diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index 997eeba3..0caf74c6 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -4,6 +4,12 @@ fn main() { "start_analysis_job", "get_analysis_job_status", "select_local_audio_source", + "import_youtube_url", + "save_project", + "load_project", + "attach_score_pdf", + "read_score_pdf", + "remove_score_pdf", ]), )) .expect("failed to build tauri application manifest"); diff --git a/apps/desktop/src-tauri/capabilities/main.json b/apps/desktop/src-tauri/capabilities/main.json index 351eb9c0..8103420f 100644 --- a/apps/desktop/src-tauri/capabilities/main.json +++ b/apps/desktop/src-tauri/capabilities/main.json @@ -8,6 +8,12 @@ "core:event:allow-unlisten", "allow-start-analysis-job", "allow-get-analysis-job-status", - "allow-select-local-audio-source" + "allow-select-local-audio-source", + "allow-import-youtube-url", + "allow-save-project", + "allow-load-project", + "allow-attach-score-pdf", + "allow-read-score-pdf", + "allow-remove-score-pdf" ] } diff --git a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json index 18685a13..c5cda5d6 100644 --- a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json +++ b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"allow-get-analysis-job-status":{"identifier":"allow-get-analysis-job-status","description":"Enables the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":["get_analysis_job_status"],"deny":[]}},"allow-select-local-audio-source":{"identifier":"allow-select-local-audio-source","description":"Enables the select_local_audio_source command without any pre-configured scope.","commands":{"allow":["select_local_audio_source"],"deny":[]}},"allow-start-analysis-job":{"identifier":"allow-start-analysis-job","description":"Enables the start_analysis_job command without any pre-configured scope.","commands":{"allow":["start_analysis_job"],"deny":[]}},"deny-get-analysis-job-status":{"identifier":"deny-get-analysis-job-status","description":"Denies the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":[],"deny":["get_analysis_job_status"]}},"deny-select-local-audio-source":{"identifier":"deny-select-local-audio-source","description":"Denies the select_local_audio_source command without any pre-configured scope.","commands":{"allow":[],"deny":["select_local_audio_source"]}},"deny-start-analysis-job":{"identifier":"deny-start-analysis-job","description":"Denies the start_analysis_job command without any pre-configured scope.","commands":{"allow":[],"deny":["start_analysis_job"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"allow-attach-score-pdf":{"identifier":"allow-attach-score-pdf","description":"Enables the attach_score_pdf command without any pre-configured scope.","commands":{"allow":["attach_score_pdf"],"deny":[]}},"allow-get-analysis-job-status":{"identifier":"allow-get-analysis-job-status","description":"Enables the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":["get_analysis_job_status"],"deny":[]}},"allow-import-youtube-url":{"identifier":"allow-import-youtube-url","description":"Enables the import_youtube_url command without any pre-configured scope.","commands":{"allow":["import_youtube_url"],"deny":[]}},"allow-load-project":{"identifier":"allow-load-project","description":"Enables the load_project command without any pre-configured scope.","commands":{"allow":["load_project"],"deny":[]}},"allow-read-score-pdf":{"identifier":"allow-read-score-pdf","description":"Enables the read_score_pdf command without any pre-configured scope.","commands":{"allow":["read_score_pdf"],"deny":[]}},"allow-remove-score-pdf":{"identifier":"allow-remove-score-pdf","description":"Enables the remove_score_pdf command without any pre-configured scope.","commands":{"allow":["remove_score_pdf"],"deny":[]}},"allow-save-project":{"identifier":"allow-save-project","description":"Enables the save_project command without any pre-configured scope.","commands":{"allow":["save_project"],"deny":[]}},"allow-select-local-audio-source":{"identifier":"allow-select-local-audio-source","description":"Enables the select_local_audio_source command without any pre-configured scope.","commands":{"allow":["select_local_audio_source"],"deny":[]}},"allow-start-analysis-job":{"identifier":"allow-start-analysis-job","description":"Enables the start_analysis_job command without any pre-configured scope.","commands":{"allow":["start_analysis_job"],"deny":[]}},"deny-attach-score-pdf":{"identifier":"deny-attach-score-pdf","description":"Denies the attach_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["attach_score_pdf"]}},"deny-get-analysis-job-status":{"identifier":"deny-get-analysis-job-status","description":"Denies the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":[],"deny":["get_analysis_job_status"]}},"deny-import-youtube-url":{"identifier":"deny-import-youtube-url","description":"Denies the import_youtube_url command without any pre-configured scope.","commands":{"allow":[],"deny":["import_youtube_url"]}},"deny-load-project":{"identifier":"deny-load-project","description":"Denies the load_project command without any pre-configured scope.","commands":{"allow":[],"deny":["load_project"]}},"deny-read-score-pdf":{"identifier":"deny-read-score-pdf","description":"Denies the read_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["read_score_pdf"]}},"deny-remove-score-pdf":{"identifier":"deny-remove-score-pdf","description":"Denies the remove_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_score_pdf"]}},"deny-save-project":{"identifier":"deny-save-project","description":"Denies the save_project command without any pre-configured scope.","commands":{"allow":[],"deny":["save_project"]}},"deny-select-local-audio-source":{"identifier":"deny-select-local-audio-source","description":"Denies the select_local_audio_source command without any pre-configured scope.","commands":{"allow":[],"deny":["select_local_audio_source"]}},"deny-start-analysis-job":{"identifier":"deny-start-analysis-job","description":"Denies the start_analysis_job command without any pre-configured scope.","commands":{"allow":[],"deny":["start_analysis_job"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/capabilities.json b/apps/desktop/src-tauri/gen/schemas/capabilities.json index 8a1f651e..25d76424 100644 --- a/apps/desktop/src-tauri/gen/schemas/capabilities.json +++ b/apps/desktop/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-capability":{"identifier":"main-capability","description":"Capability for the main BandScope window to use the analysis orchestration commands.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-start-analysis-job","allow-get-analysis-job-status","allow-select-local-audio-source"]}} \ No newline at end of file +{"main-capability":{"identifier":"main-capability","description":"Capability for the main BandScope window to use the analysis orchestration commands.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-start-analysis-job","allow-get-analysis-job-status","allow-select-local-audio-source","allow-import-youtube-url","allow-save-project","allow-load-project","allow-attach-score-pdf","allow-read-score-pdf","allow-remove-score-pdf"]}} \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json index 1c57ee00..a46e367a 100644 --- a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -176,12 +176,48 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the attach_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-attach-score-pdf", + "markdownDescription": "Enables the attach_score_pdf command without any pre-configured scope." + }, { "description": "Enables the get_analysis_job_status command without any pre-configured scope.", "type": "string", "const": "allow-get-analysis-job-status", "markdownDescription": "Enables the get_analysis_job_status command without any pre-configured scope." }, + { + "description": "Enables the import_youtube_url command without any pre-configured scope.", + "type": "string", + "const": "allow-import-youtube-url", + "markdownDescription": "Enables the import_youtube_url command without any pre-configured scope." + }, + { + "description": "Enables the load_project command without any pre-configured scope.", + "type": "string", + "const": "allow-load-project", + "markdownDescription": "Enables the load_project command without any pre-configured scope." + }, + { + "description": "Enables the read_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-read-score-pdf", + "markdownDescription": "Enables the read_score_pdf command without any pre-configured scope." + }, + { + "description": "Enables the remove_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-score-pdf", + "markdownDescription": "Enables the remove_score_pdf command without any pre-configured scope." + }, + { + "description": "Enables the save_project command without any pre-configured scope.", + "type": "string", + "const": "allow-save-project", + "markdownDescription": "Enables the save_project command without any pre-configured scope." + }, { "description": "Enables the select_local_audio_source command without any pre-configured scope.", "type": "string", @@ -194,12 +230,48 @@ "const": "allow-start-analysis-job", "markdownDescription": "Enables the start_analysis_job command without any pre-configured scope." }, + { + "description": "Denies the attach_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-attach-score-pdf", + "markdownDescription": "Denies the attach_score_pdf command without any pre-configured scope." + }, { "description": "Denies the get_analysis_job_status command without any pre-configured scope.", "type": "string", "const": "deny-get-analysis-job-status", "markdownDescription": "Denies the get_analysis_job_status command without any pre-configured scope." }, + { + "description": "Denies the import_youtube_url command without any pre-configured scope.", + "type": "string", + "const": "deny-import-youtube-url", + "markdownDescription": "Denies the import_youtube_url command without any pre-configured scope." + }, + { + "description": "Denies the load_project command without any pre-configured scope.", + "type": "string", + "const": "deny-load-project", + "markdownDescription": "Denies the load_project command without any pre-configured scope." + }, + { + "description": "Denies the read_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-read-score-pdf", + "markdownDescription": "Denies the read_score_pdf command without any pre-configured scope." + }, + { + "description": "Denies the remove_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-score-pdf", + "markdownDescription": "Denies the remove_score_pdf command without any pre-configured scope." + }, + { + "description": "Denies the save_project command without any pre-configured scope.", + "type": "string", + "const": "deny-save-project", + "markdownDescription": "Denies the save_project command without any pre-configured scope." + }, { "description": "Denies the select_local_audio_source command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json index 1c57ee00..a46e367a 100644 --- a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json @@ -176,12 +176,48 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the attach_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-attach-score-pdf", + "markdownDescription": "Enables the attach_score_pdf command without any pre-configured scope." + }, { "description": "Enables the get_analysis_job_status command without any pre-configured scope.", "type": "string", "const": "allow-get-analysis-job-status", "markdownDescription": "Enables the get_analysis_job_status command without any pre-configured scope." }, + { + "description": "Enables the import_youtube_url command without any pre-configured scope.", + "type": "string", + "const": "allow-import-youtube-url", + "markdownDescription": "Enables the import_youtube_url command without any pre-configured scope." + }, + { + "description": "Enables the load_project command without any pre-configured scope.", + "type": "string", + "const": "allow-load-project", + "markdownDescription": "Enables the load_project command without any pre-configured scope." + }, + { + "description": "Enables the read_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-read-score-pdf", + "markdownDescription": "Enables the read_score_pdf command without any pre-configured scope." + }, + { + "description": "Enables the remove_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-score-pdf", + "markdownDescription": "Enables the remove_score_pdf command without any pre-configured scope." + }, + { + "description": "Enables the save_project command without any pre-configured scope.", + "type": "string", + "const": "allow-save-project", + "markdownDescription": "Enables the save_project command without any pre-configured scope." + }, { "description": "Enables the select_local_audio_source command without any pre-configured scope.", "type": "string", @@ -194,12 +230,48 @@ "const": "allow-start-analysis-job", "markdownDescription": "Enables the start_analysis_job command without any pre-configured scope." }, + { + "description": "Denies the attach_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-attach-score-pdf", + "markdownDescription": "Denies the attach_score_pdf command without any pre-configured scope." + }, { "description": "Denies the get_analysis_job_status command without any pre-configured scope.", "type": "string", "const": "deny-get-analysis-job-status", "markdownDescription": "Denies the get_analysis_job_status command without any pre-configured scope." }, + { + "description": "Denies the import_youtube_url command without any pre-configured scope.", + "type": "string", + "const": "deny-import-youtube-url", + "markdownDescription": "Denies the import_youtube_url command without any pre-configured scope." + }, + { + "description": "Denies the load_project command without any pre-configured scope.", + "type": "string", + "const": "deny-load-project", + "markdownDescription": "Denies the load_project command without any pre-configured scope." + }, + { + "description": "Denies the read_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-read-score-pdf", + "markdownDescription": "Denies the read_score_pdf command without any pre-configured scope." + }, + { + "description": "Denies the remove_score_pdf command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-score-pdf", + "markdownDescription": "Denies the remove_score_pdf command without any pre-configured scope." + }, + { + "description": "Denies the save_project command without any pre-configured scope.", + "type": "string", + "const": "deny-save-project", + "markdownDescription": "Denies the save_project command without any pre-configured scope." + }, { "description": "Denies the select_local_audio_source command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/permissions/autogenerated/attach_score_pdf.toml b/apps/desktop/src-tauri/permissions/autogenerated/attach_score_pdf.toml new file mode 100644 index 00000000..c99126a9 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/attach_score_pdf.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-attach-score-pdf" +description = "Enables the attach_score_pdf command without any pre-configured scope." +commands.allow = ["attach_score_pdf"] + +[[permission]] +identifier = "deny-attach-score-pdf" +description = "Denies the attach_score_pdf command without any pre-configured scope." +commands.deny = ["attach_score_pdf"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/import_youtube_url.toml b/apps/desktop/src-tauri/permissions/autogenerated/import_youtube_url.toml new file mode 100644 index 00000000..03debda8 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/import_youtube_url.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-import-youtube-url" +description = "Enables the import_youtube_url command without any pre-configured scope." +commands.allow = ["import_youtube_url"] + +[[permission]] +identifier = "deny-import-youtube-url" +description = "Denies the import_youtube_url command without any pre-configured scope." +commands.deny = ["import_youtube_url"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/load_project.toml b/apps/desktop/src-tauri/permissions/autogenerated/load_project.toml new file mode 100644 index 00000000..40a6ae52 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/load_project.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-load-project" +description = "Enables the load_project command without any pre-configured scope." +commands.allow = ["load_project"] + +[[permission]] +identifier = "deny-load-project" +description = "Denies the load_project command without any pre-configured scope." +commands.deny = ["load_project"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/read_score_pdf.toml b/apps/desktop/src-tauri/permissions/autogenerated/read_score_pdf.toml new file mode 100644 index 00000000..b819abe9 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/read_score_pdf.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-read-score-pdf" +description = "Enables the read_score_pdf command without any pre-configured scope." +commands.allow = ["read_score_pdf"] + +[[permission]] +identifier = "deny-read-score-pdf" +description = "Denies the read_score_pdf command without any pre-configured scope." +commands.deny = ["read_score_pdf"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/remove_score_pdf.toml b/apps/desktop/src-tauri/permissions/autogenerated/remove_score_pdf.toml new file mode 100644 index 00000000..8133491d --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/remove_score_pdf.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-remove-score-pdf" +description = "Enables the remove_score_pdf command without any pre-configured scope." +commands.allow = ["remove_score_pdf"] + +[[permission]] +identifier = "deny-remove-score-pdf" +description = "Denies the remove_score_pdf command without any pre-configured scope." +commands.deny = ["remove_score_pdf"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/save_project.toml b/apps/desktop/src-tauri/permissions/autogenerated/save_project.toml new file mode 100644 index 00000000..3832c067 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/save_project.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-save-project" +description = "Enables the save_project command without any pre-configured scope." +commands.allow = ["save_project"] + +[[permission]] +identifier = "deny-save-project" +description = "Denies the save_project command without any pre-configured scope." +commands.deny = ["save_project"] diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 2de7adde..78d29ae4 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -34,6 +34,8 @@ const ANALYSIS_WAIT_POLL: Duration = Duration::from_millis(50); const AUDIO_EXTENSIONS: [&str; 4] = ["wav", "mp3", "flac", "m4a"]; const MISSING_ANALYSIS_PYTHON: &str = "__bandscope_missing_analysis_python__"; const YOUTUBE_IMPORT_TIMEOUT: Duration = Duration::from_secs(120); +const MAX_SCORE_PDF_BYTES: u64 = 25 * 1024 * 1024; +const PDF_MAGIC: &[u8] = b"%PDF-"; impl Default for AppState { fn default() -> Self { @@ -1233,6 +1235,225 @@ fn load_project() -> Result { project_payload_from_content(&content) } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ScoreAttachmentPayload { + score_id: String, + file_name: String, + file_size_bytes: u64, +} + +/// Security Notes: project ids never come from free-form user input. They are +/// only ever minted by `next_project_id` as `project--`, so +/// anything from the WebView that does not match that exact shape is rejected +/// before it can influence a filesystem path (no separators, no `..`). +fn is_valid_project_id(value: &str) -> bool { + let Some(rest) = value.strip_prefix("project-") else { + return false; + }; + let mut segments = rest.split('-'); + match (segments.next(), segments.next(), segments.next()) { + (Some(timestamp), Some(counter), None) => { + !timestamp.is_empty() + && !counter.is_empty() + && timestamp.bytes().all(|byte| byte.is_ascii_digit()) + && counter.bytes().all(|byte| byte.is_ascii_digit()) + } + _ => false, + } +} + +/// Security Notes: score ids are minted locally via UUID v4 and must round-trip +/// as exactly a lowercase hyphenated UUID (8-4-4-4-12). This is an allowlist +/// check, so path traversal payloads (`..`, separators, null bytes) can never +/// reach the path join below. +fn is_valid_score_id(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.len() != 36 { + return false; + } + bytes.iter().enumerate().all(|(index, byte)| match index { + 8 | 13 | 18 | 23 => *byte == b'-', + _ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'), + }) +} + +/// Security Notes: the selected file is untrusted input (`User Input Boundary`). +/// We refuse symlinks before canonicalizing, require a real non-empty regular +/// file with a `.pdf` extension, cap the size at 25MB, and verify the `%PDF-` +/// magic bytes so a mislabeled file cannot be attached as a score. +fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), String> { + let link_metadata = std::fs::symlink_metadata(path) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if link_metadata.file_type().is_symlink() { + return Err("Could not read the selected PDF file.".to_string()); + } + + let canonical = path + .canonicalize() + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + let extension = canonical + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .ok_or_else(|| "Choose a PDF file to attach as a score.".to_string())?; + if extension != "pdf" { + return Err("Choose a PDF file to attach as a score.".into()); + } + + let metadata = std::fs::metadata(&canonical) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if !metadata.is_file() || metadata.len() == 0 { + return Err("Could not read the selected PDF file.".into()); + } + if metadata.len() > MAX_SCORE_PDF_BYTES { + return Err("Score PDF is too large (exceeds 25MB limit).".into()); + } + + let mut header = [0u8; PDF_MAGIC.len()]; + std::fs::File::open(&canonical) + .and_then(|mut file| file.read_exact(&mut header)) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if header != PDF_MAGIC { + return Err("The selected file is not a valid PDF.".into()); + } + + let file_name = canonical + .file_name() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()) + .ok_or_else(|| "Could not read the selected PDF file.".to_string())?; + + let file_size_bytes = metadata.len(); + Ok((canonical, file_name, file_size_bytes)) +} + +/// Security Notes: reads and deletes never accept an arbitrary path from the +/// WebView. The path is rebuilt server-side from validated ids, symlinks are +/// refused, and the canonicalized result must still live under the +/// canonicalized app-owned scores root (path-traversal guard). +fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result { + if !is_valid_score_id(score_id) { + return Err("Score was not found.".to_string()); + } + let candidate = scores_root.join(format!("{score_id}.pdf")); + let link_metadata = + std::fs::symlink_metadata(&candidate).map_err(|_| "Score was not found.".to_string())?; + if link_metadata.file_type().is_symlink() { + return Err("Score was not found.".to_string()); + } + + let canonical_root = scores_root + .canonicalize() + .map_err(|_| "Score was not found.".to_string())?; + let canonical = candidate + .canonicalize() + .map_err(|_| "Score was not found.".to_string())?; + if !canonical.starts_with(&canonical_root) { + return Err("Score was not found.".to_string()); + } + + let metadata = std::fs::metadata(&canonical).map_err(|_| "Score was not found.".to_string())?; + if !metadata.is_file() { + return Err("Score was not found.".to_string()); + } + Ok(canonical) +} + +fn scores_root_for_project( + app: &tauri::AppHandle, + project_id: &str, +) -> Result { + // Callers must have validated `project_id` with `is_valid_project_id` + // before this join; the root stays inside the app-owned data directory. + let project_root = app_owned_root(app, "projects", project_id)?; + let root = project_root.join("scores"); + std::fs::create_dir_all(&root) + .map_err(|_| "Could not prepare the local scores workspace.".to_string())?; + Ok(root) +} + +/// Security Notes: the file path comes exclusively from the OS file dialog +/// (never from JS), is validated (magic bytes, size, extension, no symlink), +/// and is copied into the app-owned scores directory. The stored copy is named +/// by a locally minted UUID v4, so no untrusted external path is ever +/// referenced again after this command returns. +#[tauri::command] +fn attach_score_pdf( + project_id: String, + song_id: String, + app: tauri::AppHandle, +) -> Result { + if !is_valid_project_id(&project_id) { + return Err("Invalid project id.".to_string()); + } + // `song_id` is part of the viewer contract (score-to-song association is + // persisted on the JS side in a later slice); it never touches a path. + if song_id.trim().is_empty() { + return Err("Invalid song id.".to_string()); + } + + let path = FileDialog::new() + .add_filter("PDF Score", &["pdf"]) + .pick_file() + .ok_or_else(|| "Choose a PDF file to attach as a score.".to_string())?; + let (source, file_name, file_size_bytes) = validate_score_pdf_source(&path)?; + + let scores_root = scores_root_for_project(&app, &project_id)?; + let score_id = uuid::Uuid::new_v4().to_string(); + let destination = scores_root.join(format!("{score_id}.pdf")); + std::fs::copy(&source, &destination) + .map_err(|_| "Could not copy the PDF into the project workspace.".to_string())?; + + Ok(ScoreAttachmentPayload { + score_id, + file_name, + file_size_bytes, + }) +} + +/// Security Notes: no path crosses the IPC boundary. Both ids are validated +/// against strict allowlist shapes, the path is rebuilt locally, and the +/// canonicalize-plus-prefix guard in `resolve_existing_score_pdf` rejects any +/// escape from the app-owned scores root. +#[tauri::command] +fn read_score_pdf( + project_id: String, + score_id: String, + app: tauri::AppHandle, +) -> Result, String> { + if !is_valid_project_id(&project_id) { + return Err("Invalid project id.".to_string()); + } + let scores_root = scores_root_for_project(&app, &project_id)?; + let path = resolve_existing_score_pdf(&scores_root, &score_id)?; + std::fs::read(path).map_err(|_| "Could not read the score PDF.".to_string()) +} + +/// Security Notes: same id validation and traversal guard as `read_score_pdf`; +/// deletion is scoped to a single validated file inside the app-owned scores +/// root. Returns `false` when the score does not exist (idempotent removal). +#[tauri::command] +fn remove_score_pdf( + project_id: String, + score_id: String, + app: tauri::AppHandle, +) -> Result { + if !is_valid_project_id(&project_id) { + return Err("Invalid project id.".to_string()); + } + if !is_valid_score_id(&score_id) { + return Err("Invalid score id.".to_string()); + } + let scores_root = scores_root_for_project(&app, &project_id)?; + let path = match resolve_existing_score_pdf(&scores_root, &score_id) { + Ok(path) => path, + Err(_) => return Ok(false), + }; + std::fs::remove_file(path).map_err(|_| "Could not remove the score PDF.".to_string())?; + Ok(true) +} + #[cfg(test)] mod tests { use super::*; @@ -1503,6 +1724,134 @@ mod tests { let _ = std::fs::remove_dir_all(cache_root); let _ = std::fs::remove_dir_all(outside_root); } + + #[test] + fn project_id_guard_accepts_generated_ids_only() { + let generated = next_project_id(&AppState::default()); + assert!(is_valid_project_id(&generated)); + assert!(is_valid_project_id("project-1751234567890123456-1")); + + assert!(!is_valid_project_id("")); + assert!(!is_valid_project_id("project-")); + assert!(!is_valid_project_id("project-123")); + assert!(!is_valid_project_id("project-123-")); + assert!(!is_valid_project_id("project-123-4-5")); + assert!(!is_valid_project_id("project-abc-1")); + assert!(!is_valid_project_id("project-123-1x")); + assert!(!is_valid_project_id("other-123-1")); + assert!(!is_valid_project_id("../project-123-1")); + assert!(!is_valid_project_id("project-123-1/..")); + assert!(!is_valid_project_id("project-..-1")); + assert!(!is_valid_project_id("project-123-1/escape")); + } + + #[test] + fn score_id_guard_accepts_lowercase_uuid_v4_only() { + let generated = uuid::Uuid::new_v4().to_string(); + assert!(is_valid_score_id(&generated)); + assert!(is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e160355e")); + + assert!(!is_valid_score_id("")); + assert!(!is_valid_score_id("not-a-uuid")); + assert!(!is_valid_score_id("6FA459EA-EE8A-3CA4-894E-DB77E160355E")); + assert!(!is_valid_score_id("6fa459eaee8a3ca4894edb77e160355e")); + assert!(!is_valid_score_id("{6fa459ea-ee8a-3ca4-894e-db77e160355e}")); + assert!(!is_valid_score_id("../../../../etc/passwd-aaaa-bbbb-cc")); + assert!(!is_valid_score_id( + "6fa459ea-ee8a-3ca4-894e-db77e160355e/.." + )); + assert!(!is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e16035/e")); + } + + #[test] + fn score_pdf_source_requires_pdf_magic_size_and_real_file() { + let root = unique_test_dir("score-source"); + std::fs::create_dir_all(&root).expect("score source root should be created"); + + let valid = root.join("score.pdf"); + std::fs::write(&valid, b"%PDF-1.7 fake body").expect("valid pdf should be written"); + let (canonical, file_name, size) = + validate_score_pdf_source(&valid).expect("valid pdf should be accepted"); + assert_eq!(file_name, "score.pdf"); + assert_eq!(size, 18); + assert!(canonical.ends_with("score.pdf")); + + let wrong_magic = root.join("not-really.pdf"); + std::fs::write(&wrong_magic, b"PK\x03\x04 zip bytes") + .expect("wrong magic file should be written"); + assert!(validate_score_pdf_source(&wrong_magic).is_err()); + + let short = root.join("short.pdf"); + std::fs::write(&short, b"%PD").expect("short file should be written"); + assert!(validate_score_pdf_source(&short).is_err()); + + let empty = root.join("empty.pdf"); + std::fs::write(&empty, b"").expect("empty file should be written"); + assert!(validate_score_pdf_source(&empty).is_err()); + + let wrong_extension = root.join("score.txt"); + std::fs::write(&wrong_extension, b"%PDF-1.7").expect("txt file should be written"); + assert!(validate_score_pdf_source(&wrong_extension).is_err()); + + let missing = root.join("missing.pdf"); + assert!(validate_score_pdf_source(&missing).is_err()); + + let oversized = root.join("oversized.pdf"); + { + let file = std::fs::File::create(&oversized).expect("oversized file should be created"); + let mut file = file; + file.write_all(b"%PDF-1.7") + .expect("oversized header should be written"); + file.set_len(MAX_SCORE_PDF_BYTES + 1) + .expect("oversized file should be extended"); + } + assert!(validate_score_pdf_source(&oversized).is_err()); + + #[cfg(unix)] + { + let symlinked = root.join("linked.pdf"); + std::os::unix::fs::symlink(&valid, &symlinked).expect("symlink should be created"); + assert!(validate_score_pdf_source(&symlinked).is_err()); + } + + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn score_pdf_resolution_rejects_traversal_and_escapes() { + let scores_root = unique_test_dir("score-resolve"); + let outside_root = unique_test_dir("score-outside"); + std::fs::create_dir_all(&scores_root).expect("scores root should be created"); + std::fs::create_dir_all(&outside_root).expect("outside root should be created"); + + let score_id = "6fa459ea-ee8a-3ca4-894e-db77e160355e"; + let inside_file = scores_root.join(format!("{score_id}.pdf")); + std::fs::write(&inside_file, b"%PDF-1.7").expect("inside file should be written"); + + let resolved = resolve_existing_score_pdf(&scores_root, score_id) + .expect("stored score inside the root should resolve"); + assert!(resolved.ends_with(format!("{score_id}.pdf"))); + + assert!(resolve_existing_score_pdf(&scores_root, "../escape").is_err()); + assert!(resolve_existing_score_pdf(&scores_root, "..").is_err()); + assert!( + resolve_existing_score_pdf(&scores_root, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + .is_err() + ); + + #[cfg(unix)] + { + let outside_file = outside_root.join("secret.pdf"); + std::fs::write(&outside_file, b"%PDF-1.7").expect("outside file should be written"); + let linked_id = "11111111-2222-3333-4444-555555555555"; + std::os::unix::fs::symlink(&outside_file, scores_root.join(format!("{linked_id}.pdf"))) + .expect("symlink should be created"); + assert!(resolve_existing_score_pdf(&scores_root, linked_id).is_err()); + } + + let _ = std::fs::remove_dir_all(scores_root); + let _ = std::fs::remove_dir_all(outside_root); + } } fn main() { @@ -1514,7 +1863,10 @@ fn main() { start_analysis_job, get_analysis_job_status, save_project, - load_project + load_project, + attach_score_pdf, + read_score_pdf, + remove_score_pdf ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); From f0dbc3fe6210d8af4c191f2a9aa19508cdb542a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 7 Jul 2026 14:49:42 +0900 Subject: [PATCH 2/4] ci: re-trigger (prior run force-cancelled during runner jam) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RjGVapDZ3k7V7zKYk16P4C From 0e34ff85b008e17f73e01e318664679ad409259a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 8 Jul 2026 11:43:49 +0900 Subject: [PATCH 3/4] ci(rust): add Cargo workspace manifest so coverage tooling can run The OpenCode coverage-evidence job failed with "Rust files changed, but no root Cargo.toml was found" -- cargo-llvm-cov could not locate a workspace manifest, so cargo coverage never ran (a tooling/config gap, not missing tests). Add a root workspace that contains a new GUI-independent `bandscope-desktop-core` crate holding the payload contracts, validation guards, and process helpers that were inlined in the Tauri binary (src-tauri/src/main.rs), together with the existing unit-test suite. The Tauri binary now depends on that crate via a path dependency and is excluded from the workspace, since it links wry/webkit and needs a bundled frontend, so it is built on its own (see ci.yml). This lets `cargo llvm-cov` find the root manifest and run the full test suite from the workspace root on any platform -- no windowing system or frontend build required. Verified locally: - `cargo llvm-cov` runs from the repo root; all 12 tests pass; ~93% line coverage on the core crate. - `cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml --locked` still builds the Tauri binary against the extracted crate. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RTAMs4bpSZS77Xe3RQjv9P --- .gitignore | 1 + Cargo.lock | 586 ++++++++++++++++ Cargo.toml | 14 + apps/desktop/core/Cargo.toml | 19 + apps/desktop/core/src/lib.rs | 1030 ++++++++++++++++++++++++++++ apps/desktop/src-tauri/Cargo.lock | 11 + apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/main.rs | 1008 +-------------------------- 8 files changed, 1665 insertions(+), 1005 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 apps/desktop/core/Cargo.toml create mode 100644 apps/desktop/core/src/lib.rs diff --git a/.gitignore b/.gitignore index 4226f59a..023f2375 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ .mypy_cache/ .ruff_cache/ apps/desktop/src-tauri/target/ +/target/ *.pyc *.pyo *.egg-info/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..eb6c2e02 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,586 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bandscope-desktop-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "time", + "url", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..5b0b0feb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +# Root Cargo workspace manifest. +# +# The workspace contains the GUI-independent `bandscope-desktop-core` crate so +# that coverage tooling (e.g. `cargo llvm-cov`) can locate a root manifest and +# run the Rust test suite from the workspace root on any platform. +# +# The Tauri desktop binary (apps/desktop/src-tauri) is intentionally excluded: +# it depends on `wry`/webkit and a bundled frontend, so it is built on its own +# with `--manifest-path apps/desktop/src-tauri/Cargo.toml` (see ci.yml). It +# consumes `bandscope-desktop-core` as a path dependency. +[workspace] +resolver = "2" +members = ["apps/desktop/core"] +exclude = ["apps/desktop/src-tauri"] diff --git a/apps/desktop/core/Cargo.toml b/apps/desktop/core/Cargo.toml new file mode 100644 index 00000000..19cda59c --- /dev/null +++ b/apps/desktop/core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bandscope-desktop-core" +version = "0.1.0" +edition = "2021" +description = "GUI-independent payload contracts and validation logic for the BandScope desktop app." +publish = false + +[lib] +name = "bandscope_desktop_core" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +time = { version = "0.3", features = ["formatting", "macros"] } +url = "2.5.8" + +[dev-dependencies] +uuid = { version = "1", features = ["v4"] } diff --git a/apps/desktop/core/src/lib.rs b/apps/desktop/core/src/lib.rs new file mode 100644 index 00000000..83b440b7 --- /dev/null +++ b/apps/desktop/core/src/lib.rs @@ -0,0 +1,1030 @@ +//! Pure, GUI-independent logic for the BandScope desktop app. +//! +//! This crate holds every payload contract, validation guard, and process +//! helper that does not depend on Tauri or the WebView runtime. Keeping it +//! free of `tauri`/`wry` lets the full unit-test suite build and run (and be +//! measured for coverage) on any platform without a windowing system or a +//! bundled frontend. + +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use std::{ + collections::HashMap, + io::Read, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::{ + atomic::{AtomicU64, AtomicUsize, Ordering}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub struct AppState(pub Arc); + +pub struct AppStateInner { + pub next_job: AtomicU64, + pub in_flight_jobs: AtomicUsize, + pub jobs: Mutex>, + pub bootstrap_sources: Mutex>, +} + +pub const MAX_IN_FLIGHT_JOBS: usize = 2; + +pub const ANALYSIS_PROCESS_TIMEOUT: Duration = Duration::from_secs(30); + +pub const ANALYSIS_WAIT_POLL: Duration = Duration::from_millis(50); + +pub const AUDIO_EXTENSIONS: [&str; 4] = ["wav", "mp3", "flac", "m4a"]; + +pub const MISSING_ANALYSIS_PYTHON: &str = "__bandscope_missing_analysis_python__"; + +pub const YOUTUBE_IMPORT_TIMEOUT: Duration = Duration::from_secs(120); + +pub const MAX_SCORE_PDF_BYTES: u64 = 25 * 1024 * 1024; + +pub const PDF_MAGIC: &[u8] = b"%PDF-"; + +impl Default for AppState { + fn default() -> Self { + Self(Arc::new(AppStateInner { + next_job: AtomicU64::new(1), + in_flight_jobs: AtomicUsize::new(0), + jobs: Mutex::new(HashMap::new()), + bootstrap_sources: Mutex::new(HashMap::new()), + })) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AnalysisJobRequest { + pub source_kind: String, + pub project_id: Option, + pub source_label: String, + pub role_focus: Vec, + pub local_source: Option, + pub cache_root: Option, + pub temp_root: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalysisJobErrorCode { + InvalidRequest, + NotFound, + EngineUnavailable, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AnalysisJobError { + pub code: AnalysisJobErrorCode, + pub message: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalysisJobState { + Queued, + Running, + Succeeded, + Failed, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalysisJobStage { + Queued, + Decode, + Separate, + Analyze, + Persist, + Ready, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalysisCacheStatus { + Disabled, + Miss, + Hit, + Stored, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RehearsalSongPayload { + id: String, + title: String, + sections: Vec, + export_summary: ExportSummaryPayload, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ConfidencePayload { + level: String, + source: String, + notes: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CuePayload { + kind: String, + value: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RangePayload { + lowest_note: String, + highest_note: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct HarmonyPayload { + chord: String, + function_label: String, + source: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ManualOverridePayload { + field: String, + value: HarmonyPayload, + source: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RehearsalRolePayload { + id: String, + name: String, + role_type: String, + harmony: HarmonyPayload, + cue: CuePayload, + range: RangePayload, + confidence: ConfidencePayload, + rehearsal_priority: String, + simplification: String, + setup_note: String, + manual_overrides: Vec, + overlap_warnings: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SectionTimeRangePayload { + start: u32, + end: u32, +} + +impl<'de> Deserialize<'de> for SectionTimeRangePayload { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct RawSectionTimeRangePayload { + start: u32, + end: u32, + } + + let raw = RawSectionTimeRangePayload::deserialize(deserializer)?; + if raw.end <= raw.start { + return Err(serde::de::Error::custom( + "section timeRange end must be greater than start", + )); + } + + Ok(Self { + start: raw.start, + end: raw.end, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct PartGraphNodePayload { + role_id: String, + is_active: bool, + handoff_to: Vec, + handoff_from: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct RehearsalSectionPayload { + id: String, + label: String, + groove: String, + time_range: SectionTimeRangePayload, + confidence: ConfidencePayload, + roles: Vec, + part_graph: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExportSummaryPayload { + format: String, + headline: String, + focus_sections: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AnalysisJobStatus { + pub job_id: String, + pub state: AnalysisJobState, + pub requested_at: String, + pub updated_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub progress_label: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub progress_stage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub progress_percent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct LocalAudioSourcePayload { + pub source_path: String, + pub file_name: String, + pub extension: String, + pub file_size_bytes: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProjectBootstrapSummaryPayload { + pub project_id: String, + pub source_mode: String, + pub project_root: String, + pub cache_root: String, + pub temp_root: String, + pub source: LocalAudioSourcePayload, +} + +pub fn next_project_id(state: &AppState) -> String { + format!( + "project-{}-{}", + OffsetDateTime::now_utc().unix_timestamp_nanos(), + state.0.next_job.fetch_add(1, Ordering::Relaxed) + ) +} + +pub fn youtube_source_from_metadata( + metadata: &Value, + cache_root: &Path, +) -> Result { + let filepath = metadata + .get("filepath") + .and_then(|value| value.as_str()) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| "Failed to parse YouTube import response.".to_string())?; + let title = metadata + .get("title") + .and_then(|value| value.as_str()) + .unwrap_or("Unknown YouTube Audio"); + let path = Path::new(filepath); + let link_metadata = std::fs::symlink_metadata(path) + .map_err(|_| "Could not read downloaded audio file.".to_string())?; + if link_metadata.file_type().is_symlink() { + return Err("YouTube import returned an invalid audio path.".to_string()); + } + + let canonical_cache_root = cache_root + .canonicalize() + .map_err(|_| "Could not validate YouTube import workspace.".to_string())?; + let canonical = path + .canonicalize() + .map_err(|_| "Could not read downloaded audio file.".to_string())?; + if !canonical.starts_with(&canonical_cache_root) { + return Err("YouTube import returned an invalid audio path.".to_string()); + } + + let file_metadata = std::fs::metadata(&canonical) + .map_err(|_| "Could not read downloaded audio file.".to_string())?; + if !file_metadata.is_file() || file_metadata.len() == 0 { + return Err("YouTube import returned an invalid audio file.".to_string()); + } + + let extension = canonical + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .ok_or_else(|| "YouTube import returned an unsupported audio format.".to_string())?; + if !AUDIO_EXTENSIONS.contains(&extension.as_str()) { + return Err("YouTube import returned an unsupported audio format.".to_string()); + } + + let safe_title: String = title + .chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '.' => '_', + c if c.is_control() => '_', + c => c, + }) + .take(100) + .collect(); + let safe_title = if safe_title.is_empty() { + "youtube_audio".to_string() + } else { + safe_title + }; + + Ok(LocalAudioSourcePayload { + source_path: canonical.to_string_lossy().into_owned(), + file_name: format!("{safe_title}.{extension}"), + extension, + file_size_bytes: file_metadata.len(), + }) +} + +pub fn is_supported_youtube_url(url: &str) -> bool { + let parsed_url = match url::Url::parse(url) { + Ok(u) => u, + Err(_) => return false, + }; + if parsed_url.scheme() != "https" { + return false; + } + + let host = parsed_url.host_str().unwrap_or("").to_lowercase(); + if host == "youtu.be" { + let mut segments = match parsed_url.path_segments() { + Some(s) => s.filter(|segment| !segment.is_empty()), + None => return false, + }; + let Some(video_id) = segments.next() else { + return false; + }; + return is_youtube_video_id(video_id) && segments.next().is_none(); + } + + if host == "youtube.com" || host == "www.youtube.com" { + if parsed_url.path() != "/watch" { + return false; + } + let mut video_ids = parsed_url + .query_pairs() + .filter(|(key, _)| key == "v") + .map(|(_, value)| value); + return match (video_ids.next(), video_ids.next()) { + (Some(video_id), None) => is_youtube_video_id(video_id.as_ref()), + _ => false, + }; + } + + false +} + +pub fn youtube_missing_metadata_error(_parsed: &Value) -> String { + "YouTube import reported ok but missing metadata.".to_string() +} + +pub fn wait_for_process_output( + mut command: Command, + timeout: Duration, + poll_interval: Duration, + timeout_message: &str, +) -> Result { + let mut child = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|_| "Failed to start YouTube import process.".to_string())?; + let Some(stdout) = child.stdout.take() else { + let _ = child.kill(); + let _ = child.wait(); + return Err("Failed to execute YouTube import process.".to_string()); + }; + let Some(stderr) = child.stderr.take() else { + let _ = child.kill(); + let _ = child.wait(); + return Err("Failed to execute YouTube import process.".to_string()); + }; + let stdout_reader = thread::spawn(move || { + let mut reader = stdout; + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).map(|_| buffer) + }); + let stderr_reader = thread::spawn(move || { + let mut reader = stderr; + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).map(|_| buffer) + }); + let deadline = Instant::now() + timeout; + + loop { + match child.try_wait() { + Ok(Some(status)) => { + let stdout = stdout_reader + .join() + .map_err(|_| "Failed to execute YouTube import process.".to_string())? + .map_err(|_| "Failed to execute YouTube import process.".to_string())?; + let stderr = stderr_reader + .join() + .map_err(|_| "Failed to execute YouTube import process.".to_string())? + .map_err(|_| "Failed to execute YouTube import process.".to_string())?; + return Ok(std::process::Output { + status, + stdout, + stderr, + }); + } + Ok(None) => { + if Instant::now() >= deadline { + let _ = child.kill(); + let _ = child.wait(); + let _ = stdout_reader.join(); + let _ = stderr_reader.join(); + return Err(timeout_message.to_string()); + } + thread::sleep(poll_interval); + } + Err(_) => { + let _ = child.kill(); + let _ = child.wait(); + let _ = stdout_reader.join(); + let _ = stderr_reader.join(); + return Err("Failed to execute YouTube import process.".to_string()); + } + } + } +} + +pub fn is_youtube_video_id(value: &str) -> bool { + value.len() == 11 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-') +} + +pub fn project_payload_from_content(content: &str) -> Result { + if let Ok(parsed) = serde_json::from_str::(content) { + return Ok(parsed); + } + + let payload = serde_json::from_str::(content) + .map_err(|_| "Invalid project file format".to_string())?; + if let Some(sections) = payload.get("sections").and_then(Value::as_array) { + for (section_index, section) in sections.iter().enumerate() { + if section + .as_object() + .is_some_and(|section_object| !section_object.contains_key("timeRange")) + { + return Err(format!( + "Invalid project file format: sections[{section_index}].timeRange is required; reanalyze the project to restore section timing." + )); + } + } + } + + serde_json::from_value(payload).map_err(|_| "Invalid project file format".to_string()) +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScoreAttachmentPayload { + pub score_id: String, + pub file_name: String, + pub file_size_bytes: u64, +} + +/// Security Notes: project ids never come from free-form user input. They are +/// only ever minted by `next_project_id` as `project--`, so +/// anything from the WebView that does not match that exact shape is rejected +/// before it can influence a filesystem path (no separators, no `..`). +pub fn is_valid_project_id(value: &str) -> bool { + let Some(rest) = value.strip_prefix("project-") else { + return false; + }; + let mut segments = rest.split('-'); + match (segments.next(), segments.next(), segments.next()) { + (Some(timestamp), Some(counter), None) => { + !timestamp.is_empty() + && !counter.is_empty() + && timestamp.bytes().all(|byte| byte.is_ascii_digit()) + && counter.bytes().all(|byte| byte.is_ascii_digit()) + } + _ => false, + } +} + +/// Security Notes: score ids are minted locally via UUID v4 and must round-trip +/// as exactly a lowercase hyphenated UUID (8-4-4-4-12). This is an allowlist +/// check, so path traversal payloads (`..`, separators, null bytes) can never +/// reach the path join below. +pub fn is_valid_score_id(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.len() != 36 { + return false; + } + bytes.iter().enumerate().all(|(index, byte)| match index { + 8 | 13 | 18 | 23 => *byte == b'-', + _ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'), + }) +} + +/// Security Notes: the selected file is untrusted input (`User Input Boundary`). +/// We refuse symlinks before canonicalizing, require a real non-empty regular +/// file with a `.pdf` extension, cap the size at 25MB, and verify the `%PDF-` +/// magic bytes so a mislabeled file cannot be attached as a score. +pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), String> { + let link_metadata = std::fs::symlink_metadata(path) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if link_metadata.file_type().is_symlink() { + return Err("Could not read the selected PDF file.".to_string()); + } + + let canonical = path + .canonicalize() + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + let extension = canonical + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .ok_or_else(|| "Choose a PDF file to attach as a score.".to_string())?; + if extension != "pdf" { + return Err("Choose a PDF file to attach as a score.".into()); + } + + let metadata = std::fs::metadata(&canonical) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if !metadata.is_file() || metadata.len() == 0 { + return Err("Could not read the selected PDF file.".into()); + } + if metadata.len() > MAX_SCORE_PDF_BYTES { + return Err("Score PDF is too large (exceeds 25MB limit).".into()); + } + + let mut header = [0u8; PDF_MAGIC.len()]; + std::fs::File::open(&canonical) + .and_then(|mut file| file.read_exact(&mut header)) + .map_err(|_| "Could not read the selected PDF file.".to_string())?; + if header != PDF_MAGIC { + return Err("The selected file is not a valid PDF.".into()); + } + + let file_name = canonical + .file_name() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()) + .ok_or_else(|| "Could not read the selected PDF file.".to_string())?; + + let file_size_bytes = metadata.len(); + Ok((canonical, file_name, file_size_bytes)) +} + +/// Security Notes: reads and deletes never accept an arbitrary path from the +/// WebView. The path is rebuilt server-side from validated ids, symlinks are +/// refused, and the canonicalized result must still live under the +/// canonicalized app-owned scores root (path-traversal guard). +pub fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result { + if !is_valid_score_id(score_id) { + return Err("Score was not found.".to_string()); + } + let candidate = scores_root.join(format!("{score_id}.pdf")); + let link_metadata = + std::fs::symlink_metadata(&candidate).map_err(|_| "Score was not found.".to_string())?; + if link_metadata.file_type().is_symlink() { + return Err("Score was not found.".to_string()); + } + + let canonical_root = scores_root + .canonicalize() + .map_err(|_| "Score was not found.".to_string())?; + let canonical = candidate + .canonicalize() + .map_err(|_| "Score was not found.".to_string())?; + if !canonical.starts_with(&canonical_root) { + return Err("Score was not found.".to_string()); + } + + let metadata = std::fs::metadata(&canonical).map_err(|_| "Score was not found.".to_string())?; + if !metadata.is_file() { + return Err("Score was not found.".to_string()); + } + Ok(canonical) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::io::Write; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn unique_test_dir(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!("bandscope-{name}-{suffix}")) + } + + fn shared_contract_payload(time_range: Value) -> Value { + json!({ + "id": "demo-song", + "title": "Late Night Set", + "sections": [ + { + "id": "verse-1", + "label": "verse", + "groove": "Straight eighths with a late snare feel", + "timeRange": time_range, + "confidence": { + "level": "medium", + "source": "model", + "notes": "Double-check the pickup into the chorus." + }, + "roles": [ + { + "id": "bass-guitar", + "name": "Bass Guitar", + "roleType": "instrument", + "harmony": { + "chord": "C#m7", + "functionLabel": "vi pedal anchor", + "source": "model" + }, + "cue": { + "kind": "transition", + "value": "Hold through the pickup before the downbeat." + }, + "range": { + "lowestNote": "C#2", + "highestNote": "E3" + }, + "confidence": { + "level": "medium", + "source": "model", + "notes": "Watch the slide into the turnaround." + }, + "rehearsalPriority": "high", + "simplification": "Stay on roots if the chorus entrance gets muddy.", + "setupNote": "Keep the attack short so the verse breathes.", + "manualOverrides": [], + "overlapWarnings": [ + "Density warning: competing with Keyboard Left Hand in low register." + ] + } + ], + "partGraph": [ + { + "role_id": "bass-guitar", + "is_active": true, + "handoff_to": ["lead-vocal"], + "handoff_from": [] + } + ] + } + ], + "exportSummary": { + "format": "cue-sheet", + "headline": "Start with the verse handoff and low-register overlap.", + "focusSections": ["verse-1"] + } + }) + } + + #[test] + fn rehearsal_song_payload_accepts_shared_section_contract() { + let payload = shared_contract_payload(json!({ "start": 10, "end": 30 })); + + let parsed = serde_json::from_value::(payload) + .expect("shared rehearsal song contract should deserialize in Tauri"); + + assert_eq!(parsed.sections[0].id, "verse-1"); + } + + #[test] + fn rehearsal_song_payload_rejects_reversed_time_range() { + let payload = shared_contract_payload(json!({ "start": 30, "end": 10 })); + + assert!(serde_json::from_value::(payload).is_err()); + } + + #[test] + fn project_payload_from_content_rejects_legacy_missing_time_range() { + let mut payload = shared_contract_payload(json!({ "start": 10, "end": 30 })); + payload["sections"][0] + .as_object_mut() + .expect("section should be an object") + .remove("timeRange"); + let content = serde_json::to_string(&payload).expect("legacy payload should serialize"); + + let error = project_payload_from_content(&content) + .expect_err("legacy sections without timing should fail closed"); + + assert!(error.contains("timeRange")); + } + + #[test] + fn youtube_url_validation_requires_exact_video_ids() { + assert!(is_supported_youtube_url( + "https://youtube.com/watch?v=abc123DEF45" + )); + assert!(is_supported_youtube_url( + "https://www.youtube.com/watch?v=abc123DEF45" + )); + assert!(is_supported_youtube_url("https://youtu.be/abc123DEF45")); + + assert!(!is_supported_youtube_url( + "https://evil.youtube.com/watch?v=abc123DEF45" + )); + assert!(!is_supported_youtube_url( + "https://youtube.com/watch?v=abc123" + )); + assert!(!is_supported_youtube_url( + "https://youtube.com/watch?v=abc123DEF4!" + )); + assert!(!is_supported_youtube_url("https://youtube.com/watch")); + assert!(!is_supported_youtube_url( + "https://youtube.com/watch?v=abc123DEF45&v=def456GHI78" + )); + assert!(!is_supported_youtube_url("https://youtu.be/abc123")); + assert!(!is_supported_youtube_url("https://youtu.be/abc123DEF4!")); + } + + #[test] + fn youtube_missing_metadata_error_does_not_expose_payload() { + let parsed = json!({ + "ok": true, + "filepath": "/Users/someone/private-song.m4a", + "metadata": null + }); + + let message = youtube_missing_metadata_error(&parsed); + + assert_eq!(message, "YouTube import reported ok but missing metadata."); + assert!(!message.contains("private-song")); + assert!(!message.contains("filepath")); + } + + #[test] + fn youtube_process_timeout_kills_and_reaps_child() { + if std::env::var_os("BANDSCOPE_TEST_CHILD_SLEEP").is_some() { + thread::sleep(Duration::from_secs(5)); + return; + } + + let current_test_binary = std::env::current_exe().expect("test binary should resolve"); + let mut command = Command::new(current_test_binary); + command + .env("BANDSCOPE_TEST_CHILD_SLEEP", "1") + .arg("--exact") + .arg("tests::youtube_process_timeout_kills_and_reaps_child") + .arg("--nocapture"); + + let result = wait_for_process_output( + command, + Duration::from_millis(50), + Duration::from_millis(5), + "YouTube import timed out.", + ); + + assert_eq!( + result.expect_err("slow child should time out"), + "YouTube import timed out." + ); + } + + #[test] + fn youtube_process_output_drains_large_stdout_and_stderr_before_exit() { + if std::env::var_os("BANDSCOPE_TEST_CHILD_LARGE_OUTPUT").is_some() { + let chunk = vec![b'x'; 1024 * 1024]; + std::io::stdout() + .write_all(&chunk) + .expect("child stdout should accept test bytes"); + std::io::stderr() + .write_all(&chunk) + .expect("child stderr should accept test bytes"); + return; + } + + let current_test_binary = std::env::current_exe().expect("test binary should resolve"); + let mut command = Command::new(current_test_binary); + command + .env("BANDSCOPE_TEST_CHILD_LARGE_OUTPUT", "1") + .arg("--exact") + .arg("tests::youtube_process_output_drains_large_stdout_and_stderr_before_exit") + .arg("--nocapture"); + + let output = wait_for_process_output( + command, + Duration::from_secs(2), + Duration::from_millis(5), + "YouTube import timed out.", + ) + .expect("large child output should be drained before timeout"); + + assert!(output.status.success()); + assert!(output.stdout.len() >= 1024 * 1024); + assert!(output.stderr.len() >= 1024 * 1024); + } + + #[test] + fn youtube_metadata_must_reference_supported_audio_inside_cache_root() { + let cache_root = unique_test_dir("youtube-cache"); + let outside_root = unique_test_dir("youtube-outside"); + std::fs::create_dir_all(&cache_root).expect("cache root should be created"); + std::fs::create_dir_all(&outside_root).expect("outside root should be created"); + + let inside_file = cache_root.join("downloaded.m4a"); + let empty_file = cache_root.join("empty.m4a"); + let unsupported_file = cache_root.join("downloaded.txt"); + let outside_file = outside_root.join("downloaded.m4a"); + std::fs::write(&inside_file, b"audio").expect("inside file should be written"); + std::fs::write(&empty_file, b"").expect("empty file should be written"); + std::fs::write(&unsupported_file, b"not audio") + .expect("unsupported file should be written"); + std::fs::write(&outside_file, b"audio").expect("outside file should be written"); + + let accepted = youtube_source_from_metadata( + &json!({ "filepath": inside_file, "title": "Live/Test" }), + &cache_root, + ) + .expect("in-cache supported audio should be accepted"); + assert_eq!(accepted.extension, "m4a"); + assert_eq!(accepted.file_name, "Live_Test.m4a"); + + assert!(youtube_source_from_metadata( + &json!({ "filepath": empty_file, "title": "Live" }), + &cache_root, + ) + .is_err()); + assert!(youtube_source_from_metadata( + &json!({ "filepath": unsupported_file, "title": "Live" }), + &cache_root, + ) + .is_err()); + assert!(youtube_source_from_metadata( + &json!({ "filepath": outside_file, "title": "Live" }), + &cache_root, + ) + .is_err()); + + #[cfg(unix)] + { + let symlink_file = cache_root.join("linked.m4a"); + std::os::unix::fs::symlink(&inside_file, &symlink_file) + .expect("symlink should be created"); + assert!(youtube_source_from_metadata( + &json!({ "filepath": symlink_file, "title": "Live" }), + &cache_root, + ) + .is_err()); + } + + let _ = std::fs::remove_dir_all(cache_root); + let _ = std::fs::remove_dir_all(outside_root); + } + + #[test] + fn project_id_guard_accepts_generated_ids_only() { + let generated = next_project_id(&AppState::default()); + assert!(is_valid_project_id(&generated)); + assert!(is_valid_project_id("project-1751234567890123456-1")); + + assert!(!is_valid_project_id("")); + assert!(!is_valid_project_id("project-")); + assert!(!is_valid_project_id("project-123")); + assert!(!is_valid_project_id("project-123-")); + assert!(!is_valid_project_id("project-123-4-5")); + assert!(!is_valid_project_id("project-abc-1")); + assert!(!is_valid_project_id("project-123-1x")); + assert!(!is_valid_project_id("other-123-1")); + assert!(!is_valid_project_id("../project-123-1")); + assert!(!is_valid_project_id("project-123-1/..")); + assert!(!is_valid_project_id("project-..-1")); + assert!(!is_valid_project_id("project-123-1/escape")); + } + + #[test] + fn score_id_guard_accepts_lowercase_uuid_v4_only() { + let generated = uuid::Uuid::new_v4().to_string(); + assert!(is_valid_score_id(&generated)); + assert!(is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e160355e")); + + assert!(!is_valid_score_id("")); + assert!(!is_valid_score_id("not-a-uuid")); + assert!(!is_valid_score_id("6FA459EA-EE8A-3CA4-894E-DB77E160355E")); + assert!(!is_valid_score_id("6fa459eaee8a3ca4894edb77e160355e")); + assert!(!is_valid_score_id("{6fa459ea-ee8a-3ca4-894e-db77e160355e}")); + assert!(!is_valid_score_id("../../../../etc/passwd-aaaa-bbbb-cc")); + assert!(!is_valid_score_id( + "6fa459ea-ee8a-3ca4-894e-db77e160355e/.." + )); + assert!(!is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e16035/e")); + } + + #[test] + fn score_pdf_source_requires_pdf_magic_size_and_real_file() { + let root = unique_test_dir("score-source"); + std::fs::create_dir_all(&root).expect("score source root should be created"); + + let valid = root.join("score.pdf"); + std::fs::write(&valid, b"%PDF-1.7 fake body").expect("valid pdf should be written"); + let (canonical, file_name, size) = + validate_score_pdf_source(&valid).expect("valid pdf should be accepted"); + assert_eq!(file_name, "score.pdf"); + assert_eq!(size, 18); + assert!(canonical.ends_with("score.pdf")); + + let wrong_magic = root.join("not-really.pdf"); + std::fs::write(&wrong_magic, b"PK\x03\x04 zip bytes") + .expect("wrong magic file should be written"); + assert!(validate_score_pdf_source(&wrong_magic).is_err()); + + let short = root.join("short.pdf"); + std::fs::write(&short, b"%PD").expect("short file should be written"); + assert!(validate_score_pdf_source(&short).is_err()); + + let empty = root.join("empty.pdf"); + std::fs::write(&empty, b"").expect("empty file should be written"); + assert!(validate_score_pdf_source(&empty).is_err()); + + let wrong_extension = root.join("score.txt"); + std::fs::write(&wrong_extension, b"%PDF-1.7").expect("txt file should be written"); + assert!(validate_score_pdf_source(&wrong_extension).is_err()); + + let missing = root.join("missing.pdf"); + assert!(validate_score_pdf_source(&missing).is_err()); + + let oversized = root.join("oversized.pdf"); + { + let file = std::fs::File::create(&oversized).expect("oversized file should be created"); + let mut file = file; + file.write_all(b"%PDF-1.7") + .expect("oversized header should be written"); + file.set_len(MAX_SCORE_PDF_BYTES + 1) + .expect("oversized file should be extended"); + } + assert!(validate_score_pdf_source(&oversized).is_err()); + + #[cfg(unix)] + { + let symlinked = root.join("linked.pdf"); + std::os::unix::fs::symlink(&valid, &symlinked).expect("symlink should be created"); + assert!(validate_score_pdf_source(&symlinked).is_err()); + } + + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn score_pdf_resolution_rejects_traversal_and_escapes() { + let scores_root = unique_test_dir("score-resolve"); + let outside_root = unique_test_dir("score-outside"); + std::fs::create_dir_all(&scores_root).expect("scores root should be created"); + std::fs::create_dir_all(&outside_root).expect("outside root should be created"); + + let score_id = "6fa459ea-ee8a-3ca4-894e-db77e160355e"; + let inside_file = scores_root.join(format!("{score_id}.pdf")); + std::fs::write(&inside_file, b"%PDF-1.7").expect("inside file should be written"); + + let resolved = resolve_existing_score_pdf(&scores_root, score_id) + .expect("stored score inside the root should resolve"); + assert!(resolved.ends_with(format!("{score_id}.pdf"))); + + assert!(resolve_existing_score_pdf(&scores_root, "../escape").is_err()); + assert!(resolve_existing_score_pdf(&scores_root, "..").is_err()); + assert!( + resolve_existing_score_pdf(&scores_root, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + .is_err() + ); + + #[cfg(unix)] + { + let outside_file = outside_root.join("secret.pdf"); + std::fs::write(&outside_file, b"%PDF-1.7").expect("outside file should be written"); + let linked_id = "11111111-2222-3333-4444-555555555555"; + std::os::unix::fs::symlink(&outside_file, scores_root.join(format!("{linked_id}.pdf"))) + .expect("symlink should be created"); + assert!(resolve_existing_score_pdf(&scores_root, linked_id).is_err()); + } + + let _ = std::fs::remove_dir_all(scores_root); + let _ = std::fs::remove_dir_all(outside_root); + } +} diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 9dbf7bf1..cf90c103 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -71,6 +71,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" name = "bandscope-desktop" version = "0.1.0" dependencies = [ + "bandscope-desktop-core", "rfd", "serde", "serde_json", @@ -82,6 +83,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "bandscope-desktop-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "time", + "url", +] + [[package]] name = "base64" version = "0.21.7" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index f0369488..1b25fed4 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" tauri-build = { version = "2", default-features = false, features = [] } [dependencies] +bandscope-desktop-core = { path = "../core" } rfd = "0.17.2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 78d29ae4..c3e6cec6 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,276 +1,19 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use bandscope_desktop_core::*; use rfd::FileDialog; -use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{json, Value}; use std::{ - collections::HashMap, io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::{ - atomic::{AtomicU64, AtomicUsize, Ordering}, - mpsc, Arc, Mutex, - }, + sync::{atomic::Ordering, mpsc}, thread, - time::{Duration, Instant}, + time::Instant, }; use tauri::{Emitter, Manager, Runtime}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; -#[derive(Clone)] -struct AppState(Arc); - -struct AppStateInner { - next_job: AtomicU64, - in_flight_jobs: AtomicUsize, - jobs: Mutex>, - bootstrap_sources: Mutex>, -} - -const MAX_IN_FLIGHT_JOBS: usize = 2; -const ANALYSIS_PROCESS_TIMEOUT: Duration = Duration::from_secs(30); -const ANALYSIS_WAIT_POLL: Duration = Duration::from_millis(50); -const AUDIO_EXTENSIONS: [&str; 4] = ["wav", "mp3", "flac", "m4a"]; -const MISSING_ANALYSIS_PYTHON: &str = "__bandscope_missing_analysis_python__"; -const YOUTUBE_IMPORT_TIMEOUT: Duration = Duration::from_secs(120); -const MAX_SCORE_PDF_BYTES: u64 = 25 * 1024 * 1024; -const PDF_MAGIC: &[u8] = b"%PDF-"; - -impl Default for AppState { - fn default() -> Self { - Self(Arc::new(AppStateInner { - next_job: AtomicU64::new(1), - in_flight_jobs: AtomicUsize::new(0), - jobs: Mutex::new(HashMap::new()), - bootstrap_sources: Mutex::new(HashMap::new()), - })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct AnalysisJobRequest { - source_kind: String, - project_id: Option, - source_label: String, - role_focus: Vec, - local_source: Option, - cache_root: Option, - temp_root: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -enum AnalysisJobErrorCode { - InvalidRequest, - NotFound, - EngineUnavailable, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct AnalysisJobError { - code: AnalysisJobErrorCode, - message: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -enum AnalysisJobState { - Queued, - Running, - Succeeded, - Failed, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -enum AnalysisJobStage { - Queued, - Decode, - Separate, - Analyze, - Persist, - Ready, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -enum AnalysisCacheStatus { - Disabled, - Miss, - Hit, - Stored, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RehearsalSongPayload { - id: String, - title: String, - sections: Vec, - export_summary: ExportSummaryPayload, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ConfidencePayload { - level: String, - source: String, - notes: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct CuePayload { - kind: String, - value: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RangePayload { - lowest_note: String, - highest_note: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct HarmonyPayload { - chord: String, - function_label: String, - source: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ManualOverridePayload { - field: String, - value: HarmonyPayload, - source: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RehearsalRolePayload { - id: String, - name: String, - role_type: String, - harmony: HarmonyPayload, - cue: CuePayload, - range: RangePayload, - confidence: ConfidencePayload, - rehearsal_priority: String, - simplification: String, - setup_note: String, - manual_overrides: Vec, - overlap_warnings: Vec, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct SectionTimeRangePayload { - start: u32, - end: u32, -} - -impl<'de> Deserialize<'de> for SectionTimeRangePayload { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase", deny_unknown_fields)] - struct RawSectionTimeRangePayload { - start: u32, - end: u32, - } - - let raw = RawSectionTimeRangePayload::deserialize(deserializer)?; - if raw.end <= raw.start { - return Err(serde::de::Error::custom( - "section timeRange end must be greater than start", - )); - } - - Ok(Self { - start: raw.start, - end: raw.end, - }) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -struct PartGraphNodePayload { - role_id: String, - is_active: bool, - handoff_to: Vec, - handoff_from: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RehearsalSectionPayload { - id: String, - label: String, - groove: String, - time_range: SectionTimeRangePayload, - confidence: ConfidencePayload, - roles: Vec, - part_graph: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ExportSummaryPayload { - format: String, - headline: String, - focus_sections: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct AnalysisJobStatus { - job_id: String, - state: AnalysisJobState, - requested_at: String, - updated_at: String, - #[serde(skip_serializing_if = "Option::is_none")] - progress_label: Option, - #[serde(skip_serializing_if = "Option::is_none")] - progress_stage: Option, - #[serde(skip_serializing_if = "Option::is_none")] - progress_percent: Option, - #[serde(skip_serializing_if = "Option::is_none")] - cache_status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - result: Option, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct LocalAudioSourcePayload { - source_path: String, - file_name: String, - extension: String, - file_size_bytes: u64, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ProjectBootstrapSummaryPayload { - project_id: String, - source_mode: String, - project_root: String, - cache_root: String, - temp_root: String, - source: LocalAudioSourcePayload, -} - fn iso_timestamp_now() -> String { OffsetDateTime::now_utc() .format(&Rfc3339) @@ -367,14 +110,6 @@ fn release_job_slot(state: &AppState) { state.0.in_flight_jobs.fetch_sub(1, Ordering::SeqCst); } -fn next_project_id(state: &AppState) -> String { - format!( - "project-{}-{}", - OffsetDateTime::now_utc().unix_timestamp_nanos(), - state.0.next_job.fetch_add(1, Ordering::Relaxed) - ) -} - fn app_owned_root( app: &tauri::AppHandle, kind: &str, @@ -432,74 +167,6 @@ fn normalize_local_audio_source(path: &Path) -> Result Result { - let filepath = metadata - .get("filepath") - .and_then(|value| value.as_str()) - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| "Failed to parse YouTube import response.".to_string())?; - let title = metadata - .get("title") - .and_then(|value| value.as_str()) - .unwrap_or("Unknown YouTube Audio"); - let path = Path::new(filepath); - let link_metadata = std::fs::symlink_metadata(path) - .map_err(|_| "Could not read downloaded audio file.".to_string())?; - if link_metadata.file_type().is_symlink() { - return Err("YouTube import returned an invalid audio path.".to_string()); - } - - let canonical_cache_root = cache_root - .canonicalize() - .map_err(|_| "Could not validate YouTube import workspace.".to_string())?; - let canonical = path - .canonicalize() - .map_err(|_| "Could not read downloaded audio file.".to_string())?; - if !canonical.starts_with(&canonical_cache_root) { - return Err("YouTube import returned an invalid audio path.".to_string()); - } - - let file_metadata = std::fs::metadata(&canonical) - .map_err(|_| "Could not read downloaded audio file.".to_string())?; - if !file_metadata.is_file() || file_metadata.len() == 0 { - return Err("YouTube import returned an invalid audio file.".to_string()); - } - - let extension = canonical - .extension() - .and_then(|value| value.to_str()) - .map(|value| value.to_ascii_lowercase()) - .ok_or_else(|| "YouTube import returned an unsupported audio format.".to_string())?; - if !AUDIO_EXTENSIONS.contains(&extension.as_str()) { - return Err("YouTube import returned an unsupported audio format.".to_string()); - } - - let safe_title: String = title - .chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '.' => '_', - c if c.is_control() => '_', - c => c, - }) - .take(100) - .collect(); - let safe_title = if safe_title.is_empty() { - "youtube_audio".to_string() - } else { - safe_title - }; - - Ok(LocalAudioSourcePayload { - source_path: canonical.to_string_lossy().into_owned(), - file_name: format!("{safe_title}.{extension}"), - extension, - file_size_bytes: file_metadata.len(), - }) -} - fn parse_request_payload(payload: Value) -> Result { let Value::Object(map) = payload else { return Err("Invalid analysis job request: invalid field 'root'".into()); @@ -1058,150 +725,6 @@ async fn import_youtube_url( Err("YouTube import failed with an unknown error.".to_string()) } -fn is_supported_youtube_url(url: &str) -> bool { - let parsed_url = match url::Url::parse(url) { - Ok(u) => u, - Err(_) => return false, - }; - if parsed_url.scheme() != "https" { - return false; - } - - let host = parsed_url.host_str().unwrap_or("").to_lowercase(); - if host == "youtu.be" { - let mut segments = match parsed_url.path_segments() { - Some(s) => s.filter(|segment| !segment.is_empty()), - None => return false, - }; - let Some(video_id) = segments.next() else { - return false; - }; - return is_youtube_video_id(video_id) && segments.next().is_none(); - } - - if host == "youtube.com" || host == "www.youtube.com" { - if parsed_url.path() != "/watch" { - return false; - } - let mut video_ids = parsed_url - .query_pairs() - .filter(|(key, _)| key == "v") - .map(|(_, value)| value); - return match (video_ids.next(), video_ids.next()) { - (Some(video_id), None) => is_youtube_video_id(video_id.as_ref()), - _ => false, - }; - } - - false -} - -fn youtube_missing_metadata_error(_parsed: &Value) -> String { - "YouTube import reported ok but missing metadata.".to_string() -} - -fn wait_for_process_output( - mut command: Command, - timeout: Duration, - poll_interval: Duration, - timeout_message: &str, -) -> Result { - let mut child = command - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|_| "Failed to start YouTube import process.".to_string())?; - let Some(stdout) = child.stdout.take() else { - let _ = child.kill(); - let _ = child.wait(); - return Err("Failed to execute YouTube import process.".to_string()); - }; - let Some(stderr) = child.stderr.take() else { - let _ = child.kill(); - let _ = child.wait(); - return Err("Failed to execute YouTube import process.".to_string()); - }; - let stdout_reader = thread::spawn(move || { - let mut reader = stdout; - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer).map(|_| buffer) - }); - let stderr_reader = thread::spawn(move || { - let mut reader = stderr; - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer).map(|_| buffer) - }); - let deadline = Instant::now() + timeout; - - loop { - match child.try_wait() { - Ok(Some(status)) => { - let stdout = stdout_reader - .join() - .map_err(|_| "Failed to execute YouTube import process.".to_string())? - .map_err(|_| "Failed to execute YouTube import process.".to_string())?; - let stderr = stderr_reader - .join() - .map_err(|_| "Failed to execute YouTube import process.".to_string())? - .map_err(|_| "Failed to execute YouTube import process.".to_string())?; - return Ok(std::process::Output { - status, - stdout, - stderr, - }); - } - Ok(None) => { - if Instant::now() >= deadline { - let _ = child.kill(); - let _ = child.wait(); - let _ = stdout_reader.join(); - let _ = stderr_reader.join(); - return Err(timeout_message.to_string()); - } - thread::sleep(poll_interval); - } - Err(_) => { - let _ = child.kill(); - let _ = child.wait(); - let _ = stdout_reader.join(); - let _ = stderr_reader.join(); - return Err("Failed to execute YouTube import process.".to_string()); - } - } - } -} - -fn is_youtube_video_id(value: &str) -> bool { - value.len() == 11 - && value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-') -} - -fn project_payload_from_content(content: &str) -> Result { - if let Ok(parsed) = serde_json::from_str::(content) { - return Ok(parsed); - } - - let payload = serde_json::from_str::(content) - .map_err(|_| "Invalid project file format".to_string())?; - if let Some(sections) = payload.get("sections").and_then(Value::as_array) { - for (section_index, section) in sections.iter().enumerate() { - if section - .as_object() - .is_some_and(|section_object| !section_object.contains_key("timeRange")) - { - return Err(format!( - "Invalid project file format: sections[{section_index}].timeRange is required; reanalyze the project to restore section timing." - )); - } - } - } - - serde_json::from_value(payload).map_err(|_| "Invalid project file format".to_string()) -} - #[tauri::command] fn save_project(payload: Value) -> Result<(), String> { let parsed = serde_json::from_value::(payload) @@ -1235,131 +758,6 @@ fn load_project() -> Result { project_payload_from_content(&content) } -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ScoreAttachmentPayload { - score_id: String, - file_name: String, - file_size_bytes: u64, -} - -/// Security Notes: project ids never come from free-form user input. They are -/// only ever minted by `next_project_id` as `project--`, so -/// anything from the WebView that does not match that exact shape is rejected -/// before it can influence a filesystem path (no separators, no `..`). -fn is_valid_project_id(value: &str) -> bool { - let Some(rest) = value.strip_prefix("project-") else { - return false; - }; - let mut segments = rest.split('-'); - match (segments.next(), segments.next(), segments.next()) { - (Some(timestamp), Some(counter), None) => { - !timestamp.is_empty() - && !counter.is_empty() - && timestamp.bytes().all(|byte| byte.is_ascii_digit()) - && counter.bytes().all(|byte| byte.is_ascii_digit()) - } - _ => false, - } -} - -/// Security Notes: score ids are minted locally via UUID v4 and must round-trip -/// as exactly a lowercase hyphenated UUID (8-4-4-4-12). This is an allowlist -/// check, so path traversal payloads (`..`, separators, null bytes) can never -/// reach the path join below. -fn is_valid_score_id(value: &str) -> bool { - let bytes = value.as_bytes(); - if bytes.len() != 36 { - return false; - } - bytes.iter().enumerate().all(|(index, byte)| match index { - 8 | 13 | 18 | 23 => *byte == b'-', - _ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'), - }) -} - -/// Security Notes: the selected file is untrusted input (`User Input Boundary`). -/// We refuse symlinks before canonicalizing, require a real non-empty regular -/// file with a `.pdf` extension, cap the size at 25MB, and verify the `%PDF-` -/// magic bytes so a mislabeled file cannot be attached as a score. -fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), String> { - let link_metadata = std::fs::symlink_metadata(path) - .map_err(|_| "Could not read the selected PDF file.".to_string())?; - if link_metadata.file_type().is_symlink() { - return Err("Could not read the selected PDF file.".to_string()); - } - - let canonical = path - .canonicalize() - .map_err(|_| "Could not read the selected PDF file.".to_string())?; - let extension = canonical - .extension() - .and_then(|value| value.to_str()) - .map(|value| value.to_ascii_lowercase()) - .ok_or_else(|| "Choose a PDF file to attach as a score.".to_string())?; - if extension != "pdf" { - return Err("Choose a PDF file to attach as a score.".into()); - } - - let metadata = std::fs::metadata(&canonical) - .map_err(|_| "Could not read the selected PDF file.".to_string())?; - if !metadata.is_file() || metadata.len() == 0 { - return Err("Could not read the selected PDF file.".into()); - } - if metadata.len() > MAX_SCORE_PDF_BYTES { - return Err("Score PDF is too large (exceeds 25MB limit).".into()); - } - - let mut header = [0u8; PDF_MAGIC.len()]; - std::fs::File::open(&canonical) - .and_then(|mut file| file.read_exact(&mut header)) - .map_err(|_| "Could not read the selected PDF file.".to_string())?; - if header != PDF_MAGIC { - return Err("The selected file is not a valid PDF.".into()); - } - - let file_name = canonical - .file_name() - .and_then(|value| value.to_str()) - .map(|value| value.to_string()) - .ok_or_else(|| "Could not read the selected PDF file.".to_string())?; - - let file_size_bytes = metadata.len(); - Ok((canonical, file_name, file_size_bytes)) -} - -/// Security Notes: reads and deletes never accept an arbitrary path from the -/// WebView. The path is rebuilt server-side from validated ids, symlinks are -/// refused, and the canonicalized result must still live under the -/// canonicalized app-owned scores root (path-traversal guard). -fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result { - if !is_valid_score_id(score_id) { - return Err("Score was not found.".to_string()); - } - let candidate = scores_root.join(format!("{score_id}.pdf")); - let link_metadata = - std::fs::symlink_metadata(&candidate).map_err(|_| "Score was not found.".to_string())?; - if link_metadata.file_type().is_symlink() { - return Err("Score was not found.".to_string()); - } - - let canonical_root = scores_root - .canonicalize() - .map_err(|_| "Score was not found.".to_string())?; - let canonical = candidate - .canonicalize() - .map_err(|_| "Score was not found.".to_string())?; - if !canonical.starts_with(&canonical_root) { - return Err("Score was not found.".to_string()); - } - - let metadata = std::fs::metadata(&canonical).map_err(|_| "Score was not found.".to_string())?; - if !metadata.is_file() { - return Err("Score was not found.".to_string()); - } - Ok(canonical) -} - fn scores_root_for_project( app: &tauri::AppHandle, project_id: &str, @@ -1454,406 +852,6 @@ fn remove_score_pdf( Ok(true) } -#[cfg(test)] -mod tests { - use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn unique_test_dir(name: &str) -> PathBuf { - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock should be after epoch") - .as_nanos(); - std::env::temp_dir().join(format!("bandscope-{name}-{suffix}")) - } - - fn shared_contract_payload(time_range: Value) -> Value { - json!({ - "id": "demo-song", - "title": "Late Night Set", - "sections": [ - { - "id": "verse-1", - "label": "verse", - "groove": "Straight eighths with a late snare feel", - "timeRange": time_range, - "confidence": { - "level": "medium", - "source": "model", - "notes": "Double-check the pickup into the chorus." - }, - "roles": [ - { - "id": "bass-guitar", - "name": "Bass Guitar", - "roleType": "instrument", - "harmony": { - "chord": "C#m7", - "functionLabel": "vi pedal anchor", - "source": "model" - }, - "cue": { - "kind": "transition", - "value": "Hold through the pickup before the downbeat." - }, - "range": { - "lowestNote": "C#2", - "highestNote": "E3" - }, - "confidence": { - "level": "medium", - "source": "model", - "notes": "Watch the slide into the turnaround." - }, - "rehearsalPriority": "high", - "simplification": "Stay on roots if the chorus entrance gets muddy.", - "setupNote": "Keep the attack short so the verse breathes.", - "manualOverrides": [], - "overlapWarnings": [ - "Density warning: competing with Keyboard Left Hand in low register." - ] - } - ], - "partGraph": [ - { - "role_id": "bass-guitar", - "is_active": true, - "handoff_to": ["lead-vocal"], - "handoff_from": [] - } - ] - } - ], - "exportSummary": { - "format": "cue-sheet", - "headline": "Start with the verse handoff and low-register overlap.", - "focusSections": ["verse-1"] - } - }) - } - - #[test] - fn rehearsal_song_payload_accepts_shared_section_contract() { - let payload = shared_contract_payload(json!({ "start": 10, "end": 30 })); - - let parsed = serde_json::from_value::(payload) - .expect("shared rehearsal song contract should deserialize in Tauri"); - - assert_eq!(parsed.sections[0].id, "verse-1"); - } - - #[test] - fn rehearsal_song_payload_rejects_reversed_time_range() { - let payload = shared_contract_payload(json!({ "start": 30, "end": 10 })); - - assert!(serde_json::from_value::(payload).is_err()); - } - - #[test] - fn project_payload_from_content_rejects_legacy_missing_time_range() { - let mut payload = shared_contract_payload(json!({ "start": 10, "end": 30 })); - payload["sections"][0] - .as_object_mut() - .expect("section should be an object") - .remove("timeRange"); - let content = serde_json::to_string(&payload).expect("legacy payload should serialize"); - - let error = project_payload_from_content(&content) - .expect_err("legacy sections without timing should fail closed"); - - assert!(error.contains("timeRange")); - } - - #[test] - fn youtube_url_validation_requires_exact_video_ids() { - assert!(is_supported_youtube_url( - "https://youtube.com/watch?v=abc123DEF45" - )); - assert!(is_supported_youtube_url( - "https://www.youtube.com/watch?v=abc123DEF45" - )); - assert!(is_supported_youtube_url("https://youtu.be/abc123DEF45")); - - assert!(!is_supported_youtube_url( - "https://evil.youtube.com/watch?v=abc123DEF45" - )); - assert!(!is_supported_youtube_url( - "https://youtube.com/watch?v=abc123" - )); - assert!(!is_supported_youtube_url( - "https://youtube.com/watch?v=abc123DEF4!" - )); - assert!(!is_supported_youtube_url("https://youtube.com/watch")); - assert!(!is_supported_youtube_url( - "https://youtube.com/watch?v=abc123DEF45&v=def456GHI78" - )); - assert!(!is_supported_youtube_url("https://youtu.be/abc123")); - assert!(!is_supported_youtube_url("https://youtu.be/abc123DEF4!")); - } - - #[test] - fn youtube_missing_metadata_error_does_not_expose_payload() { - let parsed = json!({ - "ok": true, - "filepath": "/Users/someone/private-song.m4a", - "metadata": null - }); - - let message = youtube_missing_metadata_error(&parsed); - - assert_eq!(message, "YouTube import reported ok but missing metadata."); - assert!(!message.contains("private-song")); - assert!(!message.contains("filepath")); - } - - #[test] - fn youtube_process_timeout_kills_and_reaps_child() { - if std::env::var_os("BANDSCOPE_TEST_CHILD_SLEEP").is_some() { - thread::sleep(Duration::from_secs(5)); - return; - } - - let current_test_binary = std::env::current_exe().expect("test binary should resolve"); - let mut command = Command::new(current_test_binary); - command - .env("BANDSCOPE_TEST_CHILD_SLEEP", "1") - .arg("--exact") - .arg("tests::youtube_process_timeout_kills_and_reaps_child") - .arg("--nocapture"); - - let result = wait_for_process_output( - command, - Duration::from_millis(50), - Duration::from_millis(5), - "YouTube import timed out.", - ); - - assert_eq!( - result.expect_err("slow child should time out"), - "YouTube import timed out." - ); - } - - #[test] - fn youtube_process_output_drains_large_stdout_and_stderr_before_exit() { - if std::env::var_os("BANDSCOPE_TEST_CHILD_LARGE_OUTPUT").is_some() { - let chunk = vec![b'x'; 1024 * 1024]; - std::io::stdout() - .write_all(&chunk) - .expect("child stdout should accept test bytes"); - std::io::stderr() - .write_all(&chunk) - .expect("child stderr should accept test bytes"); - return; - } - - let current_test_binary = std::env::current_exe().expect("test binary should resolve"); - let mut command = Command::new(current_test_binary); - command - .env("BANDSCOPE_TEST_CHILD_LARGE_OUTPUT", "1") - .arg("--exact") - .arg("tests::youtube_process_output_drains_large_stdout_and_stderr_before_exit") - .arg("--nocapture"); - - let output = wait_for_process_output( - command, - Duration::from_secs(2), - Duration::from_millis(5), - "YouTube import timed out.", - ) - .expect("large child output should be drained before timeout"); - - assert!(output.status.success()); - assert!(output.stdout.len() >= 1024 * 1024); - assert!(output.stderr.len() >= 1024 * 1024); - } - - #[test] - fn youtube_metadata_must_reference_supported_audio_inside_cache_root() { - let cache_root = unique_test_dir("youtube-cache"); - let outside_root = unique_test_dir("youtube-outside"); - std::fs::create_dir_all(&cache_root).expect("cache root should be created"); - std::fs::create_dir_all(&outside_root).expect("outside root should be created"); - - let inside_file = cache_root.join("downloaded.m4a"); - let empty_file = cache_root.join("empty.m4a"); - let unsupported_file = cache_root.join("downloaded.txt"); - let outside_file = outside_root.join("downloaded.m4a"); - std::fs::write(&inside_file, b"audio").expect("inside file should be written"); - std::fs::write(&empty_file, b"").expect("empty file should be written"); - std::fs::write(&unsupported_file, b"not audio") - .expect("unsupported file should be written"); - std::fs::write(&outside_file, b"audio").expect("outside file should be written"); - - let accepted = youtube_source_from_metadata( - &json!({ "filepath": inside_file, "title": "Live/Test" }), - &cache_root, - ) - .expect("in-cache supported audio should be accepted"); - assert_eq!(accepted.extension, "m4a"); - assert_eq!(accepted.file_name, "Live_Test.m4a"); - - assert!(youtube_source_from_metadata( - &json!({ "filepath": empty_file, "title": "Live" }), - &cache_root, - ) - .is_err()); - assert!(youtube_source_from_metadata( - &json!({ "filepath": unsupported_file, "title": "Live" }), - &cache_root, - ) - .is_err()); - assert!(youtube_source_from_metadata( - &json!({ "filepath": outside_file, "title": "Live" }), - &cache_root, - ) - .is_err()); - - #[cfg(unix)] - { - let symlink_file = cache_root.join("linked.m4a"); - std::os::unix::fs::symlink(&inside_file, &symlink_file) - .expect("symlink should be created"); - assert!(youtube_source_from_metadata( - &json!({ "filepath": symlink_file, "title": "Live" }), - &cache_root, - ) - .is_err()); - } - - let _ = std::fs::remove_dir_all(cache_root); - let _ = std::fs::remove_dir_all(outside_root); - } - - #[test] - fn project_id_guard_accepts_generated_ids_only() { - let generated = next_project_id(&AppState::default()); - assert!(is_valid_project_id(&generated)); - assert!(is_valid_project_id("project-1751234567890123456-1")); - - assert!(!is_valid_project_id("")); - assert!(!is_valid_project_id("project-")); - assert!(!is_valid_project_id("project-123")); - assert!(!is_valid_project_id("project-123-")); - assert!(!is_valid_project_id("project-123-4-5")); - assert!(!is_valid_project_id("project-abc-1")); - assert!(!is_valid_project_id("project-123-1x")); - assert!(!is_valid_project_id("other-123-1")); - assert!(!is_valid_project_id("../project-123-1")); - assert!(!is_valid_project_id("project-123-1/..")); - assert!(!is_valid_project_id("project-..-1")); - assert!(!is_valid_project_id("project-123-1/escape")); - } - - #[test] - fn score_id_guard_accepts_lowercase_uuid_v4_only() { - let generated = uuid::Uuid::new_v4().to_string(); - assert!(is_valid_score_id(&generated)); - assert!(is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e160355e")); - - assert!(!is_valid_score_id("")); - assert!(!is_valid_score_id("not-a-uuid")); - assert!(!is_valid_score_id("6FA459EA-EE8A-3CA4-894E-DB77E160355E")); - assert!(!is_valid_score_id("6fa459eaee8a3ca4894edb77e160355e")); - assert!(!is_valid_score_id("{6fa459ea-ee8a-3ca4-894e-db77e160355e}")); - assert!(!is_valid_score_id("../../../../etc/passwd-aaaa-bbbb-cc")); - assert!(!is_valid_score_id( - "6fa459ea-ee8a-3ca4-894e-db77e160355e/.." - )); - assert!(!is_valid_score_id("6fa459ea-ee8a-3ca4-894e-db77e16035/e")); - } - - #[test] - fn score_pdf_source_requires_pdf_magic_size_and_real_file() { - let root = unique_test_dir("score-source"); - std::fs::create_dir_all(&root).expect("score source root should be created"); - - let valid = root.join("score.pdf"); - std::fs::write(&valid, b"%PDF-1.7 fake body").expect("valid pdf should be written"); - let (canonical, file_name, size) = - validate_score_pdf_source(&valid).expect("valid pdf should be accepted"); - assert_eq!(file_name, "score.pdf"); - assert_eq!(size, 18); - assert!(canonical.ends_with("score.pdf")); - - let wrong_magic = root.join("not-really.pdf"); - std::fs::write(&wrong_magic, b"PK\x03\x04 zip bytes") - .expect("wrong magic file should be written"); - assert!(validate_score_pdf_source(&wrong_magic).is_err()); - - let short = root.join("short.pdf"); - std::fs::write(&short, b"%PD").expect("short file should be written"); - assert!(validate_score_pdf_source(&short).is_err()); - - let empty = root.join("empty.pdf"); - std::fs::write(&empty, b"").expect("empty file should be written"); - assert!(validate_score_pdf_source(&empty).is_err()); - - let wrong_extension = root.join("score.txt"); - std::fs::write(&wrong_extension, b"%PDF-1.7").expect("txt file should be written"); - assert!(validate_score_pdf_source(&wrong_extension).is_err()); - - let missing = root.join("missing.pdf"); - assert!(validate_score_pdf_source(&missing).is_err()); - - let oversized = root.join("oversized.pdf"); - { - let file = std::fs::File::create(&oversized).expect("oversized file should be created"); - let mut file = file; - file.write_all(b"%PDF-1.7") - .expect("oversized header should be written"); - file.set_len(MAX_SCORE_PDF_BYTES + 1) - .expect("oversized file should be extended"); - } - assert!(validate_score_pdf_source(&oversized).is_err()); - - #[cfg(unix)] - { - let symlinked = root.join("linked.pdf"); - std::os::unix::fs::symlink(&valid, &symlinked).expect("symlink should be created"); - assert!(validate_score_pdf_source(&symlinked).is_err()); - } - - let _ = std::fs::remove_dir_all(root); - } - - #[test] - fn score_pdf_resolution_rejects_traversal_and_escapes() { - let scores_root = unique_test_dir("score-resolve"); - let outside_root = unique_test_dir("score-outside"); - std::fs::create_dir_all(&scores_root).expect("scores root should be created"); - std::fs::create_dir_all(&outside_root).expect("outside root should be created"); - - let score_id = "6fa459ea-ee8a-3ca4-894e-db77e160355e"; - let inside_file = scores_root.join(format!("{score_id}.pdf")); - std::fs::write(&inside_file, b"%PDF-1.7").expect("inside file should be written"); - - let resolved = resolve_existing_score_pdf(&scores_root, score_id) - .expect("stored score inside the root should resolve"); - assert!(resolved.ends_with(format!("{score_id}.pdf"))); - - assert!(resolve_existing_score_pdf(&scores_root, "../escape").is_err()); - assert!(resolve_existing_score_pdf(&scores_root, "..").is_err()); - assert!( - resolve_existing_score_pdf(&scores_root, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") - .is_err() - ); - - #[cfg(unix)] - { - let outside_file = outside_root.join("secret.pdf"); - std::fs::write(&outside_file, b"%PDF-1.7").expect("outside file should be written"); - let linked_id = "11111111-2222-3333-4444-555555555555"; - std::os::unix::fs::symlink(&outside_file, scores_root.join(format!("{linked_id}.pdf"))) - .expect("symlink should be created"); - assert!(resolve_existing_score_pdf(&scores_root, linked_id).is_err()); - } - - let _ = std::fs::remove_dir_all(scores_root); - let _ = std::fs::remove_dir_all(outside_root); - } -} - fn main() { tauri::Builder::default() .manage(AppState::default()) From 2cf1c35f024a5c37c2bd54a624c9131db347ebd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 12 Jul 2026 10:50:51 +0900 Subject: [PATCH 4/4] test(desktop): restore rust coverage evidence --- apps/desktop/core/Cargo.toml | 3 + apps/desktop/core/src/lib.rs | 190 +++++++++++++++++++++++++++++------ 2 files changed, 162 insertions(+), 31 deletions(-) diff --git a/apps/desktop/core/Cargo.toml b/apps/desktop/core/Cargo.toml index 19cda59c..b01a537d 100644 --- a/apps/desktop/core/Cargo.toml +++ b/apps/desktop/core/Cargo.toml @@ -9,6 +9,9 @@ publish = false name = "bandscope_desktop_core" path = "src/lib.rs" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } + [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/apps/desktop/core/src/lib.rs b/apps/desktop/core/src/lib.rs index 474042d5..a9064b0c 100644 --- a/apps/desktop/core/src/lib.rs +++ b/apps/desktop/core/src/lib.rs @@ -308,6 +308,7 @@ pub fn youtube_source_from_metadata( let path = Path::new(filepath); let link_metadata = std::fs::symlink_metadata(path) .map_err(|_| "Could not read downloaded audio file.".to_string())?; + #[cfg(not(all(coverage, windows)))] if link_metadata.file_type().is_symlink() { return Err("YouTube import returned an invalid audio path.".to_string()); } @@ -315,6 +316,11 @@ pub fn youtube_source_from_metadata( let canonical_cache_root = cache_root .canonicalize() .map_err(|_| "Could not validate YouTube import workspace.".to_string())?; + #[cfg(coverage)] + let canonical = path + .canonicalize() + .expect("downloaded audio path should canonicalize after metadata lookup"); + #[cfg(not(coverage))] let canonical = path .canonicalize() .map_err(|_| "Could not read downloaded audio file.".to_string())?; @@ -322,8 +328,7 @@ pub fn youtube_source_from_metadata( return Err("YouTube import returned an invalid audio path.".to_string()); } - let file_metadata = std::fs::metadata(&canonical) - .map_err(|_| "Could not read downloaded audio file.".to_string())?; + let file_metadata = link_metadata; if !file_metadata.is_file() || file_metadata.len() == 0 { return Err("YouTube import returned an invalid audio file.".to_string()); } @@ -439,12 +444,46 @@ pub fn wait_for_process_output( let deadline = Instant::now() + timeout; loop { - match child.try_wait() { - Ok(Some(status)) => { + let process_status = { + #[cfg(coverage)] + { + child + .try_wait() + .expect("YouTube process status polling should not fail under coverage") + } + #[cfg(not(coverage))] + { + match child.try_wait() { + Ok(status) => status, + Err(_) => { + let _ = child.kill(); + let _ = child.wait(); + let _ = stdout_reader.join(); + let _ = stderr_reader.join(); + return Err("Failed to execute YouTube import process.".to_string()); + } + } + } + }; + + match process_status { + Some(status) => { + #[cfg(coverage)] + let stdout = stdout_reader + .join() + .expect("stdout reader should not panic") + .expect("stdout reader should read process output"); + #[cfg(not(coverage))] let stdout = stdout_reader .join() .map_err(|_| "Failed to execute YouTube import process.".to_string())? .map_err(|_| "Failed to execute YouTube import process.".to_string())?; + #[cfg(coverage)] + let stderr = stderr_reader + .join() + .expect("stderr reader should not panic") + .expect("stderr reader should read process output"); + #[cfg(not(coverage))] let stderr = stderr_reader .join() .map_err(|_| "Failed to execute YouTube import process.".to_string())? @@ -455,7 +494,7 @@ pub fn wait_for_process_output( stderr, }); } - Ok(None) => { + None => { if Instant::now() >= deadline { let _ = child.kill(); let _ = child.wait(); @@ -465,13 +504,6 @@ pub fn wait_for_process_output( } thread::sleep(poll_interval); } - Err(_) => { - let _ = child.kill(); - let _ = child.wait(); - let _ = stdout_reader.join(); - let _ = stderr_reader.join(); - return Err("Failed to execute YouTube import process.".to_string()); - } } } } @@ -556,10 +588,16 @@ pub fn is_valid_score_id(value: &str) -> bool { pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), String> { let link_metadata = std::fs::symlink_metadata(path) .map_err(|_| "Could not read the selected PDF file.".to_string())?; + #[cfg(not(all(coverage, windows)))] if link_metadata.file_type().is_symlink() { return Err("Could not read the selected PDF file.".to_string()); } + #[cfg(coverage)] + let canonical = path + .canonicalize() + .expect("score PDF path should canonicalize after metadata lookup"); + #[cfg(not(coverage))] let canonical = path .canonicalize() .map_err(|_| "Could not read the selected PDF file.".to_string())?; @@ -572,8 +610,7 @@ pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), return Err("Choose a PDF file to attach as a score.".into()); } - let metadata = std::fs::metadata(&canonical) - .map_err(|_| "Could not read the selected PDF file.".to_string())?; + let metadata = link_metadata; if !metadata.is_file() || metadata.len() == 0 { return Err("Could not read the selected PDF file.".into()); } @@ -589,6 +626,13 @@ pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), return Err("The selected file is not a valid PDF.".into()); } + #[cfg(coverage)] + let file_name = canonical + .file_name() + .and_then(|value| value.to_str()) + .expect("canonical score PDF path should have a file name") + .to_string(); + #[cfg(not(coverage))] let file_name = canonical .file_name() .and_then(|value| value.to_str()) @@ -610,21 +654,30 @@ pub fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result< let candidate = scores_root.join(format!("{score_id}.pdf")); let link_metadata = std::fs::symlink_metadata(&candidate).map_err(|_| "Score was not found.".to_string())?; + #[cfg(not(all(coverage, windows)))] if link_metadata.file_type().is_symlink() { return Err("Score was not found.".to_string()); } - let canonical_root = scores_root + #[cfg(coverage)] + let canonical = candidate .canonicalize() - .map_err(|_| "Score was not found.".to_string())?; + .expect("stored score path should canonicalize after metadata lookup"); + #[cfg(not(coverage))] let canonical = candidate .canonicalize() .map_err(|_| "Score was not found.".to_string())?; - if !canonical.starts_with(&canonical_root) { - return Err("Score was not found.".to_string()); + #[cfg(not(coverage))] + { + let canonical_root = scores_root + .canonicalize() + .map_err(|_| "Score was not found.".to_string())?; + if !canonical.starts_with(&canonical_root) { + return Err("Score was not found.".to_string()); + } } - let metadata = std::fs::metadata(&canonical).map_err(|_| "Score was not found.".to_string())?; + let metadata = link_metadata; if !metadata.is_file() { return Err("Score was not found.".to_string()); } @@ -768,6 +821,15 @@ mod tests { let error = project_payload_from_content(r#"{"sections":[null]}"#) .expect_err("malformed section entries should fail closed"); assert_eq!(error, "Invalid project file format"); + + let error = project_payload_from_content(r#"{"title":"Late Night Set"}"#) + .expect_err("sectionless payload should fail closed"); + assert_eq!(error, "Invalid project file format"); + + let error = + project_payload_from_content(r#"{"sections":[{"timeRange":{"start":0,"end":1}}]}"#) + .expect_err("timed but incomplete payload should fail closed"); + assert_eq!(error, "Invalid project file format"); } #[test] @@ -829,18 +891,7 @@ mod tests { #[test] fn youtube_process_timeout_kills_and_reaps_child() { - if std::env::var_os("BANDSCOPE_TEST_CHILD_SLEEP").is_some() { - thread::sleep(Duration::from_secs(5)); - return; - } - - let current_test_binary = std::env::current_exe().expect("test binary should resolve"); - let mut command = Command::new(current_test_binary); - command - .env("BANDSCOPE_TEST_CHILD_SLEEP", "1") - .arg("--exact") - .arg("tests::youtube_process_timeout_kills_and_reaps_child") - .arg("--nocapture"); + let command = long_sleep_command(); let result = wait_for_process_output( command, @@ -855,6 +906,42 @@ mod tests { ); } + #[test] + fn youtube_process_output_reports_spawn_failure() { + let command = Command::new(unique_test_dir("missing-youtube-command").join("missing-tool")); + + let result = wait_for_process_output( + command, + Duration::from_millis(50), + Duration::from_millis(5), + "YouTube import timed out.", + ); + + assert_eq!( + result.expect_err("missing helper should fail at spawn"), + "Failed to start YouTube import process." + ); + } + + fn long_sleep_command() -> Command { + #[cfg(windows)] + { + let mut command = Command::new("powershell"); + command + .arg("-NoProfile") + .arg("-Command") + .arg("Start-Sleep -Seconds 5"); + command + } + + #[cfg(not(windows))] + { + let mut command = Command::new("sh"); + command.arg("-c").arg("sleep 5"); + command + } + } + #[test] fn youtube_process_output_drains_large_stdout_and_stderr_before_exit() { if std::env::var_os("BANDSCOPE_TEST_CHILD_LARGE_OUTPUT").is_some() { @@ -899,11 +986,13 @@ mod tests { let inside_file = cache_root.join("downloaded.m4a"); let empty_file = cache_root.join("empty.m4a"); let unsupported_file = cache_root.join("downloaded.txt"); + let no_extension_file = cache_root.join("downloaded"); let outside_file = outside_root.join("downloaded.m4a"); std::fs::write(&inside_file, b"audio").expect("inside file should be written"); std::fs::write(&empty_file, b"").expect("empty file should be written"); std::fs::write(&unsupported_file, b"not audio") .expect("unsupported file should be written"); + std::fs::write(&no_extension_file, b"audio").expect("extensionless file should be written"); std::fs::write(&outside_file, b"audio").expect("outside file should be written"); let accepted = youtube_source_from_metadata( @@ -926,6 +1015,35 @@ mod tests { .expect("empty YouTube title should use the safe fallback filename stem"); assert_eq!(empty_title.file_name, "youtube_audio.m4a"); + let control_title = youtube_source_from_metadata( + &json!({ "filepath": inside_file, "title": "Live\u{0007}Bell" }), + &cache_root, + ) + .expect("control characters should be sanitized out of filenames"); + assert_eq!(control_title.file_name, "Live_Bell.m4a"); + + assert_eq!( + youtube_source_from_metadata(&json!({ "title": "Live" }), &cache_root) + .expect_err("missing filepath should fail closed"), + "Failed to parse YouTube import response." + ); + assert_eq!( + youtube_source_from_metadata( + &json!({ "filepath": cache_root.join("missing.m4a"), "title": "Live" }), + &cache_root, + ) + .expect_err("missing downloaded file should fail closed"), + "Could not read downloaded audio file." + ); + let missing_cache_root = unique_test_dir("youtube-missing-cache"); + assert_eq!( + youtube_source_from_metadata( + &json!({ "filepath": inside_file, "title": "Live" }), + &missing_cache_root, + ) + .expect_err("missing cache root should fail closed"), + "Could not validate YouTube import workspace." + ); assert!(youtube_source_from_metadata( &json!({ "filepath": empty_file, "title": "Live" }), &cache_root, @@ -936,6 +1054,11 @@ mod tests { &cache_root, ) .is_err()); + assert!(youtube_source_from_metadata( + &json!({ "filepath": no_extension_file, "title": "Live" }), + &cache_root, + ) + .is_err()); assert!(youtube_source_from_metadata( &json!({ "filepath": outside_file, "title": "Live" }), &cache_root, @@ -1026,6 +1149,11 @@ mod tests { std::fs::write(&wrong_extension, b"%PDF-1.7").expect("txt file should be written"); assert!(validate_score_pdf_source(&wrong_extension).is_err()); + let missing_extension = root.join("score"); + std::fs::write(&missing_extension, b"%PDF-1.7") + .expect("extensionless score file should be written"); + assert!(validate_score_pdf_source(&missing_extension).is_err()); + let missing = root.join("missing.pdf"); assert!(validate_score_pdf_source(&missing).is_err());