Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 6569591

Browse files
update
1 parent 44709e9 commit 6569591

File tree

3 files changed

+120
-17
lines changed

3 files changed

+120
-17
lines changed

‎Sources/swiftui-loop-videoplayer/ext+/URL+.swift

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,32 @@ import Foundation
1010

1111
extension URL {
1212

13-
/// Validates a string as a well-formed HTTP or HTTPS URL and returns a URL object if valid.
14-
///
15-
/// - Parameter urlString: The string to validate as a URL.
16-
/// - Returns: An optional URL object if the string is a valid URL.
17-
/// - Throws: An error if the URL is not valid or cannot be created.
18-
static func validURLFromString(_ string: String) -> URL? {
19-
let pattern = "^(https?:\\/\\/)(([a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+\\.[a-zA-Z]{2,})(:\\d{1,5})?(\\/[\\S]*)?$"
20-
let regex = try? NSRegularExpression(pattern: pattern, options: [])
21-
22-
let matches = regex?.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
23-
24-
guard let _ = matches, !matches!.isEmpty else {
25-
// If no matches are found, the URL is not valid
26-
return nil
13+
/// Validates and returns an HTTP/HTTPS URL or nil.
14+
/// Strategy:
15+
/// 1) Parse once to detect an existing scheme (mailto, ftp, etc.).
16+
/// 2) If a scheme exists and it's not http/https -> reject.
17+
/// 3) If no scheme exists -> optionally prepend https:// and parse again.
18+
static func validURLFromString(from raw: String, assumeHTTPSIfMissing: Bool = true) -> URL? {
19+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
20+
21+
// First parse to detect an existing scheme.
22+
if let pre = URLComponents(string: trimmed), let scheme = pre.scheme?.lowercased() {
23+
// Reject anything that is not http/https.
24+
guard scheme == "http" || scheme == "https" else { return nil }
25+
26+
let comps = pre
27+
// Require a host
28+
guard let host = comps.host, !host.isEmpty else { return nil }
29+
// Validate port range
30+
if let port = comps.port, !(1...65535).contains(port) { return nil }
31+
return comps.url
2732
}
2833

29-
// If a match is found, attempt to create a URL object
30-
return URL(string: string)
34+
// No scheme present -> optionally add https://
35+
guard assumeHTTPSIfMissing else { return nil }
36+
guard let comps = URLComponents(string: "https://" + trimmed) else { return nil }
37+
guard let host = comps.host, !host.isEmpty else { return nil }
38+
if let port = comps.port, !(1...65535).contains(port) { return nil }
39+
return comps.url
3140
}
3241
}

‎Sources/swiftui-loop-videoplayer/fn/fn+.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func subtitlesAssetFor(_ settings: VideoSettings) -> AVURLAsset? {
4444
/// - Returns: An optional `AVURLAsset`, or `nil` if neither a valid URL nor a local resource file is found.
4545
fileprivate func assetFrom(name: String, fileExtension: String?) -> AVURLAsset? {
4646
// Attempt to create a valid URL from the provided string.
47-
if let url = URL.validURLFromString(name) {
47+
if let url = URL.validURLFromString(from:name) {
4848
return AVURLAsset(url: url)
4949
}
5050

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// testURL+.swift
3+
// swiftui-loop-videoplayer
4+
//
5+
// Created by Igor Shelopaev on 20.08.25.
6+
//
7+
8+
import XCTest
9+
@testable import swiftui_loop_videoplayer
10+
11+
final class testURL: XCTestCase {
12+
13+
// MARK: - Positive cases (should pass)
14+
15+
func testSampleVideoURLsPass() {
16+
// Given: four sample URLs from the sandbox dictionary
17+
let urls = [
18+
"https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8",
19+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
20+
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
21+
"https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8"
22+
]
23+
24+
// When/Then
25+
for raw in urls {
26+
let url = URL.validURLFromString(from: raw)
27+
XCTAssertNotNil(url, "Expected to parse: \(raw)")
28+
XCTAssertEqual(url?.scheme?.lowercased(), "https")
29+
}
30+
}
31+
32+
func testAddsHTTPSIfMissing() {
33+
// Given
34+
let raw = "example.com/path?x=1#y"
35+
36+
// When
37+
let url = URL.validURLFromString(from: raw)
38+
39+
// Then
40+
XCTAssertNotNil(url)
41+
XCTAssertEqual(url?.scheme, "https")
42+
XCTAssertEqual(url?.host, "example.com")
43+
XCTAssertEqual(url?.path, "/path")
44+
}
45+
46+
func testTrimsWhitespace() {
47+
let raw = " https://example.com/video.m3u8 "
48+
let url = URL.validURLFromString(from: raw)
49+
XCTAssertNotNil(url)
50+
XCTAssertEqual(url?.host, "example.com")
51+
XCTAssertEqual(url?.path, "/video.m3u8")
52+
}
53+
54+
func testIPv6AndLocalHosts() {
55+
// IPv6 loopback
56+
XCTAssertNotNil(URL.validURLFromString(from: "https://[::1]"))
57+
// localhost
58+
XCTAssertNotNil(URL.validURLFromString(from: "http://localhost"))
59+
// IPv4 with port and query/fragment
60+
XCTAssertNotNil(URL.validURLFromString(from: "http://127.0.0.1:8080/path?a=1#x"))
61+
}
62+
63+
func testIDNUnicodeHost() {
64+
// Unicode host (IDN). URLComponents should handle this.
65+
let url = URL.validURLFromString(from: "https://bücher.de")
66+
XCTAssertNotNil(url)
67+
XCTAssertEqual(url?.scheme, "https")
68+
XCTAssertNotNil(url?.host)
69+
}
70+
71+
// MARK: - Negative cases (should fail)
72+
73+
func testRejectsNonHTTP() {
74+
XCTAssertNil(URL.validURLFromString(from: "ftp://example.com/file.mp4"))
75+
XCTAssertNil(URL.validURLFromString(from: "mailto:user@example.com"))
76+
XCTAssertNil(URL.validURLFromString(from: "file:///Users/me/movie.mp4"))
77+
}
78+
79+
func testRejectsInvalidPort() {
80+
XCTAssertNil(URL.validURLFromString(from: "https://example.com:0"))
81+
XCTAssertNil(URL.validURLFromString(from: "https://example.com:65536"))
82+
XCTAssertNotNil(URL.validURLFromString(from: "https://example.com:65535"))
83+
}
84+
85+
func testRejectsMissingHost() {
86+
XCTAssertNil(URL.validURLFromString(from: "https://"))
87+
XCTAssertNil(URL.validURLFromString(from: "https:///path-only"))
88+
}
89+
90+
func testNoAutoSchemeOption() {
91+
// When auto-scheme is disabled, a bare host should fail.
92+
XCTAssertNil(URL.validURLFromString(from: "example.com", assumeHTTPSIfMissing: false))
93+
}
94+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /