Skip to content

Feature: FoliCon icon overlay plugin ecosystem - #312

Open
DineshSolanki wants to merge 19 commits into
masterfrom
feature/plugin-eco
Open

Feature: FoliCon icon overlay plugin ecosystem#312
DineshSolanki wants to merge 19 commits into
masterfrom
feature/plugin-eco

Conversation

@DineshSolanki

@DineshSolanki DineshSolanki commented Jul 18, 2026

Copy link
Copy Markdown
Owner

What

Folicon Plugin ecosystem rebuilt from scratch after the previous discarded dll approch.

Why

This is part of folicon plugin feature ecosystem.

How

Implemented multiple overlay definitions with respective properties, updated the PreviewerViewModel for dynamic loading, and enhanced the PosterIconConfigViewModel for better data binding. Refactored the UI for improved scalability for multiple overlay, overlays are now controlled by json

Testing

  • Builds successfully (dotnet build)
  • Tested manually on Windows
  • No regressions observed

Screenshots

Summary by CodeRabbit

  • New Features
    • Added an Overlay Store for discovering, previewing, installing, updating, and removing overlays.
    • Added an Overlay Designer with templates, drafts, editing tools, undo/redo, validation, export, and installation.
    • Added dynamic previews for built-in and installed designs.
    • Added automatic update checks and filtering by status, tags, and availability.
  • Localization
    • Added translated Store and Designer content across supported languages.
  • Documentation
    • Updated setup guidance, screenshots, overlay plugin information, and translation instructions.

- Introduced multiple new overlay definitions including "Alternate", "Faelpessoal", "Legacy", "Liaher", and "Windows 11" with respective properties and configurations.
- Updated the PreviewerViewModel to load available overlays dynamically.
- Enhanced PosterIconConfigViewModel to manage overlay selection with a new OverlayItemViewModel for better data binding.
- Implemented a dynamic poster icon renderer that builds its visual tree based on the selected overlay definition.
- Refactored the poster icon configuration view to utilize an ItemsControl for overlay selection, improving UI scalability and maintainability.
- Add Tests for the plugin feature
@codacy-production

codacy-production Bot commented Jul 18, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high · 4 medium · 5 minor

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
BestPractice 4 medium
ErrorProne 1 high
CodeStyle 5 minor

View in Codacy

🟢 Metrics 1511 complexity · 64 duplication

Metric Results
Complexity 1511
Duplication 64

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 1: Core Plugin System (MVP) — ✅ COMPLETE

All 7 phases done. Build verified, 58 tests passing.

Phase Status Key deliverables
Phase 1: Data Model ✅ Done PosterOverlayDefinition, OverlayLayerConfig, 6 overlay.json files, JSON schema
Phase 2: Overlay Loader ✅ Done IOverlayProvider, OverlayProvider, OverlayValidator
Phase 3: Dynamic Renderer ✅ Done DynamicPosterIcon, OverlayPreviewCache, StaRenderer integration
Phase 4: Migrate Overlays ✅ Done 6 overlay definitions, IconOverlay deprecated
Phase 5: UI Integration ✅ Done Dynamic PosterIconConfig, Previewer, DI registration
Phase 7: Settings & Migration ✅ Done String-based overlay IDs, enum→string migration
Testing ✅ Done 58/58 tests passing, golden-image parity with tight 100px threshold

…y overlays via a GitHub-backed catalog.

```
GitHub (FoliCon-Overlays repo)
  catalog.json ──→ OverlayRepositoryService ──→ OverlayStoreViewModel ──→ OverlayStore.xaml
  overlays/{id}/                                   ↕
     manifest.json                              OverlayProvider (reads from %AppData%/FoliCon/Overlays/)
     overlay.json                               DynamicPosterIcon (renders previews)
     preview.png                                OverlayPreviewCache (caches rendered previews)
     *.png (assets)
```
@DineshSolanki DineshSolanki self-assigned this Jul 19, 2026
@DineshSolanki DineshSolanki added the enhancement New feature or request label Jul 19, 2026
@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 2: Overlay Store + Repository — ✅ COMPLETE

Phase 1: Data Models — ✅ DONE

Deliverable Status Notes
OverlayManifest.cs ✅ Done Per-overlay manifest with metadata, assets, SHA256, ToCatalogEntry() converter
manifest-schema.json ✅ Done JSON Schema for manifest validation (5MB max, semver, SHA256 hex)

Phase 2: Repository Service — ✅ DONE

Deliverable Status Notes
IOverlayRepositoryService.cs ✅ Done Interface: FetchCatalog, FetchManifest, Install, Update, Uninstall, MarkUpdateAvailable
OverlayRepositoryService.cs ✅ Done HTTP via Services.HttpC, disk cache (24h TTL), atomic install (tmp→validate→rename), SHA256 verification, backup/rollback, injectable paths/URL, env var + file override for local dev

Phase 3: ViewModels — ✅ DONE

Deliverable Status Notes
OverlayStoreViewModel.cs ✅ Done Catalog loading, search/filter by query+tag, install/update/uninstall commands, card state persistence across filter changes
OverlayCardViewModel.cs ✅ Done Preview lazy-loading from URL, SizeDisplay/VersionDisplay formatting, INotifyPropertyChanged

Phase 4: Overlay Store UI — ✅ DONE

Deliverable Status Notes
OverlayStore.xaml + .cs ✅ Done HandyControl dialog: SearchBar, tag ComboBox, WrapPanel card grid, BusyIndicator, status bar with error display

Phase 5: Wire Up UI + DI — ✅ DONE

Deliverable Status Notes
DI registrations in App.xaml.cs ✅ Done IOverlayRepositoryService singleton, OverlayUpdateChecker singleton, OverlayStore dialog
DialogServiceExtensions.cs ✅ Done ShowOverlayStore() extension method
PosterIconConfigViewModel ✅ Done IDialogService injection, BrowseOverlayStoreCommand, DemoIconPath returns frozen BitmapImage for community overlays (no file lock)
PosterIconConfig.xaml ✅ Done "Browse Overlay Store..." button enabled with command binding
GlobalVariables.SetOverlayProvider() ✅ Done Ensures DI singleton and static accessor share same IOverlayProvider instance

Phase 6: GitHub Repo Prep — ✅ DONE

  • FoliCon-Overlays

    Deliverable Status Notes
    FoliCon-Overlays/README.md ✅ Done Contribution guide with directory structure, manifest format, validation limits
    FoliCon-Overlays/catalog.json ✅ Done Initial catalog with example-dvd-case overlay
    FoliCon-Overlays/overlays/example-dvd-case/ ✅ Done Test overlay: overlay.json, manifest.json, base.png, front.png, preview.png
    .github/workflows/generate-catalog.yml ✅ Done Auto-generates catalog.json from manifest files on push to main
    .agents/skill ✅ Done AI agent skill to help user interactively generate the overlays

Phase 7: Update Checker — ✅ DONE

Deliverable Status Notes
OverlayUpdateChecker.cs ✅ Done Non-blocking background check on app start, marks updates via MarkUpdateAvailable(), uses shared OverlayConstants.TryCompareVersions()

Community Overlay Support — ✅ DONE

Deliverable Status Notes
PosterOverlayDefinition.OverlayFolderPath ✅ Done [JsonIgnore] property set by OverlayProvider when loading user overlays
DynamicPosterIcon.ResolveImageSource ✅ Done Now non-static, resolves relative paths against _overlayFolderPath for community overlays
DemoIconPath for community overlays ✅ Done Returns frozen BitmapImage (no file lock) instead of file path string

Testing — ✅ DONE

Deliverable Status Notes
OverlayManifestTests.cs ✅ Done 6 tests: defaults, JSON round-trip, serialize, ToCatalogEntry mapping
OverlayRepositoryServiceTests.cs ✅ Done 11 tests: install/uninstall/cache/version with injectable temp paths
OverlayStoreViewModelTests.cs ✅ Done 6 tests: field mapping, size formatting, property notifications, filtering (uses StubRepositoryService)
Total test run ✅ Done 81 tests, 81 passed, 0 failed (verified 2026-07-20)

- Updated OverlayDesigner.xaml.cs to use localized title for color picker.
- Enhanced OverlayStore.xaml with localization for various UI elements including buttons and tooltips.
- Modified posterIconConfig.xaml to utilize localized strings for tooltips and button content.
- Added localization tests to ensure all overlay strings are translated across multiple cultures.
- Introduced smoke tests for OverlayDesigner and OverlayStore views to validate XAML loading and layout.
- Implemented a new XamlLoadingCollection to prevent concurrent loading issues in tests.
- Improved WpfTestHost to set up application resources for testing localized strings.
@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 3: Overlay Designer (v1.1) — ✅ COMPLETE (5 of 5 steps)

Scope changed during planning: GitHub PR automation moved out to Release 3.1 (externally blocked on OAuth App
registration, and public_repo scope warrants its own review), and "New from template" was added because the original
scope could only open existing packages — a designer that presents a dead end to anyone without an overlay folder.

Step Status Notes
1. Document + validation foundation ✅ Done Typed edit state, undo/redo with baseline dirty tracking, centralized margin↔bounds geometry, template cloning with pack-resource extraction, read-only package loading, structured validation
2. Designer dialog + live preview ✅ Done Template picker with rendered thumbnails, drag/resize canvas, selection-driven properties, debounced STA preview, keyboard operability, validation gating
3. Drafts + package export ✅ Done OverlayDraftStore, OverlayExporter, camelCase serializer, deterministic preview.png + manifest.json
4. Guided manual submission ✅ Done Submission panel, local install, live catalog clash check
5. Launch points + localization ✅ Done Main-menu + store entries, resx keys, regression sweep (translations outstanding)
Deliverable Status
Modules/Overlays/Designer/ (12 files) ✅ Done
OverlayDesignerViewModel.cs + OverlayElementViewModel.cs + OverlayTemplateCardViewModel.cs ✅ Done
OverlayDesigner.xaml + .xaml.cs ✅ Done
OverlayExporter.cs + OverlayPackageSerializer.cs ✅ Done
OverlayDraftStore.cs ✅ Done
OverlaySubmissionGuide.cs ✅ Done
GitHub device auth + publisher ⏸️ Deferred to Release 3.1

An author can now create an overlay from a template, edit it on a canvas, save drafts, export a store-ready package,
install it for their own use, and be walked through submitting it — without leaving FoliCon.

Tests: 453 total (81 from Releases 1–2 + 372 added across Release 3 and the store rework), 0 warnings, 0 errors,
stable across repeated full runs.

Completion pass (2026-07-26): layer reordering and per-corner clip radius editing exposed in the designer;
OverlayStore / OverlayDesigner translated into all 7 locales; the 6 obsolete PosterIcon*.xaml views and the
callerless ReferenceImageExporter removed (13 files) with app startup verified. The [Obsolete] IconOverlay enum is
deliberately retained — now unreferenced, but removing a public type is a breaking change for a major version.

Store tag filter reworked (2026-07-26): was a single-select ComboBox — a Release 2 gap the review had already
flagged (hc:TagContainer was planned) and which contradicted DESIGN.md's Chips component. Now multi-select **
hc:Tag** chips on their own wrapping row: per-tag counts, popularity ordering, AND semantics so "dvd + classic"
narrows rather than widens, a "Clear tags" action, and selection preserved across refresh. SelectedTag/AvailableTags
are gone.

Defects found and fixed

Thirteen in total across Release 3 — three by tests, ten by manual use, which is the ratio worth remembering:
automated coverage caught structural faults, but every visual and workflow defect needed a human.

Found by Count Examples
Automated tests 3 Null-command crash on dialog open; layer-reorder undo not rebuilding the rail; export determinism
Manual QA round 1 4 Clipped template cards; invisible card text; Close navigating to the wrong place; Close doing nothing at all when dirty
Manual QA round 2 7 No draft resume; no uninstall for locally-installed overlays; chips rendering as plain text; rating number not following its badge; blank numeric editors; radius only applying on focus loss; title text never drawn

Two of these were more than UI polish:

  • Title text was never rendered when layerOrder omitted "title" — a DynamicPosterIcon bug affecting all
    community overlays, not just the designer. The renderer now appends any element that exists but is unlisted; a
    companion test confirms layerOrder still governs z-order for listed layers so built-in parity is undisturbed.
  • Locally-installed overlays were unremovable. The store can only uninstall what it knows from the catalog, and a
    designer-installed overlay has no catalog entry. Uninstall now lives in OverlayExporter.UninstallLocal beside
    install, with a ✕ in Change Poster Icon Overlay.

@DineshSolanki

Copy link
Copy Markdown
Owner Author

This feature was worked in 3 Releases and many phases of each release, though it will be merged only once everything is completed

Release 1: Core Plugin System (MVP)

Release 2: Overlay Store + Repository

  • New GitHub repository: FoliCon-Overlays with per-overlay manifests + GH Actions

Release 3: Overlay Designer

@DineshSolanki DineshSolanki changed the title Add overlay definitions and enhance poster icon configuration Feature: FoliCon icon overlay plugin ecosystem Jul 27, 2026
@DineshSolanki
DineshSolanki requested a review from Copilot July 28, 2026 05:12

This comment was marked as off-topic.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds a complete overlay platform with data-driven rendering, built-in and community overlay management, an Overlay Designer, an Overlay Store, localized UI resources, package validation and export, preview caching, and extensive WPF test coverage.

Changes

Overlay platform

