102 lines
6.4 KiB
Swift
102 lines
6.4 KiB
Swift
import Foundation
|
|
|
|
@main
|
|
struct SourceCheckChecks {
|
|
struct Failure: Error {}
|
|
static func require(_ condition: @autoclosure () -> Bool, _ message: String) throws {
|
|
guard condition() else {
|
|
FileHandle.standardError.write(Data("FAIL: \(message)\n".utf8))
|
|
throw Failure()
|
|
}
|
|
}
|
|
|
|
static func main() async throws {
|
|
let watch = "https://zokoanime.video/stream/ani/1/1/sub"
|
|
guard case .checkSource(let id, let episode) = try PlayerCommand.parse(["--check-source", watch]) else {
|
|
throw Failure()
|
|
}
|
|
try require(id == "1" && episode == 1, "check command is not a launch")
|
|
let secret = "private-fixture-token"
|
|
for arguments in [
|
|
["--check-source"], ["--check-source", watch, watch],
|
|
["--new-instance", "--check-source", watch], ["--no-config", "--check-source", watch],
|
|
["--check-source", watch, "--progress-file", "/tmp/" + secret],
|
|
["--source-file", "/tmp/" + secret, "--check-source", watch],
|
|
["--check-source", watch, "--help"], ["--check-source", "--check-source"],
|
|
["--check-source", "https://youtu.be/jNQXAC9IVRw"],
|
|
["--check-source", "https://barelystarted.miruro.tv/watch/1/show?ep=1"],
|
|
["--check-source", " " + watch], ["--check-source", watch + "\n"],
|
|
["--check-source", watch + "?token=" + secret], ["--check-source", watch + "#" + secret],
|
|
["--check-source", watch.replacingOccurrences(of: "https:", with: "http:")],
|
|
["--check-source", watch.replacingOccurrences(of: "/1/1/", with: "/01/1/")],
|
|
["--check-source", watch.replacingOccurrences(of: "/1/sub", with: "/2147483648/sub")],
|
|
["--check-source", watch.replacingOccurrences(of: "/sub", with: "/dub")],
|
|
["--check-source", "https://" + secret + "@zokoanime.video/stream/ani/1/1/sub"],
|
|
["--check-source", "https://zokoanime.video.evil.example/stream/ani/1/1/sub"],
|
|
["--check-source", "https://zokoanime.video/stream/ani/%31/1/sub"],
|
|
["--check-source", String(repeating: secret, count: 100)]
|
|
] {
|
|
do {
|
|
_ = try PlayerCommand.parse(arguments)
|
|
} catch {
|
|
try require(!error.localizedDescription.contains(secret), "argument errors redact input")
|
|
continue
|
|
}
|
|
throw Failure()
|
|
}
|
|
for status in SourceCheck.Status.allCases {
|
|
let json = status.json
|
|
let row = try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any]
|
|
try require(row.count == 4 && row["version"] as? Int == 1 && row["provider"] as? String == "zokoanime" &&
|
|
row["track"] as? String == "sub" && row["status"] as? String == status.rawValue,
|
|
"one bounded result schema without source addresses or error strings")
|
|
try require(json.utf8.count < 128 && !json.contains("\n"), "bounded single-line JSON")
|
|
}
|
|
let root = URL(string: "https://hls.dramahot.top/master.m3u8?token=" + secret)!
|
|
let master = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000\nvideo.m3u8\n"
|
|
let media = "#EXTM3U\n#EXT-X-TARGETDURATION:10\n#EXTINF:10,\nhttps://hls1.drama1.cfd/segment.jpg\n#EXT-X-ENDLIST\n"
|
|
try MiruroTransport.validateZokoManifest(master, base: root)
|
|
try MiruroTransport.validateZokoManifest(media, base: root)
|
|
for malformed in ["<html>" + secret + "</html>", "#EXTM3U", "#EXTM3Ugarbage\n" + master,
|
|
"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n", "#EXTM3U\nrandom.txt\n",
|
|
master.replacingOccurrences(of: "video.m3u8", with: "https://127.0.0.1/private.m3u8"),
|
|
master.replacingOccurrences(of: "video.m3u8", with: "https://hls1.drama1.cfd/private.m3u8"),
|
|
media.replacingOccurrences(of: "https://hls1.drama1.cfd/segment.jpg", with: "https://evil.example/" + secret),
|
|
"#EXTM3U\n#EXT-X-KEY:METHOD=AES-128,URI=\"http://127.0.0.1/" + secret + "\"\n" + media,
|
|
String(repeating: "x", count: ZokoResolver.bodyLimit + 1)] {
|
|
do { try MiruroTransport.validateZokoManifest(malformed, base: root) }
|
|
catch {
|
|
try require(!error.localizedDescription.contains(secret), "manifest errors redact source details")
|
|
continue
|
|
}
|
|
throw Failure()
|
|
}
|
|
// Exercise error classification, not fake provider availability. Live proof uses the actual CLI below.
|
|
for category in [ZokoError.unavailable, .transport, .protection, .malformed, .mapping] {
|
|
let result = await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in throw category },
|
|
validate: { _ in throw Failure() })
|
|
try require(result == (category == .unavailable ? .unavailable : .failed), "only explicit missing episode is unavailable")
|
|
}
|
|
let source = ResolvedSource(url: root.absoluteString, headers: "", identity: "anilist:1:1", title: "Fixture",
|
|
subtitles: [], requirements: .init(zoko: true))
|
|
let resourceFailure = await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in source },
|
|
validate: { _ in throw URLError(.badServerResponse, userInfo: [NSURLErrorFailingURLStringErrorKey: root.absoluteString]) })
|
|
try require(resourceFailure == .failed && !resourceFailure.json.contains(secret), "resource errors are failed and private")
|
|
let deadline = await SourceCheck.run(mediaID: "1", episode: 1, timeout: .zero, resolve: { _, _ in
|
|
try await Task.sleep(for: .seconds(60))
|
|
return source
|
|
}, validate: { _ in throw Failure() })
|
|
try require(deadline == .failed, "total deadline cancels pending work")
|
|
let cancelled = Task {
|
|
await SourceCheck.run(mediaID: "1", episode: 1, resolve: { _, _ in
|
|
try await Task.sleep(for: .seconds(60))
|
|
return source
|
|
}, validate: { _ in throw Failure() })
|
|
}
|
|
cancelled.cancel()
|
|
let cancelledResult = await cancelled.value
|
|
try require(cancelledResult == .failed, "caller cancellation stops source check")
|
|
print("PASS: source command isolation, redaction, bounded JSON, shared manifest validation, error taxonomy, total deadline and cancellation")
|
|
}
|
|
}
|