74 lines
3.9 KiB
Swift
74 lines
3.9 KiB
Swift
import Foundation
|
||
import Darwin
|
||
|
||
struct StreamQueue: Sendable {
|
||
struct Entry: Sendable {
|
||
let url: URL
|
||
let identity: String
|
||
let progressFile: URL
|
||
let label: String
|
||
}
|
||
let entries: [Entry]
|
||
|
||
private struct Document: Decodable {
|
||
struct Item: Decodable { let url: String; let progress_file: String }
|
||
let version: Int
|
||
let items: [Item]
|
||
}
|
||
private struct InvalidQueue: LocalizedError {
|
||
var errorDescription: String? { "Invalid stream queue: expected version 1, 1–4096 unique supported videos and new absolute report files in private owned directories." }
|
||
}
|
||
|
||
static func load(from file: URL) throws -> StreamQueue {
|
||
do {
|
||
guard file.isFileURL, file.path.hasPrefix("/") else { throw InvalidQueue() }
|
||
let descriptor = Darwin.open(file.path, O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC)
|
||
guard descriptor >= 0 else { throw InvalidQueue() }
|
||
let handle = FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
|
||
defer { try? handle.close() }
|
||
var info = stat()
|
||
guard fstat(descriptor, &info) == 0, (info.st_mode & S_IFMT) == S_IFREG else { throw InvalidQueue() }
|
||
let data = try handle.read(upToCount: 8 * 1024 * 1024 + 1) ?? Data()
|
||
guard data.count <= 8 * 1024 * 1024 else { throw InvalidQueue() }
|
||
let document = try JSONDecoder().decode(Document.self, from: data)
|
||
guard document.version == 1, (1...4096).contains(document.items.count) else { throw InvalidQueue() }
|
||
var identities = Set<String>()
|
||
var reports = Set<String>()
|
||
let entries = try document.items.map { item -> Entry in
|
||
guard item.url == item.url.trimmingCharacters(in: .whitespacesAndNewlines) else { throw InvalidQueue() }
|
||
let stream = try StreamURL.parse(item.url)
|
||
let url: URL
|
||
let identity: String
|
||
let label: String
|
||
switch stream {
|
||
case .youtube(let canonical):
|
||
url = canonical
|
||
identity = canonical.absoluteString
|
||
label = "YouTube · " + (URLComponents(url: canonical, resolvingAgainstBaseURL: false)?.queryItems?.first?.value ?? "Video")
|
||
case .miruro(let mediaID, let episode):
|
||
url = URL(string: item.url)!
|
||
identity = "miruro:\(mediaID):\(episode)"
|
||
label = "\(url.lastPathComponent.replacingOccurrences(of: "-", with: " ")) · Episode \(episode)"
|
||
}
|
||
guard identities.insert(identity).inserted,
|
||
item.progress_file.hasPrefix("/"), !item.progress_file.contains("\0") else { throw InvalidQueue() }
|
||
let report = URL(fileURLWithPath: item.progress_file).standardizedFileURL
|
||
let parent = report.deletingLastPathComponent()
|
||
var directoryInfo = stat()
|
||
guard lstat(parent.path, &directoryInfo) == 0,
|
||
(directoryInfo.st_mode & S_IFMT) == S_IFDIR,
|
||
directoryInfo.st_uid == geteuid(), (directoryInfo.st_mode & 0o7777) == 0o700,
|
||
let resolvedParent = realpath(parent.path, nil) else { throw InvalidQueue() }
|
||
defer { free(resolvedParent) }
|
||
let canonicalReport = URL(fileURLWithPath: String(cString: resolvedParent), isDirectory: true)
|
||
.appendingPathComponent(report.lastPathComponent)
|
||
guard reports.insert(canonicalReport.path).inserted else { throw InvalidQueue() }
|
||
var existing = stat()
|
||
guard lstat(canonicalReport.path, &existing) != 0, errno == ENOENT else { throw InvalidQueue() }
|
||
return Entry(url: url, identity: identity, progressFile: canonicalReport, label: label)
|
||
}
|
||
return StreamQueue(entries: entries)
|
||
} catch { throw InvalidQueue() }
|
||
}
|
||
}
|