import Foundation import CoreFoundation struct MiruroInterval: Sendable, Equatable { enum Kind: Sendable { case intro, outro } let kind: Kind var start: Double var end: Double let episode: Int var rawKind: String = "" var provider: String? = nil var votes: Double? = nil var referenceLength: Double? = nil } struct MiruroMetadata: Sendable { let title: String? let intervals: [MiruroInterval] func visibleIntervals(episode: Int, duration: Double) -> [MiruroInterval] { guard duration.isFinite, duration > 0 else { return [] } var intro: MiruroInterval?, outro: MiruroInterval? for var row in intervals { guard row.episode == episode, row.start.isFinite, row.end.isFinite, row.end > row.start, !(row.provider?.range(of: "crunchyroll", options: .caseInsensitive) != nil), (row.votes ?? 0).isFinite, (row.votes ?? 0) >= 0 else { continue } if let length = row.referenceLength, length.isFinite, length > 0, abs(length - duration) / duration > 0.05 { continue } row.start = max(0, row.start) row.end = min(duration, row.end) guard row.end > row.start else { continue } // Strictly greater preserves input order on ties, equivalent to stable descending votes. switch row.kind { case .intro: if intro == nil || (row.votes ?? 0) > (intro?.votes ?? 0) { intro = row } case .outro: if outro == nil || (row.votes ?? 0) > (outro?.votes ?? 0) { outro = row } } } var selected: [MiruroInterval] = [] if let intro { selected.append(intro) } if let outro { selected.append(outro) } return selected } } private struct MiruroMetadataError: LocalizedError { var errorDescription: String? { "Miruro optional metadata was unavailable or invalid." } } enum MiruroMetadataResolver { static let bodyLimit = 2 * 1024 * 1024 static let redirectLimit = 4 // Both HTTP redirects and SvelteKit's HTTP-200 redirect envelopes use this policy. struct Route { let mediaID: String let episode: Int private(set) var url: URL private var visited: Set private var redirects = 0 init(mediaID: String, episode: Int) throws { guard ResolvedSource.validIdentity(mediaID: mediaID, episode: episode) else { throw MiruroMetadataError() } self.mediaID = mediaID self.episode = episode url = URL(string: "https://barelystarted.miruro.tv/watch/\(mediaID)/episode/__data.json?ep=\(episode)")! visited = [url] } mutating func follow(_ location: String) throws { guard redirects < MiruroMetadataResolver.redirectLimit, !location.unicodeScalars.contains(where: { CharacterSet.whitespacesAndNewlines.contains($0) || CharacterSet.controlCharacters.contains($0) }), !location.contains("\\"), let destination = URL(string: location, relativeTo: url)?.absoluteURL, var parts = URLComponents(url: destination, resolvingAgainstBaseURL: true), parts.scheme?.lowercased() == "https", parts.host?.lowercased() == "barelystarted.miruro.tv", parts.port == nil || parts.port == 443, parts.user == nil, parts.password == nil, parts.fragment == nil, parts.percentEncodedPath == parts.path, parts.percentEncodedQuery == "ep=\(episode)" else { throw MiruroMetadataError() } let path = parts.path.split(separator: "/", omittingEmptySubsequences: false) guard (path.count == 4 || path.count == 5), path[0].isEmpty, path[1] == "watch", path[2] == Substring(mediaID), !path[3].isEmpty, path[3].utf8.count <= 512, path[3].utf8.allSatisfy({ (65...90).contains($0) || (97...122).contains($0) || (48...57).contains($0) || $0 == 45 || $0 == 95 }), path.count == 4 || path[4] == "__data.json" else { throw MiruroMetadataError() } parts.scheme = "https" parts.host = "barelystarted.miruro.tv" parts.port = nil parts.path = "/watch/\(mediaID)/\(path[3])/__data.json" guard let next = parts.url, visited.insert(next).inserted else { throw MiruroMetadataError() } redirects += 1 url = next } } static func resolve(mediaID: String, episode: Int) async throws -> MiruroMetadata { try Task.checkCancellation() var route = try Route(mediaID: mediaID, episode: episode) let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForRequest = 25 configuration.timeoutIntervalForResource = 25 configuration.httpShouldSetCookies = false configuration.httpCookieStorage = nil configuration.urlCache = nil let session = URLSession(configuration: configuration, delegate: MetadataRedirectPolicy(), delegateQueue: nil) defer { session.invalidateAndCancel() } do { while true { try Task.checkCancellation() var request = URLRequest(url: route.url) request.timeoutInterval = 25 request.setValue("application/json, text/sveltekit-data", forHTTPHeaderField: "Accept") let (bytes, response) = try await session.bytes(for: request) guard let http = response as? HTTPURLResponse else { throw MiruroMetadataError() } if [301, 302, 303, 307, 308].contains(http.statusCode) { guard let location = http.value(forHTTPHeaderField: "Location") else { throw MiruroMetadataError() } try route.follow(location) continue } try validateResponse(response) var body = Data() if response.expectedContentLength > 0 { body.reserveCapacity(Int(response.expectedContentLength)) } for try await byte in bytes { try Task.checkCancellation() guard body.count < bodyLimit else { throw MiruroMetadataError() } body.append(byte) } try Task.checkCancellation() switch try decodeRoute(body, mediaID: mediaID, episode: episode) { case .redirect(let location): try route.follow(location) case .metadata(let metadata): return metadata } } } catch { if Task.isCancelled || error is CancellationError || (error as? URLError)?.code == .cancelled { throw CancellationError() } throw MiruroMetadataError() } } static func validateResponse(_ response: URLResponse) throws { guard let http = response as? HTTPURLResponse, http.statusCode == 200, ["application/json", "text/sveltekit-data"].contains(http.mimeType?.lowercased() ?? ""), http.expectedContentLength <= Int64(bodyLimit) else { throw MiruroMetadataError() } } enum RouteData { case redirect(String), metadata(MiruroMetadata) } static func decode(_ data: Data, mediaID: String, episode: Int) throws -> MiruroMetadata { guard case .metadata(let metadata) = try decodeRoute(data, mediaID: mediaID, episode: episode) else { throw MiruroMetadataError() } return metadata } static func decodeRoute(_ data: Data, mediaID: String, episode: Int) throws -> RouteData { guard data.count <= bodyLimit, ResolvedSource.validIdentity(mediaID: mediaID, episode: episode) else { throw MiruroMetadataError() } // Bound JSON nesting before Foundation allocates containers. Devalue traversal below is schema-guided, // so unrelated source payloads and unsupported tagged types are never recursively expanded. var depth = 0, quoted = false, escaped = false for byte in data { if quoted { if escaped { escaped = false } else if byte == 92 { escaped = true } else if byte == 34 { quoted = false } } else if byte == 34 { quoted = true } else if byte == 91 || byte == 123 { depth += 1 guard depth <= 64 else { throw MiruroMetadataError() } } else if byte == 93 || byte == 125 { depth -= 1 guard depth >= 0 else { throw MiruroMetadataError() } } } let lines = data.split(separator: 10).filter { !$0.allSatisfy({ $0 == 13 || $0 == 32 || $0 == 9 }) } guard !lines.isEmpty, lines.count <= 128 else { throw MiruroMetadataError() } let envelopes = try lines.map { line -> [String: Any] in guard let envelope = try JSONSerialization.jsonObject(with: Data(line)) as? [String: Any] else { throw MiruroMetadataError() } return envelope } // Chunks may arrive before the initial envelope and each has its own independent table. let initials = envelopes.filter { ($0["type"] as? String) == "data" || ($0["type"] as? String) == "redirect" } guard initials.count == 1, let initial = initials.first else { throw MiruroMetadataError() } if initial["type"] as? String == "redirect" { guard envelopes.count == 1, let location = initial["location"] as? String else { throw MiruroMetadataError() } return .redirect(location) } guard let nodes = initial["nodes"] as? [Any], nodes.count > 1, let node = nodes[1] as? [String: Any], node["type"] as? String == "data", let table = node["data"] as? [Any], !table.isEmpty else { throw MiruroMetadataError() } let reader = MetadataReader(envelopes: envelopes) let root = MetadataReader.Cursor(table: table, index: 0, depth: 0) let media = try? reader.field(root, "media") if let id = media.flatMap({ try? reader.string(reader.field($0, "id")) }), id != mediaID { throw MiruroMetadataError() } if let number = try? reader.integer(reader.field(root, "episodeNumber")), number != episode { throw MiruroMetadataError() } var series: String? if let media, let title = try? reader.field(media, "title") { series = cleanTitle(try? reader.string(title)) if series == nil { for key in ["english", "romaji", "native"] { if let value = cleanTitle(try? reader.string(reader.field(title, key))) { series = value; break } } } } let episodes = try? reader.field(root, "episodesData") var episodeTitle: String? if let episodes { if let current = try? reader.field(episodes, "currentEpisode"), (try? reader.integer(reader.field(current, "number"))) == episode { episodeTitle = cleanTitle(try? reader.string(reader.field(current, "title"))) } if episodeTitle == nil, let rows = try? reader.array(reader.field(episodes, "episodes")) { for row in rows where (try? reader.integer(reader.field(row, "number"))) == episode { episodeTitle = cleanTitle(try? reader.string(reader.field(row, "title"))) if episodeTitle != nil { break } } } } var intervals: [MiruroInterval] = [] if let episodes, let rows = try? reader.array(reader.field(episodes, "aniskip")) { for row in rows { guard let rawKind = try? reader.string(reader.field(row, "type")), let number = try? reader.integer(reader.field(row, "episode")), let start = try? reader.number(reader.field(row, "start")), let end = try? reader.number(reader.field(row, "end")) else { continue } let kind: MiruroInterval.Kind switch rawKind { case "op", "mixed-op": kind = .intro case "ed", "mixed-ed": kind = .outro default: continue } intervals.append(MiruroInterval(kind: kind, start: start, end: end, episode: number, rawKind: rawKind, provider: try? reader.string(reader.field(row, "provider")), votes: try? reader.number(reader.field(row, "votes")), referenceLength: try? reader.number(reader.field(row, "episode_length")))) } } let title: String? if let series { title = series + " — Episode \(episode)" + (episodeTitle.map { ": " + $0 } ?? "") } else { title = episodeTitle.map { "Episode \(episode): " + $0 } } return .metadata(MiruroMetadata(title: title, intervals: intervals)) } private static func cleanTitle(_ value: String?) -> String? { guard let value, value.utf8.count <= 2048, !value.unicodeScalars.contains(where: { CharacterSet.controlCharacters.contains($0) }) else { return nil } let title = value.trimmingCharacters(in: .whitespacesAndNewlines) return title.isEmpty ? nil : title } } // Only traverse fields needed for metadata. The budget counts repeated references as well as strings, // bounding amplification without copying/expanding whole devalue objects or sourcesData. private final class MetadataReader { struct Cursor { let table: [Any]; let index: Int; let depth: Int } private let envelopes: [[String: Any]] private var budget = MiruroMetadataResolver.bodyLimit init(envelopes: [[String: Any]]) { self.envelopes = envelopes } private func charge(_ cost: Int = 32) throws { guard cost <= budget else { throw MiruroMetadataError() } budget -= cost } private func value(_ cursor: Cursor) throws -> Any { let cursor = try resolved(cursor) try charge() let value = cursor.table[cursor.index] if let tag = value as? [Any], tag.first is String { throw MiruroMetadataError() } return value } private func reference(_ value: Any, from cursor: Cursor) throws -> Cursor { guard let index = Self.integer(value), index >= 0 else { throw MiruroMetadataError() } return Cursor(table: cursor.table, index: index, depth: cursor.depth + 1) } func field(_ cursor: Cursor, _ name: String) throws -> Cursor { // A promise's resolved object must retain its own table for subsequent references. let cursor = try resolved(cursor) guard let object = try value(cursor) as? [String: Any], let reference = object[name] else { throw MiruroMetadataError() } return try self.reference(reference, from: cursor) } private func resolved(_ cursor: Cursor) throws -> Cursor { try charge() guard cursor.depth < 32, cursor.table.indices.contains(cursor.index) else { throw MiruroMetadataError() } guard let tag = cursor.table[cursor.index] as? [Any], tag.first as? String == "Promise" else { return cursor } guard tag.count == 2 else { throw MiruroMetadataError() } let id = try integer(reference(tag[1], from: cursor)) let matches = envelopes.filter { ($0["type"] as? String) == "chunk" && Self.integer($0["id"]) == id } guard matches.count == 1, let chunk = matches.first, chunk["error"] == nil, let table = chunk["data"] as? [Any] else { throw MiruroMetadataError() } return try resolved(Cursor(table: table, index: 0, depth: cursor.depth + 1)) } func array(_ cursor: Cursor) throws -> [Cursor] { let cursor = try resolved(cursor) guard let rows = try value(cursor) as? [Any] else { throw MiruroMetadataError() } try charge(rows.count * 32) // Bad individual references do not invalidate their siblings. return rows.compactMap { try? reference($0, from: cursor) } } func string(_ cursor: Cursor) throws -> String { guard let string = try value(cursor) as? String else { throw MiruroMetadataError() } try charge(string.utf8.count) return string } func number(_ cursor: Cursor) throws -> Double { guard let number = try value(cursor) as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else { throw MiruroMetadataError() } return number.doubleValue } func integer(_ cursor: Cursor) throws -> Int { guard let integer = Self.integer(try value(cursor)) else { throw MiruroMetadataError() } return integer } private static func integer(_ value: Any?) -> Int? { guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID() else { return nil } let value = number.doubleValue guard value.isFinite, value.rounded(.towardZero) == value, value >= 0, value < Double(Int.max) else { return nil } return Int(value) } } private final class MetadataRedirectPolicy: NSObject, URLSessionTaskDelegate, @unchecked Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } }