Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Flow.Launcher.Test/Plugins/UrlPluginTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using NUnit.Framework;

Check warning on line 1 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)
using NUnit.Framework.Legacy;

Check warning on line 2 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)

Check warning on line 2 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)
using Flow.Launcher.Plugin.Url;

namespace Flow.Launcher.Test.Plugins
Expand All @@ -14,20 +14,47 @@
ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com"));
ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com"));
ClassicAssert.IsTrue(plugin.IsURL("http://google.com"));
ClassicAssert.IsTrue(plugin.IsURL("ftp://google.com"));
ClassicAssert.IsTrue(plugin.IsURL("www.google.com"));

Check warning on line 18 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)
ClassicAssert.IsTrue(plugin.IsURL("google.com"));

Check warning on line 19 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)

Check warning on line 19 in Flow.Launcher.Test/Plugins/UrlPluginTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`google` is not a recognized word. (unrecognized-spelling)
ClassicAssert.IsTrue(plugin.IsURL("http://localhost"));
ClassicAssert.IsTrue(plugin.IsURL("https://localhost"));
ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80"));
ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80"));
ClassicAssert.IsTrue(plugin.IsURL("localhost"));
ClassicAssert.IsTrue(plugin.IsURL("localhost:8080"));
ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10"));
ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10"));
ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10:8080"));
ClassicAssert.IsTrue(plugin.IsURL("192.168.1.1"));
ClassicAssert.IsTrue(plugin.IsURL("192.168.1.1:3000"));
ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10"));
ClassicAssert.IsTrue(plugin.IsURL("[2001:db8::1]"));
ClassicAssert.IsTrue(plugin.IsURL("[2001:db8::1]:8080"));
ClassicAssert.IsTrue(plugin.IsURL("http://[2001:db8::1]"));
ClassicAssert.IsTrue(plugin.IsURL("https://[2001:db8::1]:8080"));
ClassicAssert.IsTrue(plugin.IsURL("[::1]"));
ClassicAssert.IsTrue(plugin.IsURL("[::1]:8080"));
ClassicAssert.IsTrue(plugin.IsURL("2001:db8::1"));
ClassicAssert.IsTrue(plugin.IsURL("::1"));
ClassicAssert.IsTrue(plugin.IsURL("HTTP://EXAMPLE.COM"));
ClassicAssert.IsTrue(plugin.IsURL("HTTPS://EXAMPLE.COM"));
ClassicAssert.IsTrue(plugin.IsURL("EXAMPLE.COM"));
ClassicAssert.IsTrue(plugin.IsURL("LOCALHOST"));
Comment thread
VictoriousRaptor marked this conversation as resolved.
Outdated


ClassicAssert.IsFalse(plugin.IsURL("wwww"));
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
ClassicAssert.IsFalse(plugin.IsURL("wwww.c"));
ClassicAssert.IsFalse(plugin.IsURL("not a url"));
ClassicAssert.IsFalse(plugin.IsURL("just text"));
ClassicAssert.IsFalse(plugin.IsURL("http://"));
ClassicAssert.IsFalse(plugin.IsURL("://example.com"));
ClassicAssert.IsFalse(plugin.IsURL("0.0.0.0")); // Pattern excludes 0.0.0.0
Comment thread
VictoriousRaptor marked this conversation as resolved.
Outdated
ClassicAssert.IsFalse(plugin.IsURL("256.1.1.1")); // Invalid IPv4
ClassicAssert.IsFalse(plugin.IsURL("example")); // No TLD
ClassicAssert.IsFalse(plugin.IsURL(".com"));
ClassicAssert.IsFalse(plugin.IsURL("http://.com"));
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

Missing test coverage for invalid port numbers. The regex pattern allows port numbers up to 99999 (5 digits), but valid TCP/UDP ports range from 0 to 65535. Consider adding test cases like "example.com:99999" or "192.168.1.1:70000" to verify they are correctly rejected or accepted based on the intended behavior.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

Missing test coverage for IPv6 addresses with embedded IPv4 notation (e.g., "::ffff:192.0.2.1" or "64:ff9b::192.0.2.1"). These are valid IPv6 formats that may be used in URLs but are not covered by the current test cases or the regex patterns.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IPv6 is a disaster.

}
}
65 changes: 33 additions & 32 deletions Plugins/Flow.Launcher.Plugin.Url/Main.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using Flow.Launcher.Plugin.SharedCommands;
Expand All @@ -15,19 +16,26 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
// user:pass authentication
"(?:\\S+(?::\\S*)?@)?" +
"(?:" +
// IP address exclusion
// private & local networks
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broacast addresses
// (first & last IP address of each class)
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
// IPv6 address with optional brackets (brackets required if followed by port)
// IPv6 with brackets
"(?:\\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\]|" + // standard IPv6
"\\[(?:[0-9a-fA-F]{1,4}:){1,7}:\\]|" + // IPv6 with trailing ::
"\\[(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\\]|" + // IPv6 compressed
"\\[::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}\\]|" + // IPv6 with leading ::
"\\[::1\\])" + // IPv6 loopback
"|" +
// IPv6 without brackets (only when no port follows)
"(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|" + // standard IPv6
"(?:[0-9a-fA-F]{1,4}:){1,7}:|" + // IPv6 with trailing ::
"(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + // IPv6 compressed
"::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}|" + // IPv6 with leading ::
"::1)(?!:[0-9])" + // IPv6 loopback (not followed by port)
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The IPv6 regex patterns are incomplete and won't match many valid compressed IPv6 addresses. For example, fe80:1:2::3:4:5:6 (where :: appears in the middle with multiple segments on both sides) won't match any of these patterns. Line 30's pattern (?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4} expects the final segment to have no colons, but in addresses like fe80:1:2::3:4, there are multiple segments after the ::. A comprehensive IPv6 regex would need additional patterns to cover all valid positions and combinations of the :: compression.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a test case to valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot Propose your fix.

Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The negative lookahead (?!:[0-9]) used to prevent matching ports with unbracketed IPv6 addresses is insufficient. It only prevents matching when immediately followed by a colon and a single digit, but ports can have multiple digits (e.g., :8080, :3000). This could cause the regex to incorrectly match an IPv6 address that is actually followed by a port number. The pattern should use (?!:[0-9]+) or (?!:\\d) to properly prevent matching any port number.

Suggested change
"::1)(?!:[0-9])" + // IPv6 loopback (not followed by port)
"::1)(?!:[0-9]+)" + // IPv6 loopback (not followed by port)

Copilot uses AI. Check for mistakes.
"|" +
// IPv4 address - all valid addresses including private networks (excluding 0.0.0.0)
"(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|[1-9])\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))" +
"|" +
// localhost
"localhost" +
"|" +
// host name
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
Expand All @@ -37,20 +45,25 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
")" +
// port number
"(?::\\d{2,5})?" +
"(?::\\d{1,5})?" +
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

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

The port number pattern allows single-digit ports (1-9) with the pattern \d{1,5}, but also allows invalid port numbers up to 99999. Valid TCP/UDP port numbers range from 1 to 65535. The pattern should validate that the port number is within this valid range, or at minimum should be \d{2,5} to match the original pattern (though that still allows invalid ports like 99999). Consider using a more restrictive pattern like (?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}) to properly validate port numbers.

Suggested change
"(?::\\d{1,5})?" +
"(?::(?:6553[0-5]|655[0-2]\\d|65[0-4]\\d{2}|6[0-4]\\d{3}|[1-5]\\d{4}|[1-9]\\d{0,3}))?" +

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's right but not necessary.

Comment thread
VictoriousRaptor marked this conversation as resolved.
Outdated
Comment thread
VictoriousRaptor marked this conversation as resolved.
Outdated
// resource path
"(?:/\\S*)?" +
"$";
private readonly Regex UrlRegex = new(UrlPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static PluginInitContext Context { get; private set; }
internal static Settings Settings { get; private set; }

private static readonly string[] UrlSchemes = ["http://", "https://", "ftp://"];

public List<Result> Query(Query query)
{
var raw = query.Search;
if (IsURL(raw))
if (!IsURL(raw))
{
return
return [];
}

return
[
new()
{
Expand All @@ -60,7 +73,8 @@ public List<Result> Query(Query query)
Score = 8,
Action = _ =>
{
if (!raw.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !raw.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
// not a recognized scheme, add preferred http scheme
if (!UrlSchemes.Any(scheme => raw.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)))
{
raw = GetHttpPreference() + "://" + raw;
}
Comment on lines +63 to 67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Bare IPv6 addresses won't open correctly.

The Action handler adds the scheme to bare IPv6 addresses without wrapping them in brackets. For input "2001:db8::1", this produces "http://2001:db8::1" instead of the required "http://[2001:db8::1]". Browsers will reject these URLs.

The IsURL method correctly handles bracket-wrapping for validation (lines 137-157), but the Action handler needs the same logic.

🔎 Proposed fix
                        {
                            // not a recognized scheme, add preferred http scheme
                            if (!UrlSchemes.Any(scheme => raw.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)))
                            {
-                                raw = GetHttpPreference() + "://" + raw;
+                                // Wrap bare IPv6 addresses in brackets
+                                if (raw.Count(c => c == ':') > 1 && !raw.Contains("://") && !raw.Contains('@') && !raw.StartsWith('['))
+                                {
+                                    // Check if there's already a port pattern (]:port)
+                                    var portMatch = Regex.Match(raw, @"\]:(\d{1,5})$");
+                                    if (!portMatch.Success)
+                                    {
+                                        raw = $"{GetHttpPreference()}://[{raw}]";
+                                    }
+                                    else
+                                    {
+                                        raw = GetHttpPreference() + "://" + raw;
+                                    }
+                                }
+                                else
+                                {
+                                    raw = GetHttpPreference() + "://" + raw;
+                                }
                            }

Alternatively, extract the bracket-wrapping logic into a helper method to avoid duplication with IsURL.

🤖 Prompt for AI Agents
In Plugins/Flow.Launcher.Plugin.Url/Main.cs around lines 63 to 67, the code
prepends the HTTP scheme to strings but does not bracket-wrap bare IPv6
addresses; change the logic so that when adding the scheme you detect a bare
IPv6 (contains ':' and does not start with '[' and is not already a recognized
scheme) and wrap it with '[' and ']' before prefixing the scheme (or extract the
existing bracket-wrapping logic from IsURL into a shared helper and call that
here) so resulting URLs become "http://[2001:db8::1]" instead of
"http://2001:db8::1".

Expand Down Expand Up @@ -92,9 +106,6 @@ public List<Result> Query(Query query)
}
}
];
}

return [];
}

private static string GetHttpPreference()
Expand All @@ -104,17 +115,7 @@ private static string GetHttpPreference()

public bool IsURL(string raw)
{
raw = raw.ToLower();

if (UrlRegex.Match(raw).Value == raw) return true;

if (raw == "localhost" || raw.StartsWith("localhost:") ||
raw == "http://localhost" || raw.StartsWith("http://localhost:") ||
raw == "https://localhost" || raw.StartsWith("https://localhost:")
)
{
return true;
}
if (UrlRegex.Match(raw.ToLower()).Value == raw) return true;
Comment thread
VictoriousRaptor marked this conversation as resolved.
Outdated

return false;
}
Comment thread
VictoriousRaptor marked this conversation as resolved.
Expand Down
Loading