import Foundation import Darwin struct ResolvedSubtitle: Decodable, Sendable { let url: String let format: String let language: String let label: String let isDefault: Bool private let headerFields: [String: String] var headers: String { headerFields.sorted { $0.key < $1.key }.map { "\($0.key): \($0.value)\r\n" }.joined() } fileprivate var validatedHeaders: [String: String] { headerFields } init(url: String, format: String, language: String, label: String, isDefault: Bool, headers: [String: String]) { self.url = url self.format = format self.language = language self.label = label self.isDefault = isDefault self.headerFields = headers } private enum CodingKeys: String, CodingKey { case url, format, language, label case isDefault = "default" case headerFields = "headers" } } struct ResolvedSource: Sendable { let url: String let headers: String let identity: String let title: String let subtitles: [ResolvedSubtitle] struct Document: Decodable { struct Identity: Decodable { let provider: String let media_id: String let episode: Int } let version: Int let url: String let headers: [String: String] let identity: Identity let title: String? let subtitles: [ResolvedSubtitle] } private struct InvalidSource: LocalizedError { var errorDescription: String? { "Invalid or unreadable resolved source file." } } static func load(from file: URL) throws -> ResolvedSource { do { guard file.isFileURL else { throw InvalidSource() } let descriptor: Int32 = file.withUnsafeFileSystemRepresentation { path in guard let path else { return -1 } return Darwin.open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC) } guard descriptor >= 0 else { throw InvalidSource() } var info = stat() guard fstat(descriptor, &info) == 0, (info.st_mode & S_IFMT) == S_IFREG else { Darwin.close(descriptor) throw InvalidSource() } let handle = FileHandle(fileDescriptor: descriptor, closeOnDealloc: true) defer { try? handle.close() } let data = try handle.read(upToCount: 1_048_577) ?? Data() guard data.count <= 1_048_576 else { throw InvalidSource() } let document = try JSONDecoder().decode(Document.self, from: data) return try validated(document) } catch { throw InvalidSource() } } static func validIdentity(mediaID: String, episode: Int) -> Bool { !mediaID.isEmpty && mediaID.utf8.count <= 512 && mediaID.utf8.allSatisfy({ isIDByte($0) }) && episode > 0 && episode <= Int(Int32.max) } static func validated(_ document: Document) throws -> ResolvedSource { let id = document.identity guard document.version == 1, id.provider == "miruro", validIdentity(mediaID: id.media_id, episode: id.episode), validURL(document.url), validHeaders(document.headers), validText(document.title ?? "", limit: 4096), document.subtitles.count <= 32, document.subtitles.filter({ $0.isDefault }).count <= 1 else { throw InvalidSource() } for subtitle in document.subtitles { guard validURL(subtitle.url), subtitle.format == "vtt", validText(subtitle.language, limit: 128), !subtitle.language.isEmpty, validText(subtitle.label, limit: 4096), !subtitle.label.isEmpty, validHeaders(subtitle.validatedHeaders) else { throw InvalidSource() } } let headers = document.headers.sorted { $0.key < $1.key } .map { "\($0.key): \($0.value)\r\n" }.joined() return ResolvedSource(url: document.url, headers: headers, identity: "miruro:\(id.media_id):\(id.episode)", title: document.title ?? "Miruro episode \(id.episode)", subtitles: document.subtitles) } private static func isIDByte(_ byte: UInt8) -> Bool { (65...90).contains(byte) || (97...122).contains(byte) || (48...57).contains(byte) || byte == 45 || byte == 95 } private static func validText(_ text: String, limit: Int) -> Bool { text.utf8.count <= limit && !text.unicodeScalars.contains { $0.value < 32 || $0.value == 127 } } private static func validURL(_ text: String) -> Bool { guard !text.isEmpty, validText(text, limit: 32768), !text.contains(where: { $0.isWhitespace }), let components = URLComponents(string: text), components.scheme == "https" || components.scheme == "http", let host = components.host, !host.isEmpty, components.user == nil, components.password == nil, components.fragment == nil, components.url != nil else { return false } return components.port == nil || (1...65535).contains(components.port!) } private static func validHeaders(_ headers: [String: String]) -> Bool { guard headers.count <= 64 else { return false } var names = Set() var size = 0 for (name, value) in headers { guard !name.isEmpty, name.utf8.count <= 256, name.utf8.allSatisfy({ isIDByte($0) || "!#$%&'*+.^`|~".utf8.contains($0) }), names.insert(name.lowercased()).inserted, value.utf8.count <= 16384, !value.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }) else { return false } size += name.utf8.count + value.utf8.count + 4 } return size <= 65536 } }