223 lines
16 KiB
Swift
223 lines
16 KiB
Swift
import Foundation
|
|
import Darwin
|
|
|
|
@main
|
|
struct PlayerCommandChecks {
|
|
static func require(_ condition: Bool, _ message: String) {
|
|
if !condition { fatalError("FAIL: " + message) }
|
|
}
|
|
|
|
static func main() throws {
|
|
let directory = FileManager.default.temporaryDirectory.appendingPathComponent("terebi.cli-check.\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let movie = directory.appendingPathComponent("video with spaces.mp4")
|
|
let optionFile = directory.appendingPathComponent("--help")
|
|
let colonFile = directory.appendingPathComponent("part:1.mp4")
|
|
let instanceFile = directory.appendingPathComponent("--new-instance")
|
|
for file in [movie, optionFile, colonFile, instanceFile] { try Data().write(to: file) }
|
|
|
|
func opened(_ arguments: [String], newInstance: Bool = false, noConfig: Bool = false,
|
|
progressFile: URL? = nil) throws -> URL? {
|
|
guard case .launch(let url, let independent, let bypass, let report) = try PlayerCommand.parse(arguments, directory: directory) else {
|
|
fatalError("FAIL: expected a launch command")
|
|
}
|
|
require(independent == newInstance, "launch mode preserves default reuse or explicit independence")
|
|
require(bypass == noConfig, "configuration loading defaults on and can be explicitly bypassed")
|
|
require(report == progressFile, "reporting destination preserves launch scope and resolves against invoking directory")
|
|
return url
|
|
}
|
|
require(try opened([movie.lastPathComponent]) == movie, "relative paths resolve against invocation directory, preserving spaces")
|
|
require(try opened([movie.absoluteString]) == movie, "encoded local file URLs preserve filename identity")
|
|
require(try opened(["--", "--help"]) == optionFile, "option terminator allows filenames that look like commands")
|
|
require(try opened([colonFile.path]) == colonFile, "colons in filesystem paths are not URL schemes")
|
|
let canonical = URL(string: "https://www.youtube.com/watch?v=jNQXAC9IVRw")!
|
|
require(try opened(["https://youtu.be/jNQXAC9IVRw"]) == canonical, "YouTube positional URLs canonicalize for resume identity")
|
|
let miruro = URL(string: "https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/the-apothecary-diaries-season-3?ep=1")!
|
|
require(try opened([miruro.absoluteString]) == miruro, "Miruro positional URLs retain episode page identity")
|
|
require(try opened(["--url", miruro.absoluteString]) == miruro, "explicit Miruro URLs forward without network resolution")
|
|
require(try opened(["--new-instance"], newInstance: true) == nil, "an independent launch can omit media")
|
|
require(try opened(["--new-instance", movie.lastPathComponent], newInstance: true) == movie,
|
|
"independent local launches preserve relative paths and spaces")
|
|
require(try opened(["--new-instance", "--url", canonical.absoluteString], newInstance: true) == canonical,
|
|
"independent explicit YouTube launches retain canonical resume identity")
|
|
require(try opened(["--new-instance", "https://youtu.be/jNQXAC9IVRw"], newInstance: true) == canonical,
|
|
"independent positional YouTube launches retain canonical resume identity")
|
|
require(try opened(["--new-instance", "--", "--help"], newInstance: true) == optionFile,
|
|
"independent launch preserves the filename terminator")
|
|
require(try opened(["--", "--new-instance"]) == instanceFile,
|
|
"an escaped instance flag is a filename, not a launch option")
|
|
require(try opened(["--no-config"], noConfig: true) == nil, "configuration bypass can omit media")
|
|
require(try opened(["--no-config", movie.path], noConfig: true) == movie,
|
|
"configuration bypass retains local media delivery")
|
|
require(try opened(["--new-instance", "--no-config", "--url", canonical.absoluteString],
|
|
newInstance: true, noConfig: true) == canonical,
|
|
"independent bypass retains explicit YouTube identity")
|
|
require(try opened(["--no-config", "--new-instance", "--", "--help"],
|
|
newInstance: true, noConfig: true) == optionFile, "launch options work in either order")
|
|
guard case .help = try PlayerCommand.parse(["--no-config", "--new-instance", "--help"]) else {
|
|
fatalError("FAIL: help with launch options must not launch a player")
|
|
}
|
|
guard case .help = try PlayerCommand.parse(["--new-instance", "--help"]) else {
|
|
fatalError("FAIL: help must not launch an independent player")
|
|
}
|
|
guard case .printDefaultConfig = try PlayerCommand.parse(["--default-config"]) else {
|
|
fatalError("FAIL: printing defaults must not launch a player")
|
|
}
|
|
let report = directory.appendingPathComponent("session with spaces.jsonl")
|
|
require(try opened(["--new-instance", "--url", canonical.absoluteString, "--progress-file", report.path],
|
|
newInstance: true, progressFile: report) == canonical,
|
|
"reporting after media arguments retains canonical YouTube identity")
|
|
require(try opened(["--progress-file", report.lastPathComponent, "--new-instance", "--no-config", "--url", canonical.absoluteString],
|
|
newInstance: true, noConfig: true, progressFile: report) == canonical,
|
|
"relative report path is made absolute before GUI forwarding")
|
|
let reportFlagFile = directory.appendingPathComponent("--progress-file")
|
|
try Data().write(to: reportFlagFile)
|
|
require(try opened(["--", "--progress-file"]) == reportFlagFile,
|
|
"escaped reporting option remains a filename")
|
|
|
|
let sourceFile = directory.appendingPathComponent("source with spaces.json")
|
|
let sourceURL = "https://cdn.example/video/master.m3u8?token=secret%2Bvalue"
|
|
var source: [String: Any] = [
|
|
"version": 1, "url": sourceURL, "headers": ["Authorization": "Bearer secret "],
|
|
"identity": ["provider": "miruro", "media_id": "opaque_A-1", "episode": 12],
|
|
"subtitles": [["url": "https://cdn.example/sub.vtt?token=secret", "format": "vtt",
|
|
"language": "en", "label": "English", "default": true, "headers": ["X-Token": "secret"]]]
|
|
]
|
|
func saveSource() throws {
|
|
try JSONSerialization.data(withJSONObject: source).write(to: sourceFile)
|
|
}
|
|
try saveSource()
|
|
let resolved = try ResolvedSource.load(from: sourceFile)
|
|
require(resolved.url == sourceURL && resolved.headers == "Authorization: Bearer secret \r\n",
|
|
"resolved URLs and header values remain byte-for-byte intact")
|
|
require(resolved.identity == "miruro:opaque_A-1:12" && resolved.title == "Miruro episode 12",
|
|
"stable identity excludes CDN tokens and omitted title has a useful fallback")
|
|
require(resolved.subtitles.count == 1 && resolved.subtitles[0].isDefault &&
|
|
resolved.subtitles[0].headers == "X-Token: secret\r\n", "subtitle metadata retains independent headers")
|
|
guard case .launchSource(let delivered, let independent, let bypass, let output) =
|
|
try PlayerCommand.parse(["--new-instance", "--no-config", "--source-file", sourceFile.lastPathComponent,
|
|
"--progress-file", report.lastPathComponent], directory: directory) else {
|
|
fatalError("FAIL: expected resolved source launch")
|
|
}
|
|
require(delivered == sourceFile && independent && bypass && output == report,
|
|
"CLI forwards only an absolute source-file path with existing launch options")
|
|
for arguments in [["--source-file", sourceFile.path], ["--new-instance", "--source-file"],
|
|
["--new-instance", "--source-file", sourceFile.path, movie.path]] {
|
|
do { _ = try PlayerCommand.parse(arguments, directory: directory); fatalError("FAIL: invalid source launch accepted") }
|
|
catch {}
|
|
}
|
|
let original = source
|
|
let invalidChanges: [[String: Any]] = [
|
|
["version": 2], ["url": "https://user:secret@cdn.example/video"],
|
|
["url": "file:///secret"], ["url": "https://cdn.example/video\nsecret"],
|
|
["headers": ["X-Token": "secret\r\nInjected: yes"]], ["headers": ["X-Token": "secret\t"]],
|
|
["headers": ["Bad Name": "secret"]], ["headers": ["X-Token": "one", "x-token": "two"]],
|
|
["identity": ["provider": "miruro", "media_id": "bad:id", "episode": 1]],
|
|
["identity": ["provider": "miruro", "media_id": "opaque", "episode": 0]],
|
|
["identity": ["provider": "miruro", "media_id": "opaque", "episode": 1.5]],
|
|
["subtitles": Array(repeating: (original["subtitles"] as! [[String: Any]])[0], count: 2)]
|
|
]
|
|
for change in invalidChanges {
|
|
source = original.merging(change) { _, replacement in replacement }
|
|
try saveSource()
|
|
do { _ = try ResolvedSource.load(from: sourceFile); fatalError("FAIL: invalid source accepted") }
|
|
catch { require(!error.localizedDescription.contains("secret"), "validation never reveals credentials") }
|
|
}
|
|
try Data(repeating: 32, count: 1_048_577).write(to: sourceFile)
|
|
do { _ = try ResolvedSource.load(from: sourceFile); fatalError("FAIL: oversized source accepted") }
|
|
catch {}
|
|
let fifo = directory.appendingPathComponent("source.fifo")
|
|
require(mkfifo(fifo.path, 0o600) == 0, "create unread FIFO source boundary")
|
|
do {
|
|
_ = try PlayerCommand.parse(["--new-instance", "--source-file", fifo.path], directory: directory)
|
|
fatalError("FAIL: FIFO source accepted")
|
|
} catch {}
|
|
|
|
let queueFile = directory.appendingPathComponent("queue.json")
|
|
let resolvedDirectory = realpath(directory.path, nil)!
|
|
defer { free(resolvedDirectory) }
|
|
let privateDirectory = URL(fileURLWithPath: String(cString: resolvedDirectory), isDirectory: true)
|
|
require(chmod(privateDirectory.path, 0o700) == 0, "queue reports require a private owned parent")
|
|
let youtubeReport = privateDirectory.appendingPathComponent("youtube.jsonl").path
|
|
let miruroReport = privateDirectory.appendingPathComponent("miruro.jsonl").path
|
|
let entries: [[String: Any]] = [
|
|
["url": miruro.absoluteString, "progress_file": miruroReport],
|
|
["url": canonical.absoluteString, "progress_file": youtubeReport]
|
|
]
|
|
func saveQueue(_ value: [String: Any]) throws {
|
|
try JSONSerialization.data(withJSONObject: value).write(to: queueFile)
|
|
}
|
|
try saveQueue(["version": 1, "items": entries])
|
|
guard case .launchQueue(let deliveredQueue, let queueIndependent, let queueBypass) =
|
|
try PlayerCommand.parse(["--new-instance", "--no-config", "--queue-file", queueFile.path]) else {
|
|
fatalError("FAIL: expected stream queue launch")
|
|
}
|
|
require(deliveredQueue == queueFile && queueIndependent && queueBypass, "queue CLI preserves independent delivery")
|
|
let parsedQueue = try StreamQueue.load(from: queueFile)
|
|
require(parsedQueue.entries.map(\.url) == [miruro, canonical], "stream queue order is supplied, never filename-sorted")
|
|
require(parsedQueue.entries[0].identity.hasPrefix("miruro:") &&
|
|
parsedQueue.entries[1].identity == canonical.absoluteString, "stable identities exclude slugs and transport URLs")
|
|
let duplicateYouTube = ["url": "https://youtu.be/jNQXAC9IVRw", "progress_file": miruroReport]
|
|
let duplicateMiruro = ["url": miruro.absoluteString.replacingOccurrences(of: "the-apothecary-diaries-season-3", with: "different-slug"),
|
|
"progress_file": youtubeReport]
|
|
let invalidQueues: [[String: Any]] = [
|
|
["version": 2, "items": entries], ["version": true, "items": entries],
|
|
["version": 1, "items": []], ["version": 1],
|
|
["version": 1, "items": Array(repeating: entries[0], count: 4097)],
|
|
["version": 1, "items": [entries[1], duplicateYouTube]],
|
|
["version": 1, "items": [entries[0], duplicateMiruro]],
|
|
["version": 1, "items": [entries[0], ["url": canonical.absoluteString, "progress_file": miruroReport]]],
|
|
["version": 1, "items": [entries[0], ["url": canonical.absoluteString,
|
|
"progress_file": privateDirectory.resolvingSymlinksInPath().appendingPathComponent("miruro.jsonl").path]]],
|
|
["version": 1, "items": [["url": "https://cdn.example/expired.m3u8", "progress_file": youtubeReport]]],
|
|
["version": 1, "items": [["url": canonical.absoluteString, "progress_file": "relative.jsonl"]]],
|
|
["version": 1, "items": [["url": canonical.absoluteString, "progress_file": movie.path]]]
|
|
]
|
|
for document in invalidQueues {
|
|
try saveQueue(document)
|
|
do {
|
|
_ = try PlayerCommand.parse(["--new-instance", "--queue-file", queueFile.path])
|
|
fatalError("FAIL: malformed queue must never launch or degrade to single playback")
|
|
} catch {}
|
|
}
|
|
try saveQueue(["version": 1, "items": entries])
|
|
for arguments in [
|
|
["--queue-file", queueFile.path], ["--new-instance", "--queue-file"],
|
|
["--new-instance", "--queue-file", "queue.json"],
|
|
["--new-instance", "--queue-file", queueFile.path, "--url", canonical.absoluteString],
|
|
["--new-instance", "--queue-file", queueFile.path, "--progress-file", report.path],
|
|
["--new-instance", "--queue-file", queueFile.path, "--queue-file", queueFile.path]
|
|
] {
|
|
do { _ = try PlayerCommand.parse(arguments); fatalError("FAIL: invalid queue option combination") }
|
|
catch {}
|
|
}
|
|
require(chmod(privateDirectory.path, 0o755) == 0, "make report parent nonprivate for rejection check")
|
|
do { _ = try StreamQueue.load(from: queueFile); fatalError("FAIL: public report directory accepted") }
|
|
catch {}
|
|
require(chmod(privateDirectory.path, 0o700) == 0, "restore private check directory")
|
|
|
|
for arguments in [["--url"], ["--url", canonical.absoluteString, movie.path], ["--"],
|
|
[movie.path, movie.path], ["--unknown"], ["missing.mp4"], [directory.path],
|
|
["file://remote-host/video.mp4"], ["https://example.com/video.mp4"],
|
|
["--new-instance", "--new-instance"], ["--new-instance", "--url"],
|
|
["--new-instance", "--"], ["--new-instance", movie.path, movie.path],
|
|
["--new-instance", "--unknown"], ["--no-config", "--no-config"],
|
|
["--default-config", movie.path], ["--default-config", "--default-config"],
|
|
["--print-default-config"],
|
|
["--progress-file", report.path],
|
|
["--new-instance", "--url", canonical.absoluteString, "--progress-file"],
|
|
["--new-instance", "--progress-file", ""],
|
|
["--new-instance", "--progress-file", report.path, "--progress-file", report.path],
|
|
["--new-instance", "--progress-file", report.path, "--help"],
|
|
["--new-instance", "--progress-file", report.path, "--default-config"],
|
|
["--no-config", "--new-instance", "--no-config"]] {
|
|
do {
|
|
_ = try PlayerCommand.parse(arguments, directory: directory)
|
|
fatalError("FAIL: accepted an invalid command")
|
|
} catch {}
|
|
}
|
|
print("Player command checks passed")
|
|
}
|
|
}
|