Skip to content
63 changes: 63 additions & 0 deletions CollapseLauncher/Classes/Helper/WindowUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ internal enum WindowBackdropKind
internal static class WindowUtility
{
private static event EventHandler<RectInt32[]>? DragAreaChangeEvent;
internal static event Action<string[], PointInt32>? FileDropEvent;

private static nint _oldMainWndProcPtr;
private static nint _oldDesktopSiteBridgeWndProcPtr;
Expand Down Expand Up @@ -417,6 +418,52 @@ internal static void RegisterWindow(this Window window)
FileDialogNative.InitHandlerPointer(CurrentWindowPtr);
}

internal static void SetFileDropEnabled(bool isEnabled)
{
const uint WM_COPYDATA = 0x004A;
const uint WM_COPYGLOBALDATA = 0x0049;
const uint WM_DROPFILES = 0x0233;
const uint MSGFLT_RESET = 0;
const uint MSGFLT_ALLOW = 1;

nint windowHandle = CurrentWindowPtr;
if (windowHandle == nint.Zero)
{
return;
}

uint messageFilterAction = isEnabled ? MSGFLT_ALLOW : MSGFLT_RESET;
PInvoke.ChangeWindowMessageFilterEx(windowHandle, WM_DROPFILES, messageFilterAction, nint.Zero);
PInvoke.ChangeWindowMessageFilterEx(windowHandle, WM_COPYDATA, messageFilterAction, nint.Zero);
PInvoke.ChangeWindowMessageFilterEx(windowHandle, WM_COPYGLOBALDATA, messageFilterAction, nint.Zero);
PInvoke.DragAcceptFiles(windowHandle, isEnabled);
}

internal static bool TryGetExternalDragPosition(out PointInt32 dragPosition)
{
const int VK_LBUTTON = 0x01;
const int VK_RBUTTON = 0x02;
const int KeyPressed = 0x8000;

dragPosition = default;
nint windowHandle = CurrentWindowPtr;
if (windowHandle == nint.Zero)
{
return false;
}

bool isMouseButtonPressed = (PInvoke.GetAsyncKeyState(VK_LBUTTON) & KeyPressed) != 0 ||
(PInvoke.GetAsyncKeyState(VK_RBUTTON) & KeyPressed) != 0;
if (!isMouseButtonPressed || !PInvoke.GetCursorPos(out POINTL cursorPosition))
{
return false;
}

PInvoke.ScreenToClient(windowHandle, ref cursorPosition);
dragPosition = new PointInt32(cursorPosition.x, cursorPosition.y);
return true;
}

#region Drag Area Handler

private static void InstallDragAreaChangeMonitor()
Expand Down Expand Up @@ -496,6 +543,7 @@ private static nint MainWndProc(nint hwnd, uint msg, nuint wParam, nint lParam)
const uint WM_ACTIVATE = 0x0006;
const uint WM_QUERYENDSESSION = 0x0011;
const uint WM_ENDSESSION = 0x0016;
const uint WM_DROPFILES = 0x0233;

switch (msg)
{
Expand Down Expand Up @@ -643,6 +691,21 @@ private static nint MainWndProc(nint hwnd, uint msg, nuint wParam, nint lParam)
(CurrentWindow as MainWindow)?.CloseApp();
}
break;
case WM_DROPFILES:
if (NativeFileDrop.TryHandleFileDrop((nint)wParam,
out string[]? files,
out POINTL dropPoint,
out Exception? ex))
{
FileDropEvent?.Invoke(files, new PointInt32(dropPoint.x, dropPoint.y));
}
Comment on lines +694 to +701

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The FileDropEvent is invoked with a nullable files array, but the OnNativeFileDrop handler and subsequent ImportPlugins call access it without a null check, risking a NullReferenceException.
Severity: HIGH

Suggested Fix

Add a null check for the files variable in WindowUtility.cs before invoking the FileDropEvent. For example: if (files != null) { FileDropEvent?.Invoke(files, ...); }. This ensures the event is only fired with a valid file list.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: CollapseLauncher/Classes/Helper/WindowUtility.cs#L694-L701

Potential issue: The `NativeFileDrop.TryHandleFileDrop` method can return a `null`
`files` array, as indicated by its `out string[]?` parameter. The `FileDropEvent` is
then invoked with this potentially null array. The event handler, `OnNativeFileDrop`,
expects a non-nullable `string[]` and passes it to the `ImportPlugins` method, which
immediately accesses `selectedFiles.Length` without any null validation. This will cause
a `NullReferenceException` if a file drop operation results in a `true` return from
`TryHandleFileDrop` but with a `null` file list. The exception occurs in an `async void`
handler, making it difficult to handle gracefully.

Also affects:

  • CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml.cs:227~227


if (ex != null)
{
ErrorSender.SendException(ex);
SentryHelper.ExceptionHandler(ex);
}
return 0;
}

return PInvoke.CallWindowProc(_oldMainWndProcPtr, hwnd, msg, wParam, lParam);
Expand Down
73 changes: 61 additions & 12 deletions CollapseLauncher/Classes/Plugins/PluginImporter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Hi3Helper.Plugin.Core.Update;
using Hi3Helper.Shared.Region;
using CollapseLauncher.Helper.StreamUtility;
using System;
using System.Data;
using System.IO;
Expand All @@ -15,6 +16,14 @@ internal static partial class PluginImporter
public static async Task<PluginInfo> AutoGetImportFromPath(string filePath, CancellationToken token)
{
bool isFromPackage = filePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase);
bool isFromManifest = string.Equals(Path.GetFileName(filePath),
PluginManager.ManifestPrefix,
StringComparison.OrdinalIgnoreCase);

if (!isFromPackage && !isFromManifest)
{
throw new NotSupportedException($"{Path.GetFileName(filePath)} is not supported. Import a .zip package or a file named manifest.json.");
}

using IPluginSource pluginSource = isFromPackage ?
await ZipPluginSource.GetSourceFrom(filePath, token) :
Expand All @@ -24,6 +33,7 @@ await ZipPluginSource.GetSourceFrom(filePath, token) :
string pluginDirName = $"Hi3Helper.Plugin.{pluginExecNameNoExt}";
string pluginBaseDir = Path.Combine(LauncherConfig.AppPluginFolder, pluginDirName);
string pluginRelName = Path.Combine(pluginDirName, pluginSource.Manifest.MainLibraryName);
string pluginEntryPath = GetContainedPath(pluginBaseDir, pluginSource.Manifest.MainLibraryName);

if (PluginManager.PluginInstances.ContainsKey(pluginBaseDir) ||
PluginManager.PluginInstances.Values
Expand All @@ -44,25 +54,64 @@ await ZipPluginSource.GetSourceFrom(filePath, token) :
});
}

string pluginEntryPath = Path.Combine(pluginBaseDir, pluginSource.Manifest.MainLibraryName);
foreach (PluginManifestAssetInfo asset in pluginSource.Manifest.Assets)
Directory.CreateDirectory(LauncherConfig.AppPluginFolder);
string stagingDir = Path.Combine(LauncherConfig.AppPluginFolder, $".import-{Guid.NewGuid():N}");
string cleanupDir = stagingDir;

try
{
string assetFullPath = Path.Combine(pluginBaseDir, asset.FilePath);
string? assetFullDir = Path.GetDirectoryName(assetFullPath);
foreach (PluginManifestAssetInfo asset in pluginSource.Manifest.Assets)
{
string assetFullPath = GetContainedPath(stagingDir, asset.FilePath);
string? assetFullDir = Path.GetDirectoryName(assetFullPath);

if (!string.IsNullOrEmpty(assetFullDir))
{
Directory.CreateDirectory(assetFullDir);
}

await using FileStream assetStream = File.Create(assetFullPath);
await using Stream assetPluginSourceStream = await pluginSource.GetAssetStream(asset, token);

await assetPluginSourceStream.CopyToAsync(assetStream, token);
}

Directory.Move(stagingDir, pluginBaseDir);
cleanupDir = pluginBaseDir;

if (!string.IsNullOrEmpty(assetFullDir))
PluginInfo pluginInfo = new(pluginEntryPath,
pluginRelName,
pluginSource.Manifest);
cleanupDir = string.Empty;
return pluginInfo;
}
catch
{
if (!string.IsNullOrEmpty(cleanupDir))
{
Directory.CreateDirectory(assetFullDir);
new DirectoryInfo(cleanupDir).TryDeleteDirectory(true);
}

await using FileStream assetStream = File.Create(assetFullPath);
await using Stream assetPluginSourceStream = await pluginSource.GetAssetStream(asset, token);
throw;
}
}

private static string GetContainedPath(string baseDirectory, string relativePath)
{
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new InvalidDataException("The plugin manifest contains an empty file path.");
}

await assetPluginSourceStream.CopyToAsync(assetStream, token);
string baseFullPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(baseDirectory));
string fileFullPath = Path.GetFullPath(relativePath, baseFullPath);
string basePrefix = baseFullPath + Path.DirectorySeparatorChar;

if (!fileFullPath.StartsWith(basePrefix, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException($"The plugin manifest path '{relativePath}' points outside the plugin directory.");
}

return new PluginInfo(pluginEntryPath,
pluginRelName,
pluginSource.Manifest);
return fileFullPath;
}
}
4 changes: 2 additions & 2 deletions CollapseLauncher/XAMLs/MainApp/Pages/HomePage.Background.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ private bool IsInMultiBackgroundPipsPagerGridHoverArea(PointerRoutedEventArgs? a
}
else
{
PInvoke.GetCursorPos(out pointerPos).ThrowOnFailure();
PInvoke.ScreenToClient(WindowUtility.CurrentWindowPtr, ref pointerPos).ThrowOnFailure();
PInvoke.GetCursorPos(out pointerPos);
PInvoke.ScreenToClient(WindowUtility.CurrentWindowPtr, ref pointerPos);
}

double xFrom = gridPos.X;
Expand Down
27 changes: 25 additions & 2 deletions CollapseLauncher/XAMLs/MainApp/Pages/PluginManagerPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -823,12 +823,17 @@
</ResourceDictionary>
</Grid.Resources>
<Button x:Name="ImportBoxButton"
AllowDrop="True"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
extension:UIElementExtensions.CursorType="Hand"
Background="{StaticResource ImportBoxButtonBackground}"
Click="OnClickImportButton"
CornerRadius="20">
CornerRadius="20"
DragEnter="OnDragEnterImportBox"
DragLeave="OnDragLeaveImportBox"
DragOver="OnDragOverImportBox"
Drop="OnDropImportBox">
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<behaviors:StartAnimationAction Animation="{x:Bind ImportBoxPlusGridEnterAnimation}" />
Expand Down Expand Up @@ -906,7 +911,7 @@
</animations:AnimationSet>
</animations:Explicit.Animations>

<FontIcon FontSize="48z"
<FontIcon FontSize="48"
Glyph="&#xE710;" />
</Button>
<StackPanel Grid.Row="1"
Expand Down Expand Up @@ -938,6 +943,24 @@
</StackPanel>
</Grid>
</Button>
<Grid x:Name="ImportDropIndicator"
IsHitTestVisible="False"
Opacity="0">
<Grid.OpacityTransition>
<ScalarTransition Duration="0:0:.18" />
</Grid.OpacityTransition>
<Border Margin="3"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="17"
Opacity=".08" />
<Rectangle Margin="2"
RadiusX="18"
RadiusY="18"
Stroke="{ThemeResource AccentColor}"
StrokeDashArray="1,2"
StrokeDashCap="Round"
StrokeThickness="3" />
</Grid>
</Grid>
<Button x:Name="OpenPluginCatalogBottomLeftButton"
Grid.Column="0"
Expand Down
Loading
Loading