Files
terebi/Checks/external-subtitles.swift

205 lines
11 KiB
Swift

import Foundation
import Darwin
@main
struct ExternalSubtitleChecks {
struct Failure: Error, CustomStringConvertible { let description: String }
static func require(_ condition: @autoclosure () throws -> Bool, _ message: String) throws {
guard try condition() else { throw Failure(description: "FAIL: \(message)") }
}
static func reject(_ operation: () throws -> Void) throws {
do { try operation() }
catch is Failure { throw Failure(description: "FAIL: rejection assertion") }
catch { return }
throw Failure(description: "FAIL: malformed input accepted")
}
static let fixture = """
WEBVTT example
NOTE ignored comment
not a cue
STYLE
::cue { color: white; }
REGION
id:bottom
width:80%
cue-one
00:01.000 --> 00:04.000 line:90% position:50%,center align:center size:80% region:bottom
<v Speaker><b>Hello &amp; goodbye</b></v>
second <c.yellow>line</c> &lt;3 &#33;
cue-two
00:02.000 --> 00:03.000 vertical:rl
Overlap <00:02.500><i>text</i>
00:04.000 --> 00:05.000
Next
"""
static func main() async throws {
let document = try WebVTTDocument.parse("\u{FEFF}" + fixture.replacingOccurrences(of: "\n", with: "\r\n"))
let first = "Hello & goodbye\nsecond line <3 !"
try require(document.cues.count == 3, "BOM, CRLF, identifiers and metadata blocks")
try require(document.text(at: 0.999).isEmpty, "before first cue")
try require(document.text(at: 1) == first, "inclusive start and plain-text markup/entities")
try require(document.text(at: 2.5) == first + "\nOverlap text", "simultaneous overlapping cues")
try require(document.text(at: 3) == first, "exclusive overlap end")
try require(document.text(at: 4) == "Next", "adjacent exclusive end/inclusive start")
try require(document.text(at: 10).isEmpty, "after final cue")
try require(document.text(at: 2) == first + "\nOverlap text", "backward seek after final cue")
try require(document.text(at: .nan).isEmpty && document.text(at: -1).isEmpty, "invalid acknowledged positions")
let nested = try WebVTTDocument.parse("WEBVTT\n\n00:00.000 --> 01:00.000\nlong\n\n00:02.000 --> 00:03.000\nshort\n")
try require(nested.text(at: 4) == "long", "earlier long cue survives later cue ending")
try require(try WebVTTDocument.parse("WEBVTT\n\n01:02:03.004 --> 01:02:04.000\nhour\n").text(at: 3723.004) == "hour", "hour timestamps")
let zeroDuration = try WebVTTDocument.parse("WEBVTT\n\n00:01.000 --> 00:01.000\nnever visible\n\n00:02.000 --> 00:03.000\nvalid later cue\n")
try require(zeroDuration.text(at: 0.999).isEmpty && zeroDuration.text(at: 1).isEmpty && zeroDuration.text(at: 1.001).isEmpty, "zero-duration cue is never active")
try require(zeroDuration.text(at: 2) == "valid later cue", "zero-duration cue does not invalidate later subtitles")
try require(zeroDuration.text(at: 4).isEmpty && zeroDuration.text(at: 1).isEmpty, "zero-duration cue stays inactive after backward seek")
for malformed in [
"not VTT", "WEBVTT\n00:01.000 --> 00:02.000\nmissing separator",
"WEBVTT\n\n00:61.000 --> 00:62.000\nbad time",
"WEBVTT\n\n00:02.000 --> 00:01.000\nreversed interval",
"WEBVTT\n\n00:01.000 --> 00:02.000 size:101%\nbad setting",
"WEBVTT\n\n00:01.000 --> 00:02.000\n<b unfinished",
"WEBVTT\n\norphan identifier", "WEBVTT\n\n00:01.000 --> 00:02.000\ncue\n\nSTYLE\nlate style"
] { try reject { _ = try WebVTTDocument.parse(malformed) } }
try reject { _ = try WebVTTDocument.parse(String(repeating: "x", count: WebVTTDocument.byteLimit + 1)) }
let headers = "Authorization: Bearer fixture-secret\r\nReferer: https://provider.example/\r\nX-Track: english\r\n"
let request = try WebVTTDocument.request(url: "https://provider.example/track.jpeg", headers: headers)
try require(request.value(forHTTPHeaderField: "Authorization") == "Bearer fixture-secret", "resource authorization header")
try require(request.value(forHTTPHeaderField: "X-Track") == "english", "resource-specific custom header")
let whitespace = try WebVTTDocument.request(url: "https://provider.example/track", headers: "X-Exact: padded \t \r\n")
try require(whitespace.value(forHTTPHeaderField: "X-Exact") == " padded \t ", "preserve header value whitespace after generated separator")
for malformed in ["X: a\nInjected: value\r\n", "X: a\rInjected: value\r\n", "X: a\r\nX: b\r\n", "X: a", ": a\r\n"] {
try reject { _ = try WebVTTDocument.request(url: "https://provider.example/track", headers: malformed) }
}
for url in ["file:///tmp/track", "https://user:secret@provider.example/track", "https://provider.example/track#fragment"] {
try reject { _ = try WebVTTDocument.request(url: url, headers: "") }
}
let source = URL(string: "https://provider.example/track.jpeg")!
try require(WebVTTDocument.permitsRedirect(from: source, to: URL(string: "https://PROVIDER.example:443/other")!), "same origin includes default port")
for target in ["https://cdn.example/track", "http://provider.example/track", "https://provider.example:8443/track", "https://user:secret@provider.example/track"] {
try require(!WebVTTDocument.permitsRedirect(from: source, to: URL(string: target)!), "cross-origin or credentialed redirect rejection")
}
let server = try SubtitleFixtureServer(body: fixture)
defer { server.stop() }
let loaded = try await WebVTTDocument.load(url: server.url("/same.jpeg"), headers: headers)
try require(loaded.cues == document.cues, "load JPEG-named, image/jpeg VTT via same-origin redirect")
let requests = server.requests
try require(requests.count == 2 && requests.allSatisfy { $0.contains("Bearer fixture-secret") && $0.contains("english") }, "resource headers preserved only on same origin")
do {
_ = try await WebVTTDocument.load(url: server.url("/cross.jpeg"), headers: headers)
throw Failure(description: "FAIL: cross-origin redirect accepted")
} catch is Failure {
throw Failure(description: "FAIL: cross-origin redirect accepted")
} catch {
let message = error.localizedDescription
try require(!message.contains("fixture-secret") && !message.contains("127.0.0.1"), "sanitized visible transport error")
}
try require(server.requests.count == 3, "cross-origin redirect never sends credential-bearing request")
do {
_ = try await WebVTTDocument.load(url: server.url("/oversized.jpeg"), headers: "")
throw Failure(description: "FAIL: oversized response accepted")
} catch is Failure { throw Failure(description: "FAIL: oversized response accepted") }
catch { }
print("External subtitle checks passed")
}
}
/// A loopback-only HTTP fixture; no external services, dates, credentials or frameworks.
private final class SubtitleFixtureServer: @unchecked Sendable {
private let descriptor: Int32
private let port: UInt16
private let body: String
private let lock = NSLock()
private var recorded: [String] = []
private let ended = DispatchSemaphore(value: 0)
var requests: [String] {
lock.lock()
defer { lock.unlock() }
return recorded
}
init(body: String) throws {
self.body = body
let descriptor = socket(AF_INET, SOCK_STREAM, 0)
self.descriptor = descriptor
guard descriptor >= 0 else { throw ExternalSubtitleChecks.Failure(description: "FAIL: fixture socket") }
var address = sockaddr_in()
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
address.sin_family = sa_family_t(AF_INET)
address.sin_addr.s_addr = inet_addr("127.0.0.1")
let bound = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { bind(descriptor, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) }
}
var size = socklen_t(MemoryLayout<sockaddr_in>.size)
let named = withUnsafeMutablePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { getsockname(descriptor, $0, &size) }
}
guard bound == 0, named == 0, listen(descriptor, 4) == 0 else {
close(descriptor)
throw ExternalSubtitleChecks.Failure(description: "FAIL: fixture listener")
}
port = UInt16(bigEndian: address.sin_port)
DispatchQueue.global().async { self.serve() }
}
func url(_ path: String) -> String { "http://127.0.0.1:\(port)\(path)" }
func stop() {
shutdown(descriptor, SHUT_RDWR)
close(descriptor)
ended.wait()
}
private func serve() {
defer { ended.signal() }
while true {
let client = accept(descriptor, nil, nil)
guard client >= 0 else { return }
var noSignal: Int32 = 1
_ = setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &noSignal, socklen_t(MemoryLayout<Int32>.size))
var buffer = [UInt8](repeating: 0, count: 4096)
var data = Data()
while data.count < 65536 {
let count = recv(client, &buffer, buffer.count, 0)
if count <= 0 { break }
data.append(contentsOf: buffer.prefix(count))
if String(decoding: data, as: UTF8.self).contains("\r\n\r\n") { break }
}
let request = String(decoding: data, as: UTF8.self)
lock.lock()
recorded.append(request)
lock.unlock()
let path = request.split(separator: " ").dropFirst().first ?? ""
let response: String
switch path {
case "/same.jpeg": response = "HTTP/1.1 302 Found\r\nLocation: /track.jpeg\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
case "/cross.jpeg": response = "HTTP/1.1 302 Found\r\nLocation: http://localhost:\(port)/track.jpeg\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
case "/oversized.jpeg": response = "HTTP/1.1 200 OK\r\nContent-Length: \(WebVTTDocument.byteLimit + 1)\r\nConnection: close\r\n\r\n"
default: response = "HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\nContent-Length: \(body.utf8.count)\r\nConnection: close\r\n\r\n\(body)"
}
let bytes = Array(response.utf8)
bytes.withUnsafeBytes { raw in
var offset = 0
while offset < raw.count {
let count = send(client, raw.baseAddress!.advanced(by: offset), raw.count - offset, 0)
if count <= 0 { break }
offset += count
}
}
close(client)
}
}
}