Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
18 changes: 16 additions & 2 deletions Sources/Library/QuickOpen/QuickOpenResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,24 @@ struct QuickOpenResult: Identifiable {
return URL(string: text).flatMap { $0.host == nil || !WebFetch.isWeb($0) ? nil : $0 }
}
guard let url = URL(string: "https://\(text)"), let host = url.host,
let last = host.split(separator: ".").last, host.contains("."),
last.count >= 2, last.allSatisfy(\.isLetter) else { return nil }
isWebHost(host) else { return nil }
return url
}

/// Whether a host is one a person would type on purpose: a dotted name whose last part is letters, such as
/// `example.com`, or a dotted-quad address or `localhost`, which is how a router or a local server is written.
/// A single word, or too few dots to be an address, is not one. The fetch-safety rules that refuse local and
/// private addresses apply to background requests, not to a link the user typed.
static func isWebHost(_ host: String) -> Bool {
if host.lowercased() == "localhost" { return true }
let parts = host.split(separator: ".", omittingEmptySubsequences: false).map(String.init)
if parts.count == 4, parts.allSatisfy({ !$0.isEmpty && $0.allSatisfy(\.isNumber) }),
parts.allSatisfy({ (0...255).contains(Int($0) ?? -1) }) {
return true
}
guard host.contains("."), let last = parts.last else { return false }
return last.count >= 2 && last.allSatisfy(\.isLetter)
}
}

/// The library's openable items, prepared for Quick Open. Names are lowercased and the text each item is matched
Expand Down
4 changes: 3 additions & 1 deletion Sources/Library/System/Spotlight.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ enum Spotlight {
/// The host of a bookmark's address, such as "jira.example.com". The address may have no scheme, since a bookmark can
/// be saved as "jira.example.com/browse". Nil when no host can be read.
static func host(of address: String) -> String? {
if let host = QuickOpenResult.link(from: address)?.host, !host.isEmpty { return host }
// A dotted host, as before: a bare `localhost` is a link a person can type, but there is no domain to show
// for it in Spotlight.
if let host = QuickOpenResult.link(from: address)?.host, host.contains(".") { return host }
// A template's braces are not valid in a URL, so the host is read from the text by hand.
var text = Substring(address.trimmingCharacters(in: .whitespaces))
if let range = text.range(of: "://") { text = text[range.upperBound...] }
Expand Down
18 changes: 18 additions & 0 deletions Tests/Checks/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@ let pt = [LibraryItem(name: "Jira", url: "https://x.atlassian.net"),
check(QuickOpenResult.search("", in: pt, counts: [:]).compactMap(\.node).map(\.name) == ["Jira", "Jenkins", "Terminal"], "empty search order")
check(QuickOpenResult.search("j", in: pt, counts: [:]).compactMap(\.node).map(\.name) == ["Jira", "Jenkins"], "prefix search")
check(QuickOpenResult.link(from: "example.com/a")?.absoluteString == "https://example.com/a" && QuickOpenResult.link(from: "hello world") == nil, "link detection")
// A router or a local server is typed without a scheme, so those are links too.
do {
for text in ["192.168.1.1", "192.168.1.1:8080", "localhost:3000", "http://192.168.1.1/admin", "https://example.com:8443/x"] {
check(QuickOpenResult.link(from: text) != nil, "a typed link '\(text)' is offered")
}
check(QuickOpenResult.link(from: "192.168.1.1")?.absoluteString == "https://192.168.1.1"
&& QuickOpenResult.link(from: "192.168.1.1:8080")?.absoluteString == "https://192.168.1.1:8080"
&& QuickOpenResult.link(from: "localhost:3000")?.absoluteString == "https://localhost:3000",
"a typed address gets the https default, with its port kept")
for text in ["1.2.3", "word", "hello world", "999.1.1.1", "192.168.1", "1.2.3.4.5", "", "a b c"] {
check(QuickOpenResult.link(from: text) == nil, "not a typed link: '\(text)'")
}
check(QuickOpenResult.link(from: " 192.168.1.1 ")?.host == "192.168.1.1", "a typed address is trimmed")
check(QuickOpenResult.link(from: "192.168.1.1/x?y=z")?.host == "192.168.1.1", "a path and query on a typed address are kept")
check(QuickOpenResult.isWebHost("192.168.1.1") && QuickOpenResult.isWebHost("localhost") && QuickOpenResult.isWebHost("example.com")
&& !QuickOpenResult.isWebHost("1.2.3") && !QuickOpenResult.isWebHost("word") && !QuickOpenResult.isWebHost("256.1.1.1"),
"a host is a typed link when it is a dotted name, a dotted-quad address, or localhost")
}

/// The Quick Open search as it was before the index: walks and lowercases the whole tree on every call. The index
/// must return exactly what this returns.
Expand Down
Loading