Skip to content

Commit 6569591

Browse files
committed
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)