Layer / File(s) Summary
Overlay contracts and application wiring
FoliCon/App.xaml*, FoliCon/Models/Data/*, FoliCon/Modules/Overlays/I*.cs, FoliCon/Modules/LangProvider.cs
Adds overlay data models, provider and repository contracts, localized formatting, application registration, and startup update checking.
Provider, repository, and validation services
FoliCon/Modules/Overlays/OverlayProvider.cs, OverlayRepositoryService.cs, OverlayValidator.cs, OverlayUpdateChecker.cs
Loads built-in and installed overlays, validates packages, caches catalog data, installs and updates overlays, removes local overlays, and records available updates.
Designer engine and package workflows
FoliCon/Modules/Overlays/Designer/*
Adds typed editor state, geometry conversion, undo/redo, previews, drafts, templates, package loading, deterministic serialization, export, local installation, and submission checks.
Dynamic rendering and application integration
FoliCon/Views/DynamicPosterIcon.xaml*, FoliCon/Modules/Overlays/OverlayPreviewCache.cs, FoliCon/ViewModels/*
Renders poster icons from overlay definitions, caches previews, replaces fixed overlay selection with provider-backed collections, and adds store and designer commands.
Designer and store UI
FoliCon/Views/OverlayDesigner*, FoliCon/Views/OverlayStore*, FoliCon/ViewModels/OverlayDesignerViewModel.cs, OverlayStoreViewModel.cs
Adds editing controls, template and draft workflows, validation and export actions, catalog browsing, filtering, installation, updates, and removal operations.
Resources and localization
FoliCon/Resources/Overlays/*, FoliCon/Properties/Langs/*
Adds six built-in overlay definitions and localized overlay designer and store resources for the supported cultures.
Validation and project support
FoliconTest/*, Folicon.sln, .gitignore, README.md
Adds the WPF test project, rendering parity tests, designer and store tests, localization tests, generated-artifact ignores, and updated documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MainWindow as MainWindowViewModel
  participant Store as OverlayStoreViewModel
  participant Repository as OverlayRepositoryService
  participant Provider as OverlayProvider
  participant PreviewCache as OverlayPreviewCache
  MainWindow->>Store: Open OverlayStore dialog
  Store->>Repository: FetchCatalogAsync
  Store->>Provider: Check installation and version state
  Store->>PreviewCache: Load overlay previews
  Store->>Repository: Install, update, or uninstall overlay
  Repository->>Provider: Refresh definitions
Loading
sequenceDiagram
  participant Designer as OverlayDesignerViewModel
  participant Document as OverlayDesignerDocument
  participant Renderer as OverlayDesignerPreviewRenderer
  participant Exporter as OverlayExporter
  Designer->>Document: Apply edit command
  Designer->>Renderer: Request preview render
  Renderer->>Document: Render definition and preview context
  Designer->>Exporter: ExportAsync snapshot
  Exporter->>Document: Validate and serialize package
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: rebuilding the FoliCon icon overlay plugin ecosystem.
Description check ✅ Passed The description covers the change, motivation, implementation, and testing; the missing issue link and screenshots are non-critical.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/plugin-eco

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (23)
FoliCon/internal-nlog.txt-1-5 (1)

1-5: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the generated runtime log from the repository.

FoliCon/internal-nlog.txt contains verbose NLog diagnostics and a developer-specific absolute path on Line 5. Remove the file from version control and ignore it in .gitignore to prevent future reintroduction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/internal-nlog.txt` around lines 1 - 5, Remove the generated runtime
log file FoliCon/internal-nlog.txt from version control, then add that filename
to the repository’s .gitignore so future NLog diagnostics are not reintroduced.
FoliCon/Modules/Overlays/OverlayProvider.cs-67-76 (1)

67-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return an absolute path for built-in overlays.

GetOverlayFolderPath returns Resources\Overlays\<id> for built-in IDs. That path is relative. IOverlayProvider.GetOverlayFolderPath documents "the full path to an overlay's folder". Any caller that passes the result to File.Exists, Directory.GetFiles, or Path.GetFullPath resolves it against the current working directory. The working directory is not the application directory when the app starts from a shortcut, from a shell with a different cwd, or from a file-dialog callback.

Base the built-in path on AppContext.BaseDirectory.

🐛 Proposed fix
         if (OverlayConstants.BuiltInOverlayIds.Contains(id, StringComparer.OrdinalIgnoreCase))
         {
-            return Path.Combine("Resources", "Overlays", id);
+            return Path.Combine(AppContext.BaseDirectory, "Resources", "Overlays", id);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayProvider.cs` around lines 67 - 76, Update
GetOverlayFolderPath so built-in overlay IDs return an absolute path rooted at
AppContext.BaseDirectory, while preserving the existing Resources/Overlays/<id>
structure; leave user overlay path handling unchanged.
FoliCon/Modules/Overlays/Internal/OverlayValidator.cs-182-189 (1)

182-189: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict the pack-path bypass to built-in overlays.

ValidateAssetReference returns early for any assetPath that starts with /. ValidateDetailed runs for user-installed packages too, through OverlayProvider.LoadUserOverlays. A community overlay can therefore set "imagePath": "/Resources/Overlays/liaher/base.png" and skip every asset check: relative-path safety, PNG extension, existence, and per-image size limit. The overlay then renders app-internal resources that the package does not ship.

Gate the early return on definition.IsBuiltIn, or reject leading-slash paths when the overlay folder is a user overlay folder.

🛡️ Proposed fix
-    private static void ValidateAssetReference(string overlayFolder, string assetPath, string field, OverlayValidationResult result)
+    private static void ValidateAssetReference(string overlayFolder, string assetPath, string field,
+        OverlayValidationResult result, bool isBuiltIn)
     {
         // Built-in overlays reference embedded resources with a leading slash; those are
         // resolved by DynamicPosterIcon against pack URIs and never touch the overlay folder.
-        if (assetPath.StartsWith('/'))
+        if (isBuiltIn && assetPath.StartsWith('/'))
         {
             return;
         }

Pass definition.IsBuiltIn down from ValidateDetailed through ValidateLayers, ValidateLayer, and ValidatePosterConfig.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Internal/OverlayValidator.cs` around lines 182 -
189, Restrict the leading-slash asset bypass in ValidateAssetReference to
built-in overlays only. Propagate definition.IsBuiltIn from ValidateDetailed
through ValidateLayers, ValidateLayer, and ValidatePosterConfig into
ValidateAssetReference, and require it for the early return so user-installed
overlays still undergo all path, extension, existence, and size validation.
FoliCon/Modules/Overlays/OverlayConstants.cs-58-61 (1)

58-61: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use a case-insensitive, read-only set for BuiltInOverlayIds.

The set uses the default ordinal comparer and is a mutable HashSet<string> exposed as a public static field.

Two consequences follow:

  1. OverlayProvider.LoadUserOverlays (line 145) calls OverlayConstants.BuiltInOverlayIds.Contains(definition.Id). That call is case-sensitive. A community overlay with "id": "Liaher" passes the reserved-ID check. GetOverlayById matches with OrdinalIgnoreCase, so the community overlay can then shadow or conflict with the built-in liaher. Note that OverlayValidator.IdRegex rejects uppercase IDs only when the overlay reaches validation, which happens after this check and only produces a warning-level skip for other reasons.
  2. Any caller can mutate the shared set at runtime.
🛡️ Proposed fix
-    public static readonly HashSet<string> BuiltInOverlayIds =
-    [
-        "legacy", "alternate", "liaher", "faelpessoal", "faelpessoal-horizontal", "windows11"
-    ];
+    public static readonly IReadOnlySet<string> BuiltInOverlayIds =
+        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "legacy", "alternate", "liaher", "faelpessoal", "faelpessoal-horizontal", "windows11"
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayConstants.cs` around lines 58 - 61, Update
BuiltInOverlayIds to use OrdinalIgnoreCase comparison and expose it as a
read-only set rather than a mutable public HashSet. Preserve the existing
built-in IDs and ensure OverlayProvider.LoadUserOverlays.Contains applies the
same case-insensitive comparer.
FoliCon/Modules/Overlays/OverlayProvider.cs-114-177 (1)

114-177: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Guard _userOverlays against concurrent reload and reads.

OverlayProvider is registered as a singleton in FoliCon/App.xaml.cs. Refresh() calls LoadUserOverlays(), which clears _userOverlays and refills it item by item. OverlayUpdateChecker.CheckForUpdatesAsync runs on a background task and calls GetUserOverlays(), and the store view models call GetAllOverlays(). A read that overlaps a refresh can throw InvalidOperationException from the enumerator, or observe an empty or partial list and drop installed overlays from the UI.

Two changes are needed:

  1. Build the new list locally and publish it with a single reference assignment, or protect all reads and writes with a lock.
  2. Set definition.OverlayFolderPath before you add the definition to the list. Line 167 adds first and assigns at line 168, so a concurrent reader can observe a definition whose OverlayFolderPath is still null. DynamicPosterIcon then cannot resolve relative image paths for that overlay.
🔒️ Proposed fix sketch
-    private readonly List<PosterOverlayDefinition> _userOverlays = [];
+    private volatile IReadOnlyList<PosterOverlayDefinition> _userOverlays = [];
     private void LoadUserOverlays()
     {
-        _userOverlays.Clear();
+        var loaded = new List<PosterOverlayDefinition>();
 
         if (!Directory.Exists(_userOverlaysPath))
         {
             Logger.Debug("User overlays directory does not exist: {Path}", _userOverlaysPath);
+            _userOverlays = loaded;
             return;
         }
@@
-                _userOverlays.Add(definition);
-                definition.OverlayFolderPath = folder;
+                definition.OverlayFolderPath = folder;
+                loaded.Add(definition);
             }
             catch (Exception ex)
             {
                 Logger.Error(ex, "Failed to load overlay from '{Path}'", jsonPath);
             }
         }
 
-        Logger.Info("Loaded {Count} user overlays", _userOverlays.Count);
+        _userOverlays = loaded;
+        Logger.Info("Loaded {Count} user overlays", loaded.Count);
     }

Apply the same pattern to _builtInOverlays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayProvider.cs` around lines 114 - 177, Update
LoadUserOverlays to build a complete local collection, set
definition.OverlayFolderPath before adding each definition, then publish the
collection with one reference assignment instead of clearing and mutating
_userOverlays. Apply the same atomic publication pattern to _builtInOverlays,
and ensure GetUserOverlays and GetAllOverlays read stable published collections
so refreshes cannot expose partial data or invalidate enumeration.
FoliCon/Modules/Overlays/IOverlayRepositoryService.cs-45-70 (1)

45-70: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make _availableUpdates thread-safe.

OverlayRepositoryService is a singleton. OverlayUpdateChecker writes _availableUpdates while the store reads it through IsUpdateAvailable. UninstallOverlayAsync and InvalidateCache also mutate it. Use ConcurrentDictionary or protect every access with one lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/IOverlayRepositoryService.cs` around lines 45 - 70,
Make the _availableUpdates accesses in OverlayRepositoryService thread-safe
because the singleton is concurrently read and mutated by OverlayUpdateChecker,
IsUpdateAvailable, UninstallOverlayAsync, and InvalidateCache. Replace the
backing collection with ConcurrentDictionary or consistently protect every read,
write, and removal with a shared lock while preserving existing update-state
behavior.
FoliCon/Modules/Overlays/OverlayPreviewCache.cs-103-115 (1)

103-115: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Dispose the PosterIcon after each render.

PosterIcon implements IDisposable and holds a MemoryStream with the full poster bytes (FoliCon/Models/Data/PosterIcon.cs, lines 61-69). This method creates one per overlay and never disposes it, so GetPreviewsAsync leaks one stream for every overlay rendered. OverlayDesignerPreviewRenderer.RenderOnStaAsync already uses using var posterIcon for the same reason.

Create the PosterIcon once per GetPreviewsAsync call, or dispose it here.

🐛 Proposed fix
         return await StaRenderer.Default.EnqueueRender(() =>
         {
             // Create PosterIcon on the STA thread (WPF objects require STA)
-            var posterIcon = CreatePosterIcon(posterPath, rating, ratingVisibility, mockupVisibility);
+            using var posterIcon = CreatePosterIcon(posterPath, rating, ratingVisibility, mockupVisibility);
 
             var dynamicIcon = new DynamicPosterIcon(overlay, posterIcon);
             using var bitmap = dynamicIcon.RenderToBitmap();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayPreviewCache.cs` around lines 103 - 115,
Dispose each PosterIcon created during the render flow to release its underlying
MemoryStream. Update the render lambda in GetPreviewsAsync (or the enclosing
render helper) to scope the CreatePosterIcon result with disposal, while
preserving the existing DynamicPosterIcon and bitmap conversion behavior.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-417-428 (1)

417-428: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Protect the rollback so a failure does not delete the previous version.

RollbackUpdate runs inside a catch block. If Directory.Delete or Directory.Move throws, for example because a file is locked by the running application, the new exception replaces the original failure. The user then loses the installed overlay, and the backup stays behind as {overlayId}_previous. UninstallOverlayAsync later deletes that folder, so the previous version is unrecoverable.

Catch and log inside RollbackUpdate so the original exception still surfaces.

🛡️ Proposed fix
-    private static void RollbackUpdate(string finalDir, string backupDir)
+    private static void RollbackUpdate(string finalDir, string backupDir)
     {
-        if (Directory.Exists(finalDir))
-        {
-            Directory.Delete(finalDir, true);
-        }
-
-        if (Directory.Exists(backupDir))
-        {
-            Directory.Move(backupDir, finalDir);
-        }
+        try
+        {
+            if (Directory.Exists(finalDir))
+            {
+                Directory.Delete(finalDir, true);
+            }
+
+            if (Directory.Exists(backupDir))
+            {
+                Directory.Move(backupDir, finalDir);
+            }
+        }
+        catch (Exception ex)
+        {
+            Logger.Error(ex, "Rollback failed. Backup remains at {BackupDir}", backupDir);
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 417 - 428,
Update RollbackUpdate to handle exceptions from deleting finalDir or moving
backupDir without allowing rollback failures to replace the original update
exception. Catch and log rollback errors within RollbackUpdate, preserving the
backup directory when restoration cannot complete so it remains recoverable by
later cleanup or recovery logic.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-313-333 (1)

313-333: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject assets without a declared hash, and cap the overlay.json size.

Two gaps exist here:

  1. The SHA256 check runs only when manifest.Sha256 contains the asset key. A manifest that omits an entry installs the file with no integrity check. Fail the install instead when a hash is missing.
  2. The size check excludes OverlayConstants.overlayJsonFileName, so overlay.json has no upper bound. DownloadAssetsAsync buffers the whole response with GetByteArrayAsync, so a large file consumes unbounded memory.
🛡️ Proposed change
-        if (bytes.Length > OverlayConstants.maxImageSizeBytes && asset != OverlayConstants.overlayJsonFileName)
+        var maxBytes = asset == OverlayConstants.overlayJsonFileName
+            ? OverlayConstants.maxDefinitionSizeBytes
+            : OverlayConstants.maxImageSizeBytes;
+        if (bytes.Length > maxBytes)
         {
             throw new InvalidOperationException(string.Format(
-                Lang.OverlayInstallAssetTooLarge, asset, OverlayConstants.maxImageSizeBytes / 1024 / 1024));
+                Lang.OverlayInstallAssetTooLarge, asset, maxBytes / 1024 / 1024));
         }
 
-        // SHA256 verification
-        if (manifest.Sha256.TryGetValue(asset, out var expectedHash))
+        if (!manifest.Sha256.TryGetValue(asset, out var expectedHash))
         {
-            var actualHash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
-            if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
-            {
-                Logger.Error("SHA256 mismatch for '{Asset}': expected {Expected}, got {Actual}",
-                    asset, expectedHash, actualHash);
-                throw new InvalidOperationException(
-                    string.Format(Lang.OverlayInstallHashMismatch, asset));
-            }
+            Logger.Error("Manifest declares no SHA256 for '{Asset}'", asset);
+            throw new InvalidOperationException(string.Format(Lang.OverlayInstallHashMismatch, asset));
+        }
+
+        var actualHash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
+        if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
+        {
+            Logger.Error("SHA256 mismatch for '{Asset}': expected {Expected}, got {Actual}",
+                asset, expectedHash, actualHash);
+            throw new InvalidOperationException(
+                string.Format(Lang.OverlayInstallHashMismatch, asset));
         }

OverlayConstants.maxDefinitionSizeBytes is illustrative. Use the existing constant if one is already defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 313 - 333,
Update the asset validation in the method containing the “Size check” and
“SHA256 verification” blocks to enforce the maximum size for every asset,
including OverlayConstants.overlayJsonFileName, using the existing applicable
size constant. Require manifest.Sha256 to contain every asset; when an entry is
missing, reject the install with the existing hash-mismatch error flow, and
retain the current case-insensitive comparison and logging for declared hashes
that do not match.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-509-534 (1)

509-534: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

CheckForUpdates never runs on the cached catalog paths.

FetchCatalogFromNetworkAsync is the only caller. FetchCatalogAsync returns early from the in-memory cache at line 108 and from the disk cache at line 132. After an application restart the disk cache is fresh for 24 hours, so _availableUpdates stays empty and IsUpdateAvailable returns false for every overlay. The store then reports no updates although newer versions exist in the catalog.

Call CheckForUpdates on the cached return paths as well.

🐛 Proposed fix
         if (_cachedCatalog != null && DateTime.UtcNow - _cacheTimestamp < CacheTtl)
         {
             Logger.Debug("Returning in-memory cached catalog");
+            CheckForUpdates(_cachedCatalog);
             return _cachedCatalog;
         }

Apply the same call before the disk-cache return at line 132.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 509 - 534,
Update FetchCatalogAsync so both the in-memory cache return and the disk-cache
return invoke CheckForUpdates with the cached catalog before returning. Preserve
the existing FetchCatalogFromNetworkAsync behavior and ensure _availableUpdates
is refreshed for every cached catalog path.
FoliCon/ViewModels/PreviewerViewModel.cs-47-51 (1)

47-51: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

MediaTitle no longer affects the rendered previews.

The MediaTitle setter only calls SetProperty. It does not call RebuildPreviewsAsync. OverlayPreviewCache.GetPreviewsAsync is called with the poster path, rating, and the two visibility values, but not the title.

OverlayPreviewContext carries a MediaTitle field (see FoliCon/Modules/Overlays/Designer/OverlayDesignerPreviewRenderer.cs Lines 8-33), so the renderer supports a title. The previewer's title input is now inert.

Pass the title into the preview request and rebuild on change, or remove the input from Previewer.xaml.

🐛 Proposed fix
         public string MediaTitle
         {
             get => _mediaTitle;
-            set => SetProperty(ref _mediaTitle, value);
+            set
+            {
+                if (SetProperty(ref _mediaTitle, value))
+                {
+                    _ = RebuildPreviewsAsync();
+                }
+            }
         }

Also applies to: 84-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/PreviewerViewModel.cs` around lines 47 - 51, Update
PreviewerViewModel.MediaTitle so changing the title triggers
RebuildPreviewsAsync, and include the current title when calling
OverlayPreviewCache.GetPreviewsAsync to populate
OverlayPreviewContext.MediaTitle. Preserve the existing preview rebuild behavior
and ensure title changes are reflected in rendered previews.
FoliCon/Views/OverlayDesigner.xaml-64-76 (1)

64-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Arrow-key KeyBindings at UserControl level will block text editing.

InputBindings on the UserControl are evaluated on the bubbling route from the focused element. The arrow keys therefore fire NudgeCommand while focus is inside a TextBox or an hc:NumericUpDown.

NudgeCommand executes whenever SelectedElement != null (OverlayDesignerViewModel.cs Line 135), which is the normal editor state. The command marks the key handled, so the caret does not move in DisplayName (Line 492), OverlayId (Line 496), Description (Line 511), TagsText (Line 516), or the geometry editors (Lines 457-480).

Move the arrow-key bindings onto CanvasRoot, or gate NudgeCommand on canvas focus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Views/OverlayDesigner.xaml` around lines 64 - 76, Move the arrow-key
KeyBindings from UserControl.InputBindings to the CanvasRoot input scope so they
only invoke NudgeCommand while the canvas is focused. Preserve the existing
direction and shift CommandParameters, and leave the UndoCommand, RedoCommand,
and OpenHelpCommand bindings unchanged.
FoliCon/ViewModels/posterIconConfigViewModel.cs-47-56 (1)

47-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset IconOverlay when the store removes the active overlay.

RemoveOverlay falls back to the default when it deletes the active overlay (Lines 105-110). The store callback at Lines 50-55 does not.

If a user uninstalls the active overlay inside the overlay store, LoadOverlays rebuilds AvailableOverlays without that ID. IconOverlay keeps the stale ID, so no item has IsActive == true and the RadioButton group in FoliCon/Views/posterIconConfig.xaml (Lines 63-75) shows nothing selected. Icon generation then falls back silently at render time.

Add the same check to LoadOverlays, which covers both callbacks.

🐛 Proposed fix
             AvailableOverlays.Clear();
             foreach (var overlay in allOverlays)
             {
                 ...
                 AvailableOverlays.Add(item);
             }
+
+            // A removed overlay must not stay selected: the picker would show no
+            // checked item and icon generation would fall back without telling anyone.
+            if (AvailableOverlays.Count > 0 && AvailableOverlays.All(o => !o.IsActive))
+            {
+                IconOverlay = OverlayConstants.DefaultOverlayId;
+            }
         }

Also applies to: 142-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/posterIconConfigViewModel.cs` around lines 47 - 56, Update
LoadOverlays to detect when the current IconOverlay is no longer present in
AvailableOverlays and reset it to the default overlay, matching RemoveOverlay’s
fallback behavior. Keep the existing store callback and removal flow unchanged
so both paths use the centralized validation.
FoliCon/ViewModels/PreviewerViewModel.cs-79-109 (1)

79-109: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Guard LoadPreviewsAsync against overlapping invocations.

Rating, RatingVisibility, and OverlayVisibility each start RebuildPreviewsAsync without awaiting it. SelectImage starts another. Two runs can therefore be in flight at once.

Both runs execute OverlayPreviewItems.Clear() and then Add per item around an await. If the second run clears the collection while the first run is still adding, the bound list ends with a partial or duplicated set. InvalidateAll in one run also discards cache entries the other run just populated, so every overlay is re-rendered.

If Rating is bound with UpdateSourceTrigger=PropertyChanged, each keystroke starts a full re-render of every overlay.

Add a CancellationTokenSource per request and a short debounce, then apply the results only when the token is still current.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/PreviewerViewModel.cs` around lines 79 - 109, Update
LoadPreviewsAsync and RebuildPreviewsAsync to serialize preview refresh requests
using a per-request CancellationTokenSource and short debounce. Cancel and
replace the previous request when Rating, RatingVisibility, OverlayVisibility,
or SelectImage triggers a rebuild, pass the current token through loading, and
only clear/add OverlayPreviewItems if that request remains current; ensure
cancelled or stale requests do not apply results or invalidate newer cache data.
FoliCon/Views/OverlayDesigner.xaml.cs-65-81 (1)

65-81: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

OnLoaded subscribes the canvas handlers on every Loaded event.

The Contains guard at Line 67 protects the adorner creation only. Lines 73-76 run unconditionally.

FrameworkElement.Loaded can raise more than once for the same instance, for example after the element is removed from and re-added to the visual tree. Each raise adds another set of handlers to CanvasRoot. OnCanvasMouseMove then calls ViewModel.ApplyGesture twice per pointer move, and the handlers are never removed.

🐛 Proposed fix
     private void OnLoaded(object sender, RoutedEventArgs e)
     {
         if (!AdornerLayer.Children.Contains(_selectionOutline))
         {
             AdornerLayer.Children.Add(_selectionOutline);
             CreateHandles();
+
+            CanvasRoot.MouseLeftButtonDown += OnCanvasMouseDown;
+            CanvasRoot.MouseMove += OnCanvasMouseMove;
+            CanvasRoot.MouseLeftButtonUp += OnCanvasMouseUp;
+            CanvasRoot.MouseLeave += OnCanvasMouseLeave;
         }
 
-        CanvasRoot.MouseLeftButtonDown += OnCanvasMouseDown;
-        CanvasRoot.MouseMove += OnCanvasMouseMove;
-        CanvasRoot.MouseLeftButtonUp += OnCanvasMouseUp;
-        CanvasRoot.MouseLeave += OnCanvasMouseLeave;
-
         // Arrow-key nudge is bound at the dialog level, so focus must land here.
         Focus();
         UpdateAdorner();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Views/OverlayDesigner.xaml.cs` around lines 65 - 81, Update OnLoaded
so CanvasRoot mouse handlers are subscribed only once, guarding the
subscriptions with the same initialization state used for the adorner or a
dedicated flag; ensure repeated Loaded events do not duplicate handlers while
preserving the existing adorner setup and focus/update behavior.
FoliCon/ViewModels/posterIconConfigViewModel.cs-220-267 (1)

220-267: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Cache the community preview instead of decoding it in the property getter.

DemoIconPath is a computed property with no backing field and no change notification. Every read of the _ branch calls LoadCommunityOverlayPreview, which performs File.Exists and a full PNG decode.

WPF re-reads a bound getter whenever the binding refreshes or the container is re-templated, for example when the ItemsControl in FoliCon/Views/posterIconConfig.xaml (Lines 48-100) virtualizes or re-applies its template. The decode then runs again for each visible overlay.

Store the result in a lazily initialized field.

♻️ Proposed fix
+    private object? _demoIconPath;
+
     public object DemoIconPath => OverlayId switch
     {
         "legacy" => "/Resources/mockup_demos/simple/PosterIcon.ico",
         ...
-        _ => (object?)LoadCommunityOverlayPreview(OverlayId, IsBuiltIn) ?? "/Resources/icons/NoPosterAvailable.png"
+        _ => _demoIconPath ??=
+            (object?)LoadCommunityOverlayPreview(OverlayId, IsBuiltIn)
+            ?? "/Resources/icons/NoPosterAvailable.png"
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/posterIconConfigViewModel.cs` around lines 220 - 267,
Cache the community preview result used by DemoIconPath in a lazily initialized
backing field so repeated getter reads do not rerun LoadCommunityOverlayPreview,
including its file check and PNG decode. Preserve the existing built-in mappings
and fallback behavior, and ensure the cached value is initialized only for the
current OverlayId/IsBuiltIn state using the existing LoadCommunityOverlayPreview
method.
FoliCon/ViewModels/OverlayDesignerViewModel.cs-144-144 (1)

144-144: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap the async command body so exceptions cannot escape as async void.

new DelegateCommand(async () => await ExportPackageAsync(), ...) compiles the lambda to async void. Any exception that ExportPackageAsync does not catch is rethrown on the synchronization context and terminates the process.

ExportPackageAsync filters only IOException and UnauthorizedAccessException. OpenSubmissionPanelAsync (Line 1086) awaits _submissionGuide.CheckAsync, which performs network work and can throw HttpRequestException or TaskCanceledException. Those escape the filter.

🛡️ Proposed fix
-        ExportPackageCommand = new DelegateCommand(async () => await ExportPackageAsync(), () => CanExport && !IsBusy);
+        ExportPackageCommand = new DelegateCommand(() => _ = RunExportAsync(), () => CanExport && !IsBusy);

Add the guarded wrapper:

private async Task RunExportAsync()
{
    try
    {
        await ExportPackageAsync();
    }
    catch (Exception ex)
    {
        Logger.Error(ex, "Unhandled error while exporting overlay '{Id}'", _document.Id);
        StatusMessage = string.Format(Lang.OverlayDesignerExportFailedWithReason, ex.Message);
        IsBusy = false;
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/OverlayDesignerViewModel.cs` at line 144, Update
ExportPackageCommand to invoke a guarded Task-returning wrapper instead of
passing ExportPackageAsync directly as an async void lambda. Add RunExportAsync
near the command-related methods, await ExportPackageAsync inside it, catch any
unhandled Exception, log it with the document ID, update the export failure
status, and reset IsBusy before returning.
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs-157-177 (1)

157-177: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Constrain every path component to the drafts root before the store writes or deletes. OverlayDraftStore builds file and folder paths from values that originate in overlay.json — the asset names and the overlay ID — and neither value is checked for path separators, .. segments, or a rooted path. Path.Combine then resolves outside DraftsRoot, and the store copies, moves, or recursively deletes there.

  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs#L157-L177: reject rooted asset paths and verify Path.GetFullPath(target) stays under stagingPath before File.Copy.
  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs#L53-L59: reject a document.Id that is not a plain folder name, so finalPath, Commit, and Delete cannot act outside _draftsRoot.
  • FoliconTest/OverlayDraftStoreTests.cs#L37-L46: add tests that save a document with an escaping asset path and with an escaping ID, and assert nothing is created outside DraftsRoot.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs` around lines 157 -
177, Constrain all paths derived from overlay.json to DraftsRoot: in
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs lines 157-177, reject
rooted or escaping asset paths and verify the full target remains under
stagingPath before File.Copy; in
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs lines 53-59, validate
document.Id is a plain folder name so finalPath, Commit, and Delete remain
within _draftsRoot; in FoliconTest/OverlayDraftStoreTests.cs lines 37-46, add
save tests for escaping asset paths and IDs and assert nothing is created
outside DraftsRoot.
FoliCon/Modules/Overlays/Designer/OverlayExporter.cs-235-251 (1)

235-251: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Nested asset paths are copied but never manifested or installed.

CopyReferencedAssets creates subdirectories at line 248, so an asset such as art/base.png is copied into {staging}/art/base.png.

Two later steps only handle a flat layout:

  • WriteManifest at line 277 uses Directory.GetFiles(stagingPath) without recursion. A nested asset is missing from Assets and from Sha256.
  • InstallLocally at line 164 uses Directory.GetFiles(packagePath) without recursion. A nested asset is not installed, so the installed overlay fails to render.

Choose one of two fixes. Either reject nested asset paths at export time, or enumerate recursively in both places and store relative paths.

🐛 Option A: recursive enumeration with relative keys
-        var files = Directory.GetFiles(stagingPath)
-            .Select(Path.GetFileName)
-            .OfType<string>()
+        var files = Directory.GetFiles(stagingPath, "*", SearchOption.AllDirectories)
+            .Select(f => Path.GetRelativePath(stagingPath, f).Replace('\\', '/'))
             // Stable order so the manifest is byte-identical across exports.
             .OrderBy(f => f, StringComparer.Ordinal)
             .ToArray();
-            foreach (var file in Directory.GetFiles(packagePath))
+            foreach (var file in Directory.GetFiles(packagePath, "*", SearchOption.AllDirectories))
             {
-                File.Copy(file, Path.Combine(staging, Path.GetFileName(file)), overwrite: true);
+                var target = Path.Combine(staging, Path.GetRelativePath(packagePath, file));
+                Directory.CreateDirectory(Path.GetDirectoryName(target)!);
+                File.Copy(file, target, overwrite: true);
             }

Run the following script to check whether the validator already forbids nested asset paths:

#!/bin/bash
# Look for path-separator rules on imagePath / opacityMaskPath / fontSource.
fd -i 'OverlayValidator*.cs' --exec rg -n -C 6 'imagePath|ImagePath|OpacityMaskPath|FontSource|DirectorySeparator|Contains\(.[/\\]' {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Designer/OverlayExporter.cs` around lines 235 - 251,
Update WriteManifest and InstallLocally to enumerate staging/package files
recursively so nested assets are included in the manifest, hashes, and local
installation. Store each file using its path relative to the respective root,
preserving nested paths such as art/base.png; keep the existing flat-file
behavior unchanged.
FoliconTest/OverlayTemplateProviderTests.cs-12-15 (1)

12-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add the XamlLoadingCollection attribute to this class.

This class creates a WpfTestHost on line 15 and calls WpfTestHost.Invoke in ten tests. Every other suite in this change that uses WpfTestHost declares [Collection(XamlLoadingCollection.name)]: OverlayTemplatePickerTests line 13, OverlayDesignerGeometryTests line 14, OverlayDesignerIntegrationTests line 22, OverlayDesignerPreviewRendererTests line 12, and OverlayDesignerViewSmokeTests line 22.

Without the attribute, xUnit runs this class in parallel with those collections. It then initializes a second WPF host and queues work on the shared STA thread while another collection uses it. That causes intermittent, order-dependent failures.

💚 Proposed fix
+[Collection(XamlLoadingCollection.name)]
 public class OverlayTemplateProviderTests : IDisposable
 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayTemplateProviderTests.cs` around lines 12 - 15, Add the
xUnit [Collection(XamlLoadingCollection.name)] attribute to the
OverlayTemplateProviderTests class so its WpfTestHost usage is serialized with
the other XAML-loading test suites; leave the existing test implementation
unchanged.
FoliconTest/OverlayDesignerViewSmokeTests.cs-62-86 (1)

62-86: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore the Prism view-model factory after the test.

SetDefaultViewModelFactory changes process-wide static state. This test leaves a factory that returns the disposed viewModel. A later AutoWireViewModel path can receive that instance. Restore Prism 9's default factory in finally:

             finally
             {
+                ViewModelLocationProvider.SetDefaultViewModelFactory(
+                    type => Activator.CreateInstance(type));
                 viewModel.Dispose();
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayDesignerViewSmokeTests.cs` around lines 62 - 86, Restore
Prism’s default view-model factory in the finally block after disposing the test
view model, using the Prism 9 reset/default-factory API. Keep the existing
SetDefaultViewModelFactory setup and test flow unchanged, ensuring later
AutoWireViewModel calls cannot receive the disposed viewModel.
FoliconTest/OverlayStoreViewModelTests.cs-405-409 (1)

405-409: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run the tag-filter helpers on the dispatcher thread.

VisibleOverlays is created by CollectionViewSource.GetDefaultView(Overlays) inside the view-model constructor, and LoadTagFixtureAsync constructs the view model through WpfTestHost.Invoke (Line 427). The resulting ICollectionView has thread affinity to the STA dispatcher thread. Visible and Select run on the xUnit thread instead, as do vm.SearchQuery (Line 398), vm.ClearTagFiltersCommand.Execute() (Line 364), and vm.RefreshCommand.Execute() (Line 385). Each of these reaches the collection view from the wrong thread and can throw at runtime. The tests earlier in this file (Lines 105-115, 132-134) already wrap the same operations in WpfTestHost.Invoke. Make the tag-filter tests consistent.

🔒️ Proposed fix
     private static List<OverlayCardViewModel> Visible(OverlayStoreViewModel vm) =>
-        [.. vm.VisibleOverlays.Cast<OverlayCardViewModel>()];
+        WpfTestHost.Invoke(() => vm.VisibleOverlays.Cast<OverlayCardViewModel>().ToList());
 
     private static void Select(OverlayStoreViewModel vm, string tag, bool selected = true) =>
-        vm.TagFilters.Single(t => string.Equals(t.Tag, tag, StringComparison.OrdinalIgnoreCase)).IsSelected = selected;
+        WpfTestHost.Invoke(() =>
+            vm.TagFilters.Single(t => string.Equals(t.Tag, tag, StringComparison.OrdinalIgnoreCase)).IsSelected = selected);

Wrap the command executions and the SearchQuery assignment in WpfTestHost.Invoke as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayStoreViewModelTests.cs` around lines 405 - 409, Update the
tag-filter tests to access the dispatcher-affine collection view only through
WpfTestHost.Invoke. Wrap the Visible and Select helper bodies, plus SearchQuery
assignments and ClearTagFiltersCommand.Execute and RefreshCommand.Execute calls,
in dispatcher invocations while preserving their existing assertions and test
behavior.
FoliconTest/WpfTestHost.cs-24-69 (1)

24-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

An initialization failure hangs the whole test run.

ready.Set() runs at Line 58, after the application setup at Lines 33-54. If the reflection call or a theme ResourceDictionary load throws, Set() never runs and ready.Wait() at Line 67 blocks forever. The test process then hangs instead of failing. Capture the exception, signal the event in a finally, and rethrow on the calling thread.

🛡️ Proposed fix
         Dispatcher dispatcher = null!;
+        Exception? startupFailure = null;
         using var ready = new ManualResetEventSlim(false);
         var staThread = new Thread(() =>
         {
-            // Creating Application sets Application.Current (required for relative pack URIs).
-            // Set BaseUri to FoliCon assembly so relative URIs like /Resources/... resolve
-            // against FoliCon.dll resources, not the test runner assembly.
-            if (Application.Current == null)
+            try
             {
-                ...
+                // Creating Application sets Application.Current (required for relative pack URIs).
+                if (Application.Current == null)
+                {
+                    // ... existing setup ...
+                }
+                dispatcher = Dispatcher.CurrentDispatcher;
             }
-
-            dispatcher = Dispatcher.CurrentDispatcher;
-            // ReSharper disable once AccessToDisposedClosure - Set() runs before ready is disposed
-            ready.Set();
+            catch (Exception ex)
+            {
+                startupFailure = ex;
+            }
+            finally
+            {
+                // ReSharper disable once AccessToDisposedClosure - Set() runs before ready is disposed
+                ready.Set();
+            }
+
+            if (startupFailure != null)
+            {
+                return;
+            }
             Dispatcher.Run();
         })
@@
         ready.Wait();
+        if (startupFailure != null)
+        {
+            throw new InvalidOperationException("WPF test host failed to start.", startupFailure);
+        }
         return dispatcher;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/WpfTestHost.cs` around lines 24 - 69, Update
StartDispatcherThread so exceptions during application setup, including BaseUri
reflection or ThemeSources resource loading, are captured from the STA thread,
and always signal ready in a finally block. After ready.Wait() returns, rethrow
the captured exception on the calling thread before returning the dispatcher,
while preserving normal Dispatcher.Run startup behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e3c24e-db2d-4c7a-9232-50573b38f66f

📥 Commits

Reviewing files that changed from the base of the PR and between b18ca2f and 8b48c97.

⛔ Files ignored due to path filters (6)
  • FoliconTest/Resources/ReferenceOverlays/alternate_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/faelpessoal-horizontal_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/faelpessoal_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/legacy_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/liaher_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/windows11_reference.png is excluded by !**/*.png
📒 Files selected for processing (118)
  • .gitignore
  • FoliCon/App.xaml
  • FoliCon/App.xaml.cs
  • FoliCon/FoliCon.csproj
  • FoliCon/Models/Constants/GlobalVariables.cs
  • FoliCon/Models/Data/OverlayCatalog.cs
  • FoliCon/Models/Data/OverlayLayerConfig.cs
  • FoliCon/Models/Data/OverlayManifest.cs
  • FoliCon/Models/Data/OverlayStatusFilterOption.cs
  • FoliCon/Models/Data/PosterOverlayDefinition.cs
  • FoliCon/Models/Enums/IconOverlay.cs
  • FoliCon/Models/Enums/OverlayStatusFilter.cs
  • FoliCon/Models/Enums/OverlayStoreSection.cs
  • FoliCon/Models/Usings.cs
  • FoliCon/Modules/Convertor/LocalizedFormatConverter.cs
  • FoliCon/Modules/Extension/DialogServiceExtensions.cs
  • FoliCon/Modules/Extension/StreamExtension.cs
  • FoliCon/Modules/LangProvider.cs
  • FoliCon/Modules/Overlays/Designer/IOverlayEditCommand.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDesignerDocument.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDesignerPreviewRenderer.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs
  • FoliCon/Modules/Overlays/Designer/OverlayEditHistory.cs
  • FoliCon/Modules/Overlays/Designer/OverlayElementKind.cs
  • FoliCon/Modules/Overlays/Designer/OverlayExporter.cs
  • FoliCon/Modules/Overlays/Designer/OverlayGeometry.cs
  • FoliCon/Modules/Overlays/Designer/OverlayPackageLoader.cs
  • FoliCon/Modules/Overlays/Designer/OverlayPackageSerializer.cs
  • FoliCon/Modules/Overlays/Designer/OverlaySubmissionGuide.cs
  • FoliCon/Modules/Overlays/Designer/OverlayTemplateProvider.cs
  • FoliCon/Modules/Overlays/IOverlayProvider.cs
  • FoliCon/Modules/Overlays/IOverlayRepositoryService.cs
  • FoliCon/Modules/Overlays/Internal/OverlayValidator.cs
  • FoliCon/Modules/Overlays/OverlayConstants.cs
  • FoliCon/Modules/Overlays/OverlayPreviewCache.cs
  • FoliCon/Modules/Overlays/OverlayProvider.cs
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs
  • FoliCon/Modules/Overlays/OverlayUpdateChecker.cs
  • FoliCon/Modules/Overlays/OverlayValidationResult.cs
  • FoliCon/Modules/Validation/ApiKeyValidator.cs
  • FoliCon/Modules/utils/IconUtils.cs
  • FoliCon/Properties/Langs/Lang.Designer.cs
  • FoliCon/Properties/Langs/Lang.ar.resx
  • FoliCon/Properties/Langs/Lang.es.resx
  • FoliCon/Properties/Langs/Lang.hi.resx
  • FoliCon/Properties/Langs/Lang.ja.resx
  • FoliCon/Properties/Langs/Lang.pt.resx
  • FoliCon/Properties/Langs/Lang.resx
  • FoliCon/Properties/Langs/Lang.ru.resx
  • FoliCon/Properties/Langs/Lang.zh.resx
  • FoliCon/Resources/Overlays/alternate/overlay.json
  • FoliCon/Resources/Overlays/faelpessoal-horizontal/overlay.json
  • FoliCon/Resources/Overlays/faelpessoal/overlay.json
  • FoliCon/Resources/Overlays/legacy/overlay.json
  • FoliCon/Resources/Overlays/liaher/overlay.json
  • FoliCon/Resources/Overlays/windows11/overlay.json
  • FoliCon/ViewModels/MainWindowViewModel.cs
  • FoliCon/ViewModels/OverlayCardViewModel.cs
  • FoliCon/ViewModels/OverlayDesignerViewModel.cs
  • FoliCon/ViewModels/OverlayElementViewModel.cs
  • FoliCon/ViewModels/OverlayStoreViewModel.cs
  • FoliCon/ViewModels/OverlayTagFilterViewModel.cs
  • FoliCon/ViewModels/OverlayTemplateCardViewModel.cs
  • FoliCon/ViewModels/PreviewerViewModel.cs
  • FoliCon/ViewModels/posterIconConfigViewModel.cs
  • FoliCon/Views/DynamicPosterIcon.xaml
  • FoliCon/Views/DynamicPosterIcon.xaml.cs
  • FoliCon/Views/MainWindow.xaml
  • FoliCon/Views/OverlayDesigner.xaml
  • FoliCon/Views/OverlayDesigner.xaml.cs
  • FoliCon/Views/OverlayStore.xaml
  • FoliCon/Views/OverlayStore.xaml.cs
  • FoliCon/Views/PosterIcon.xaml
  • FoliCon/Views/PosterIcon.xaml.cs
  • FoliCon/Views/PosterIconAlt.xaml
  • FoliCon/Views/PosterIconAlt.xaml.cs
  • FoliCon/Views/PosterIconFaelpessoal.xaml
  • FoliCon/Views/PosterIconFaelpessoal.xaml.cs
  • FoliCon/Views/PosterIconFaelpessoalHorizontal.xaml
  • FoliCon/Views/PosterIconFaelpessoalHorizontal.xaml.cs
  • FoliCon/Views/PosterIconLiaher.xaml
  • FoliCon/Views/PosterIconLiaher.xaml.cs
  • FoliCon/Views/PosterIconWindows11.xaml
  • FoliCon/Views/PosterIconWindows11.xaml.cs
  • FoliCon/Views/Previewer.xaml
  • FoliCon/Views/posterIconConfig.xaml
  • FoliCon/internal-nlog.txt
  • Folicon.sln
  • FoliconTest/DynamicPosterIconParityTests.cs
  • FoliconTest/FoliconTest.csproj
  • FoliconTest/GlobalUsings.cs
  • FoliconTest/GoldenImageParityTests.cs
  • FoliconTest/OverlayDesignerDocumentTests.cs
  • FoliconTest/OverlayDesignerGeometryTests.cs
  • FoliconTest/OverlayDesignerIntegrationTests.cs
  • FoliconTest/OverlayDesignerPreviewRendererTests.cs
  • FoliconTest/OverlayDesignerViewModelTests.cs
  • FoliconTest/OverlayDesignerViewSmokeTests.cs
  • FoliconTest/OverlayDraftStoreTests.cs
  • FoliconTest/OverlayEditCommandTests.cs
  • FoliconTest/OverlayExporterTests.cs
  • FoliconTest/OverlayLocalizationTests.cs
  • FoliconTest/OverlayManifestTests.cs
  • FoliconTest/OverlayPackageLoaderTests.cs
  • FoliconTest/OverlayProviderTests.cs
  • FoliconTest/OverlayRepositoryServiceTests.cs
  • FoliconTest/OverlayStoreStyleResolutionTests.cs
  • FoliconTest/OverlayStoreViewModelTests.cs
  • FoliconTest/OverlayStoreViewSmokeTests.cs
  • FoliconTest/OverlaySubmissionGuideTests.cs
  • FoliconTest/OverlayTemplatePickerTests.cs
  • FoliconTest/OverlayTemplateProviderTests.cs
  • FoliconTest/OverlayValidatorDetailedTests.cs
  • FoliconTest/OverlayValidatorTests.cs
  • FoliconTest/PosterIconConfigViewSmokeTests.cs
  • FoliconTest/WpfTestHost.cs
  • FoliconTest/XamlLoadingCollection.cs
  • README.md
💤 Files with no reviewable changes (2)
  • FoliCon/Modules/Validation/ApiKeyValidator.cs
  • FoliCon/Models/Enums/IconOverlay.cs

Comment thread FoliCon/Modules/Overlays/Designer/OverlayExporter.cs
Comment thread FoliCon/Modules/Overlays/OverlayRepositoryService.cs
Comment thread FoliCon/Modules/utils/IconUtils.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
FoliCon/Modules/Overlays/OverlayRepositoryService.cs (2)

261-289: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Restore the existing overlay if staged replacement fails.

At Line 265, Directory.Delete(finalDir, true) removes the installed overlay before Directory.Move(tmpDir, finalDir) succeeds. If the move fails, the catch block also removes tmpDir. A direct call to InstallOverlayAsync can then lose the prior installation.

Move the existing directory to a backup sibling first. Restore it if the staged move fails. Remove the backup only after the new directory is active.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 261 - 289,
Update the replacement logic in InstallOverlayAsync to move an existing finalDir
to a backup sibling before promoting tmpDir, rather than deleting it. If
Directory.Move(tmpDir, finalDir) fails, restore the backup to finalDir before
rethrowing; once the new overlay is active, remove the backup and retain the
existing cleanup behavior for tmpDir.

124-141: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate cancellation from catalog I/O.

At Line 135 and Line 173, the broad catch (Exception) handlers also catch OperationCanceledException. A canceled request can return a stale or empty catalog instead of completing as canceled.

Rethrow OperationCanceledException before the fallback handlers.

Proposed fix
+        catch (OperationCanceledException)
+        {
+            throw;
+        }
         catch (Exception ex)
         {

Also applies to: 173-199

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 124 - 141,
Update the exception handlers surrounding catalog I/O in the repository methods
containing the disk-cache load and network fallback so
OperationCanceledException is rethrown before the broad Exception fallback.
Preserve the existing warning and recovery behavior for non-cancellation
failures while ensuring cancellation propagates to the caller instead of
returning cached or fetched catalog data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs`:
- Around line 307-313: Update the asset download flow around VerifyAsset to use
ResponseHeadersRead, reject responses whose Content-Length exceeds the
applicable limit, and stream the response while hashing and writing with a hard
byte limit instead of GetByteArrayAsync. Apply an explicit size limit to
overlay.json as well, and ensure package validation occurs only after every
streamed asset passes its limit and is safely written.

In `@FoliconTest/OverlayRepositoryServiceTests.cs`:
- Around line 151-173: Update
UninstallOverlay_InvalidId_DoesNotDeleteOutsideTheOverlayDirectory and
InvalidOverlayIds_AreNeverConsideredInstalledOrVersioned to create overlay.json
and sentinel files at the actual paths resolved from _userOverlaysDir by the
traversal inputs, including the "../../../outside" and "../outside" targets.
Assert UninstallOverlayAsync neither deletes nor reads those targets, and verify
invalid IDs remain uninstalled and unversioned using populated traversal targets
rather than empty locations.

---

Outside diff comments:
In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs`:
- Around line 261-289: Update the replacement logic in InstallOverlayAsync to
move an existing finalDir to a backup sibling before promoting tmpDir, rather
than deleting it. If Directory.Move(tmpDir, finalDir) fails, restore the backup
to finalDir before rethrowing; once the new overlay is active, remove the backup
and retain the existing cleanup behavior for tmpDir.
- Around line 124-141: Update the exception handlers surrounding catalog I/O in
the repository methods containing the disk-cache load and network fallback so
OperationCanceledException is rethrown before the broad Exception fallback.
Preserve the existing warning and recovery behavior for non-cancellation
failures while ensuring cancellation propagates to the caller instead of
returning cached or fetched catalog data.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f92e2d3-4660-431a-ad9a-540ad470b55b

📥 Commits

Reviewing files that changed from the base of the PR and between 8b48c97 and d1457c0.

📒 Files selected for processing (6)
  • FoliCon/Modules/Overlays/Designer/OverlayExporter.cs
  • FoliCon/Modules/Overlays/Internal/OverlayValidator.cs
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs
  • FoliCon/Modules/utils/IconUtils.cs
  • FoliconTest/OverlayExporterTests.cs
  • FoliconTest/OverlayRepositoryServiceTests.cs
🚧 Files skipped from review as they are similar to previous changes (4)
  • FoliCon/Modules/Overlays/Internal/OverlayValidator.cs
  • FoliCon/Modules/Overlays/Designer/OverlayExporter.cs
  • FoliconTest/OverlayExporterTests.cs
  • FoliCon/Modules/utils/IconUtils.cs

Comment thread FoliCon/Modules/Overlays/OverlayRepositoryService.cs
Comment on lines +151 to +173
[Fact]
public async Task UninstallOverlay_InvalidId_DoesNotDeleteOutsideTheOverlayDirectory()
{
var outsideDirectory = Path.Combine(_tempRoot, "outside");
Directory.CreateDirectory(outsideDirectory);
var sentinel = Path.Combine(outsideDirectory, "keep.txt");
File.WriteAllText(sentinel, "must survive");

var service = CreateService();

await Assert.ThrowsAsync<ArgumentException>(() => service.UninstallOverlayAsync("../../../outside"));

Assert.True(File.Exists(sentinel));
}

[Fact]
public void InvalidOverlayIds_AreNeverConsideredInstalledOrVersioned()
{
var service = CreateService();

Assert.False(service.IsOverlayInstalled("../outside"));
Assert.Null(service.GetInstalledVersion("../outside"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Target the path that the traversal input resolves to.

"../../../outside" resolves from _userOverlaysDir to <tempRoot>/AppData/outside. The sentinel is at <tempRoot>/outside, so it survives even if UninstallOverlayAsync permits traversal.

"../outside" also resolves to an empty location. The installed and version assertions therefore do not prove that the service rejects the invalid ID.

Create an overlay.json and sentinel at each resolved traversal target. Then assert that the service does not read or delete them.

Proposed test adjustment
- var outsideDirectory = Path.Combine(_tempRoot, "outside");
+ var traversalId = Path.Combine("..", "..", "..", "..", "outside");
+ var outsideDirectory = Path.GetFullPath(
+     Path.Combine(_userOverlaysDir, traversalId));
  Directory.CreateDirectory(outsideDirectory);
  var sentinel = Path.Combine(outsideDirectory, "keep.txt");
  File.WriteAllText(sentinel, "must survive");

  var service = CreateService();

- await Assert.ThrowsAsync<ArgumentException>(() => service.UninstallOverlayAsync("../../../outside"));
+ await Assert.ThrowsAsync<ArgumentException>(() => service.UninstallOverlayAsync(traversalId));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayRepositoryServiceTests.cs` around lines 151 - 173, Update
UninstallOverlay_InvalidId_DoesNotDeleteOutsideTheOverlayDirectory and
InvalidOverlayIds_AreNeverConsideredInstalledOrVersioned to create overlay.json
and sentinel files at the actual paths resolved from _userOverlaysDir by the
traversal inputs, including the "../../../outside" and "../outside" targets.
Assert UninstallOverlayAsync neither deletes nor reads those targets, and verify
invalid IDs remain uninstalled and unversioned using populated traversal targets
rather than empty locations.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@FoliconTest/OverlayRepositoryServiceTests.cs`:
- Around line 154-159: Update the traversal fixture in the relevant test to use
a unique target beneath _tempRoot while remaining outside _userOverlaysDir.
Replace the fixed traversalId and excessive parent-directory traversal with a
per-test unique directory name and only enough .. segments to reach a location
within _tempRoot, preventing shared-directory collisions and cleanup leaks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: a223e174-1aff-4df2-9432-579c73820e99

📥 Commits

Reviewing files that changed from the base of the PR and between d1457c0 and d9c0ac5.

📒 Files selected for processing (3)
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs
  • FoliconTest/OverlayRepositoryServiceTests.cs
  • LanguageClassGenerator/LanguageClassGenerator.csproj

Comment on lines +154 to +159
var traversalId = Path.Combine("..", "..", "..", "..", "outside");
var outsideDirectory = Path.GetFullPath(
Path.Combine(_userOverlaysDir, traversalId));
Directory.CreateDirectory(outsideDirectory);
var sentinel = Path.Combine(outsideDirectory, "keep.txt");
File.WriteAllText(sentinel, "must survive");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the traversal fixture inside _tempRoot.

traversalId resolves to Path.Combine(Path.GetTempPath(), "outside"). The test writes a fixed shared directory and Dispose does not remove it. Parallel tests can collide with this directory.

Use a unique target with fewer .. segments so it remains under _tempRoot but outside _userOverlaysDir.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayRepositoryServiceTests.cs` around lines 154 - 159, Update
the traversal fixture in the relevant test to use a unique target beneath
_tempRoot while remaining outside _userOverlaysDir. Replace the fixed
traversalId and excessive parent-directory traversal with a per-test unique
directory name and only enough .. segments to reach a location within _tempRoot,
preventing shared-directory collisions and cleanup leaks.

@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants