Merge remote-tracking branch 'origin/main' into dev/migrie/f/pincushion

This commit is contained in:
Mike Griese
2026-02-19 16:21:08 -06:00
12 changed files with 447 additions and 144 deletions

View File

@@ -25,6 +25,7 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
"env",
"environment",
"manifest",
"log",
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
public IEnumerable<SanitizationRule> GetRules()
@@ -61,6 +62,11 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
return full;
}
if (IsVersionSegment(file))
{
return full;
}
string stem, ext;
if (dot > 0 && dot < file.Length - 1)
{
@@ -106,4 +112,30 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
var maskedCount = Math.Max(1, stem.Length - keep);
return stem[..keep] + new string('*', maskedCount);
}
private static bool IsVersionSegment(string file)
{
var dotIndex = file.IndexOf('.');
if (dotIndex <= 0 || dotIndex == file.Length - 1)
{
return false;
}
var hasDot = false;
foreach (var ch in file)
{
if (ch == '.')
{
hasDot = true;
continue;
}
if (!char.IsDigit(ch))
{
return false;
}
}
return hasDot;
}
}

View File

@@ -11,7 +11,9 @@ internal sealed partial class NetworkRuleProvider : ISanitizationRuleProvider
{
public IEnumerable<SanitizationRule> GetRules()
{
yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses");
// Disabled for now as these rules can be too aggressive and cause over-sanitization, especially in scenarios like
// error report sanitization where we want to preserve as much useful information as possible while still protecting sensitive data.
// yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses");
yield return new(Ipv6BracketedRx(), "[IP6_REDACTED]", "IPv6 addresses (bracketed/with port)");
yield return new(Ipv6Rx(), "[IP6_REDACTED]", "IPv6 addresses");
yield return new(MacAddressRx(), "[MAC_ADDRESS_REDACTED]", "MAC addresses");

View File

@@ -25,52 +25,56 @@ internal sealed partial class PiiRuleProvider : ISanitizationRuleProvider
private static partial Regex EmailRx();
[GeneratedRegex("""
(?xi)
# ---------- boundaries ----------
(?<!\w) # not after a letter/digit/underscore
(?<![A-Za-z0-9]-) # avoid starting inside hyphenated tokens (GUID middles, etc.)
(?xi)
# ---------- boundaries ----------
(?<!\w) # not after a letter/digit/underscore
(?<![A-Za-z0-9]-) # avoid starting inside hyphenated tokens (GUID middles, etc.)
# ---------- global do-not-match guards ----------
(?! # ISO date (yyyy-mm-dd / yyyy.mm.dd / yyyy/mm/dd)
(?:19|20)\d{2}[-./](?:0[1-9]|1[0-2])[-./](?:0[1-9]|[12]\d|3[01])\b
)
(?! # EU date (dd-mm-yyyy / dd.mm.yyyy / dd/mm/yyyy)
(?:0[1-9]|[12]\d|3[01])[-./](?:0[1-9]|1[0-2])[-./](?:19|20)\d{2}\b
)
(?! # ISO datetime like 2025-08-24T14:32[:ss][Z|±hh:mm]
(?:19|20)\d{2}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\b
)
(?!\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b) # IPv4 with optional :port
(?!\b[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\b) # GUID, lowercase
(?!\b[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}\b) # GUID, uppercase
(?!\bv?\d+(?:\.\d+){2,}\b) # semantic/file versions like 1.2.3 or 10.0.22631.3448
(?!\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b) # MAC address
# ---------- global do-not-match guards ----------
(?! # ISO date (yyyy-mm-dd / yyyy.mm.dd / yyyy/mm/dd)
(?:19|20)\d{2}[-./](?:0[1-9]|1[0-2])[-./](?:0[1-9]|[12]\d|3[01])\b
)
(?! # EU date (dd-mm-yyyy / dd.mm.yyyy / dd/mm/yyyy)
(?:0[1-9]|[12]\d|3[01])[-./](?:0[1-9]|1[0-2])[-./](?:19|20)\d{2}\b
)
(?! # ISO datetime like 2025-08-24T14:32[:ss][Z|±hh:mm]
(?:19|20)\d{2}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\b
)
(?!\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b) # IPv4 with optional :port
(?!\b[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\b) # GUID, lowercase
(?!\b[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}\b) # GUID, uppercase
(?!\bv?\d+(?:\.\d+){2,}\b) # semantic/file versions like 1.2.3 or 10.0.22631.3448
(?!\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b) # MAC address
# ---------- digit budget ----------
(?=(?:\D*\d){7,15}) # 715 digits in total
# ---------- digit budget ----------
(?=(?:[^\r\n]*\d){7,15}[^\r\n]*(?:\r\n|$))
(?=(?:\D*\d){7,15}) # 715 digits in total
# ---------- number body ----------
(?:
# A with explicit country code, allow compact digits (E.164-ish) or grouped
(?:\+|00)[1-9]\d{0,2}
(?:
[\p{Zs}.\-\/]*\d{6,14}
|
[\p{Zs}.\-\/]* (?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
|
# ---------- number body ----------
(?:
# A with explicit country code, allow compact digits (E.164-ish) or grouped
(?:\+|00)[1-9]\d{0,2}
(?:
[\p{Zs}.\-\/]*\d{6,14}
|
[\p{Zs}.\-\/]* (?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
|
# B no country code => require separators between blocks (avoid plain big ints)
(?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
(?:\(\d{1,4}\)|\d{1,4})
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
)
# ---------- optional extension ----------
(?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?<ext>\d{1,6}))?
# ---------- optional extension ----------
(?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?<ext>\d{1,6}))?
(?!-\w) # don't end just before '-letter'/'-digit'
""",
SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace, SanitizerDefaults.DefaultMatchTimeoutMs)]
# ---------- end boundary (allow whitespace/newlines at edges) ----------
(?!-\w) # don't end just before '-letter'/'-digit'
(?!\w) # don't be immediately followed by a word char
""",
SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace,
SanitizerDefaults.DefaultMatchTimeoutMs)]
private static partial Regex PhoneRx();
[GeneratedRegex(@"\b\d{3}-\d{2}-\d{4}\b",

View File

@@ -11,37 +11,42 @@ public sealed class WindowPosition
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; set; }
public int X { get; init; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; set; }
public int Y { get; init; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; set; }
public int Width { get; init; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; set; }
public int Height { get; init; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; set; }
public int ScreenWidth { get; init; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; set; }
public int ScreenHeight { get; init; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; set; }
public int Dpi { get; init; }
/// <summary>
/// Gets a value indicating whether the width and height of the window are valid (greater than 0).
/// </summary>
public bool IsSizeValid => Width > 0 && Height > 0;
/// <summary>
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.

View File

@@ -18,7 +18,7 @@ internal static class WindowPositionHelper
private const int MinimumVisibleSize = 100;
private const int DefaultDpi = 96;
public static PointInt32? CalculateCenteredPosition(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
public static RectInt32? CenterOnDisplay(DisplayArea? displayArea, SizeInt32 windowSize, int windowDpi)
{
if (displayArea is null)
{
@@ -32,15 +32,9 @@ internal static class WindowPositionHelper
}
var targetDpi = GetDpiForDisplay(displayArea);
var predictedSize = ScaleSize(windowSize, windowDpi, targetDpi);
// Clamp to work area
var width = Math.Min(predictedSize.Width, workArea.Width);
var height = Math.Min(predictedSize.Height, workArea.Height);
return new PointInt32(
workArea.X + ((workArea.Width - width) / 2),
workArea.Y + ((workArea.Height - height) / 2));
var scaledSize = ScaleSize(windowSize, windowDpi, targetDpi);
var clampedSize = ClampSize(scaledSize.Width, scaledSize.Height, workArea);
return CenterRectInWorkArea(clampedSize, workArea);
}
/// <summary>
@@ -74,6 +68,10 @@ internal static class WindowPositionHelper
savedRect = savedRect with { Width = DefaultWidth, Height = DefaultHeight };
}
// Remember the original size before DPI scaling - needed to compute
// gaps relative to the old screen when repositioning across displays.
var originalSize = new SizeInt32(savedRect.Width, savedRect.Height);
if (targetDpi != savedDpi)
{
savedRect = ScaleRect(savedRect, savedDpi, targetDpi);
@@ -81,12 +79,17 @@ internal static class WindowPositionHelper
var clampedSize = ClampSize(savedRect.Width, savedRect.Height, workArea);
var shouldRecenter = hasInvalidSize ||
IsOffscreen(savedRect, workArea) ||
savedScreenSize.Width != workArea.Width ||
savedScreenSize.Height != workArea.Height;
if (hasInvalidSize)
{
return CenterRectInWorkArea(clampedSize, workArea);
}
if (shouldRecenter)
if (savedScreenSize.Width != workArea.Width || savedScreenSize.Height != workArea.Height)
{
return RepositionRelativeToWorkArea(savedRect, savedScreenSize, originalSize, clampedSize, workArea);
}
if (IsOffscreen(savedRect, workArea))
{
return CenterRectInWorkArea(clampedSize, workArea);
}
@@ -126,27 +129,92 @@ internal static class WindowPositionHelper
private static RectInt32 ScaleRect(RectInt32 rect, int fromDpi, int toDpi)
{
if (fromDpi <= 0 || toDpi <= 0 || fromDpi == toDpi)
{
return rect;
}
// Don't scale position, that's absolute coordinates in virtual screen space
var scale = (double)toDpi / fromDpi;
return new RectInt32(
(int)Math.Round(rect.X * scale),
(int)Math.Round(rect.Y * scale),
rect.X,
rect.Y,
(int)Math.Round(rect.Width * scale),
(int)Math.Round(rect.Height * scale));
}
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea) =>
new(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
private static SizeInt32 ClampSize(int width, int height, RectInt32 workArea)
{
return new SizeInt32(Math.Min(width, workArea.Width), Math.Min(height, workArea.Height));
}
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea) =>
new(
private static RectInt32 RepositionRelativeToWorkArea(RectInt32 savedRect, SizeInt32 savedScreenSize, SizeInt32 originalSize, SizeInt32 clampedSize, RectInt32 workArea)
{
// Treat each axis as a 3-zone grid (start / center / end) so that
// edge-snapped windows stay snapped and centered windows stay centered.
// We don't store the old work area origin, so we use the current one as a
// best estimate (correct when the same physical display changed resolution/DPI/taskbar).
var newX = ScaleAxisByZone(savedRect.X, originalSize.Width, clampedSize.Width, workArea.X, savedScreenSize.Width, workArea.Width);
var newY = ScaleAxisByZone(savedRect.Y, originalSize.Height, clampedSize.Height, workArea.Y, savedScreenSize.Height, workArea.Height);
newX = Math.Clamp(newX, workArea.X, Math.Max(workArea.X, workArea.X + workArea.Width - clampedSize.Width));
newY = Math.Clamp(newY, workArea.Y, Math.Max(workArea.Y, workArea.Y + workArea.Height - clampedSize.Height));
return new RectInt32(newX, newY, clampedSize.Width, clampedSize.Height);
}
/// <summary>
/// Repositions a window along one axis using a 3-zone model (start / center / end).
/// The zone is determined by which third of the old screen the window center falls in.
/// Uses <paramref name="oldWindowSize"/> (pre-DPI-scaling) for gap calculations against
/// the old screen, and <paramref name="newWindowSize"/> (post-scaling) for placement on the new screen.
/// </summary>
private static int ScaleAxisByZone(int savedPos, int oldWindowSize, int newWindowSize, int workAreaOrigin, int oldScreenSize, int newScreenSize)
{
if (oldScreenSize <= 0 || newScreenSize <= 0)
{
return savedPos;
}
var gapFromStart = savedPos - workAreaOrigin;
var windowCenter = gapFromStart + (oldWindowSize / 2);
if (windowCenter >= oldScreenSize / 3 && windowCenter <= oldScreenSize * 2 / 3)
{
// Center zone - keep centered
return workAreaOrigin + ((newScreenSize - newWindowSize) / 2);
}
var gapFromEnd = oldScreenSize - gapFromStart - oldWindowSize;
if (gapFromStart <= gapFromEnd)
{
// Start zone - preserve proportional distance from start edge
var rel = (double)gapFromStart / oldScreenSize;
return workAreaOrigin + (int)Math.Round(rel * newScreenSize);
}
else
{
// End zone - preserve proportional distance from end edge
var rel = (double)gapFromEnd / oldScreenSize;
return workAreaOrigin + newScreenSize - newWindowSize - (int)Math.Round(rel * newScreenSize);
}
}
private static RectInt32 CenterRectInWorkArea(SizeInt32 size, RectInt32 workArea)
{
return new RectInt32(
workArea.X + ((workArea.Width - size.Width) / 2),
workArea.Y + ((workArea.Height - size.Height) / 2),
size.Width,
size.Height);
}
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea) =>
rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
private static bool IsOffscreen(RectInt32 rect, RectInt32 workArea)
{
return rect.X + MinimumVisibleSize > workArea.X + workArea.Width ||
rect.X + rect.Width - MinimumVisibleSize < workArea.X ||
rect.Y + MinimumVisibleSize > workArea.Y + workArea.Height ||
rect.Y + rect.Height - MinimumVisibleSize < workArea.Y;
}
}

View File

@@ -72,6 +72,7 @@ public sealed partial class MainWindow : WindowEx,
private readonly IThemeService _themeService;
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
private bool _ignoreHotKeyWhenFullScreen = true;
private bool _suppressDpiChange;
private bool _themeServiceInitialized;
// Session tracking for telemetry
@@ -127,6 +128,16 @@ public sealed partial class MainWindow : WindowEx,
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
RestoreWindowPosition();
@@ -153,16 +164,6 @@ public sealed partial class MainWindow : WindowEx,
SizeChanged += WindowSizeChanged;
RootElement.Loaded += RootElementLoaded;
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
// Load our settings, and then also wire up a settings changed handler
HotReloadSettings();
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
@@ -213,6 +214,11 @@ public sealed partial class MainWindow : WindowEx,
// Now that our content has loaded, we can update our draggable regions
UpdateRegionsForCustomTitleBar();
// Also update regions when DPI changes. SizeChanged only fires when the logical
// (DIP) size changes — a DPI change that scales the physical size while preserving
// the DIP size won't trigger it, leaving drag regions at the old physical coordinates.
RootElement.XamlRoot.Changed += XamlRoot_Changed;
// Add dev ribbon if enabled
if (!BuildInfo.IsCiBuild)
{
@@ -221,6 +227,8 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void PositionCentered()
@@ -231,16 +239,14 @@ public sealed partial class MainWindow : WindowEx,
private void PositionCentered(DisplayArea displayArea)
{
var position = WindowPositionHelper.CalculateCenteredPosition(
var rect = WindowPositionHelper.CenterOnDisplay(
displayArea,
AppWindow.Size,
(int)this.GetDpiForWindow());
if (position is not null)
if (rect is not null)
{
// Use Move(), not MoveAndResize(). Windows auto-resizes on DPI change via WM_DPICHANGED;
// the helper already accounts for this when calculating the centered position.
AppWindow.Move((PointInt32)position);
MoveAndResizeDpiAware(rect.Value);
}
}
@@ -249,29 +255,62 @@ public sealed partial class MainWindow : WindowEx,
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
{
// don't try to restore if the saved position is invalid, just recenter
PositionCentered();
return;
}
// MoveAndResize is safe here—we're restoring a saved state at startup,
// not moving a live window between displays.
var newRect = WindowPositionHelper.AdjustRectForVisibility(
savedPosition.ToPhysicalWindowRectangle(),
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
MoveAndResizeDpiAware(newRect);
}
/// <summary>
/// Moves and resizes the window while suppressing WM_DPICHANGED.
/// The caller is expected to provide a rect already scaled for the target display's DPI.
/// Without suppression, the framework would apply its own DPI scaling on top, double-scaling the window.
/// </summary>
private void MoveAndResizeDpiAware(RectInt32 rect)
{
var originalMinHeight = MinHeight;
var originalMinWidth = MinWidth;
_suppressDpiChange = true;
try
{
// WindowEx is uses current DPI to calculate the minimum window size
MinHeight = 0;
MinWidth = 0;
AppWindow.MoveAndResize(rect);
}
finally
{
MinHeight = originalMinHeight;
MinWidth = originalMinWidth;
_suppressDpiChange = false;
}
}
private void UpdateWindowPositionInMemory()
{
var placement = new WINDOWPLACEMENT { length = (uint)Marshal.SizeOf<WINDOWPLACEMENT>() };
if (!PInvoke.GetWindowPlacement(_hwnd, ref placement))
{
return;
}
var rect = placement.rcNormalPosition;
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition
{
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
X = rect.X,
Y = rect.Y,
Width = rect.Width,
Height = rect.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = displayArea.WorkArea.Width,
ScreenHeight = displayArea.WorkArea.Height,
@@ -480,7 +519,7 @@ public sealed partial class MainWindow : WindowEx,
{
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
MoveAndResizeDpiAware(newRect);
}
else
{
@@ -737,18 +776,12 @@ public sealed partial class MainWindow : WindowEx,
var settings = serviceProvider.GetService<SettingsModel>();
if (settings is not null)
{
settings.LastWindowPosition = new WindowPosition
// a quick sanity check, so we don't overwrite correct values
if (_currentWindowPosition.IsSizeValid)
{
X = _currentWindowPosition.X,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
};
SettingsModel.SaveSettings(settings);
settings.LastWindowPosition = _currentWindowPosition;
SettingsModel.SaveSettings(settings);
}
}
var extensionService = serviceProvider.GetService<IExtensionService>()!;
@@ -1108,6 +1141,13 @@ public sealed partial class MainWindow : WindowEx,
// Prevent the window from maximizing when double-clicking the title bar area
case PInvoke.WM_NCLBUTTONDBLCLK:
return (LRESULT)IntPtr.Zero;
// When restoring a saved position across monitors with different DPIs,
// MoveAndResize already sets the correctly-scaled size. Suppress the
// framework's automatic DPI resize to avoid double-scaling.
case PInvoke.WM_DPICHANGED when _suppressDpiChange:
return (LRESULT)IntPtr.Zero;
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;

View File

@@ -66,4 +66,8 @@ GetStockObject
GetModuleHandle
GetWindowThreadProcessId
AttachThreadInput
AttachThreadInput
GetWindowPlacement
WINDOWPLACEMENT
WM_DPICHANGED

View File

@@ -6,42 +6,42 @@ namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
public partial class ErrorReportSanitizerTests
{
private static class TestData
internal static class TestData
{
internal static string Input =>
$"""
HRESULT: 0x80004005
HRESULT: -2147467259
$"""
HRESULT: 0x80004005
HRESULT: -2147467259
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
public const string Expected =
$"""
@@ -49,8 +49,8 @@ public partial class ErrorReportSanitizerTests
HRESULT: -2147467259
Here is e-mail address <[EMAIL_REDACTED]>
IPv4 address: [IP4_REDACTED]
IPv4 loopback address: [IP4_REDACTED]
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: [MAC_ADDRESS_REDACTED]
IPv6 address: [IP6_REDACTED]
IPv6 loopback address: [IP6_REDACTED]
@@ -77,5 +77,55 @@ public partial class ErrorReportSanitizerTests
FTP upload error: [URL_REDACTED]
Email service error: mailto:[EMAIL_REDACTED]?subject=Alert
""";
internal static string Input2 =>
$"""
============================================================
Hello World! Command Palette is starting.
Application:
App version: 0.0.1.0
Packaging flavor: Packaged
Is elevated: no
Environment:
OS version: Microsoft Windows 10.0.26220
OS architecture: X64
Runtime identifier: win-x64
Framework: .NET 9.0.13
Process architecture: X64
Culture: cs-CZ
UI culture: en-US
Paths:
Log directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
Config directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
============================================================
""";
public const string Expected2 =
"""
============================================================
Hello World! Command Palette is starting.
Application:
App version: 0.0.1.0
Packaging flavor: Packaged
Is elevated: no
Environment:
OS version: Microsoft Windows 10.0.26220
OS architecture: X64
Runtime identifier: win-x64
Framework: .NET 9.0.13
Process architecture: X64
Culture: cs-CZ
UI culture: en-US
Paths:
Log directory: [LOCALAPPLICATIONDATA_DIR]Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
Config directory: [LOCALAPPLICATIONDATA_DIR]Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
============================================================
""";
}
}

View File

@@ -22,4 +22,18 @@ public partial class ErrorReportSanitizerTests
// Assert
Assert.AreEqual(TestData.Expected, result);
}
[TestMethod]
public void Sanitize_ShouldNotMaskTooMuchPiiInErrorReport()
{
// Arrange
var reportSanitizer = new ErrorReportSanitizer();
var input = TestData.Input2;
// Act
var result = reportSanitizer.Sanitize(input);
// Assert
Assert.AreEqual(TestData.Expected2, result);
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
[TestClass]
public class FilenameMaskRuleProviderTests
{
[TestMethod]
public void GetRules_ShouldReturnExpectedRules()
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var rules = provider.GetRules();
// Assert
var ruleList = new List<SanitizationRule>(rules);
Assert.AreEqual(1, ruleList.Count);
Assert.AreEqual("Mask filename in any path", ruleList[0].Description);
}
[DataTestMethod]
[DataRow(@"C:\Users\Alice\Documents\secret.txt", @"C:\Users\Alice\Documents\se****.txt")]
[DataRow(@"logs\error-report.log", @"logs\er**********.log")]
[DataRow(@"/var/logs/trace.json", @"/var/logs/tr***.json")]
public void FilenameRules_ShouldMaskFileNamesInPaths(string input, string expected)
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
// Assert
Assert.AreEqual(expected, result);
}
[DataTestMethod]
[DataRow("C:\\Users\\Alice\\Documents\\", "C:\\Users\\Alice\\Documents\\")]
[DataRow(@"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4", @"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4")]
[DataRow(@"C:\Users\Alice\appsettings.json", @"C:\Users\Alice\appsettings.json")]
[DataRow(@"C:\Users\Alice\.env", @"C:\Users\Alice\.env")]
[DataRow(@"logs\readme", @"logs\readme")]
public void FilenameRules_ShouldNotMaskNonSensitivePatterns(string input, string expected)
{
// Arrange
var provider = new FilenameMaskRuleProvider();
// Act
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
// Assert
Assert.AreEqual(expected, result);
}
}

View File

@@ -54,6 +54,8 @@ public class PiiRuleProviderTests
[DataRow("Two numbers: 123-456-7890 and +420 777123456", "Two numbers: [PHONE_REDACTED] and [PHONE_REDACTED]")]
[DataRow("Czech phone +420 777 123 456", "Czech phone [PHONE_REDACTED]")]
[DataRow("Slovak phone +421 777 12 34 56", "Slovak phone [PHONE_REDACTED]")]
[DataRow("Version 1.2.3.4", "Version 1.2.3.4")]
[DataRow("OS version: Microsoft Windows 10.0.26220", "OS version: Microsoft Windows 10.0.26220")]
[DataRow("No phone number here", "No phone number here")]
public void PhoneRules_ShouldMaskPhoneNumbers(string input, string expected)
{
@@ -104,6 +106,8 @@ public class PiiRuleProviderTests
[DataRow("GUID: 123e4567-e89b-12d3-a456-426614174000", "GUID: 123e4567-e89b-12d3-a456-426614174000")]
[DataRow("Timestamp: 2023-10-05T14:32:10Z", "Timestamp: 2023-10-05T14:32:10Z")]
[DataRow("Version: 1.2.3", "Version: 1.2.3")]
[DataRow("Version: 1.2.3.4", "Version: 1.2.3.4")]
[DataRow("Version: 0.2.3.4", "Version: 0.2.3.4")]
[DataRow("Version: 10.0.22631.3448", "Version: 10.0.22631.3448")]
[DataRow("MAC: 00:1A:2B:3C:4D:5E", "MAC: 00:1A:2B:3C:4D:5E")]
[DataRow("Date: 2023-10-05", "Date: 2023-10-05")]

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
@@ -26,6 +28,22 @@ internal static class BrowserInfoServiceExtensions
/// </remarks>
public static bool Open(this IBrowserInfoService browserInfoService, string url)
{
// If the URL is a valid URI, attempt to open it with the default browser by invoking it through the shell.
if (Uri.TryCreate(url, UriKind.Absolute, out _))
{
try
{
ShellHelpers.OpenInShell(url);
return true;
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to launch the URI {url}: {ex}");
}
}
// Use legacy method to open the URL if it's not a well-formed URI or if the shell launch fails.
// This may handle cases where the URL is a search query or a custom URI scheme.
var defaultBrowser = browserInfoService.GetDefaultBrowser();
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
}