fix(runtime): harden application service boundaries

This commit is contained in:
edde746
2026-07-24 03:46:46 +02:00
parent 658da37b48
commit e0bf66eea8
309 changed files with 32574 additions and 4369 deletions
+48 -48
View File
@@ -1,12 +1,17 @@
import CryptoKit
import Foundation
import TVServices
private enum TopShelfShared {
static let schemaVersion = 2
static let appGroupIdentifier = "group.com.edde746.plezy"
static let cacheDataKey = "PlezySystemShelfCacheData"
static let artworkDirectoryName = "SystemShelfArtwork"
static var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupIdentifier)
static var sharedDefaults: UserDefaults? { UserDefaults(suiteName: appGroupIdentifier) }
static var artworkRoot: URL? {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?
.appendingPathComponent(artworkDirectoryName, isDirectory: true)
}
}
@@ -22,7 +27,7 @@ private struct TopShelfCachePayload: Decodable {
let title: String
let episodeTitle: String?
let description: String?
let posterUri: String?
let artworkKey: String?
let type: String?
let duration: Double?
let lastPlaybackPosition: Double?
@@ -34,7 +39,7 @@ private struct TopShelfCachePayload: Decodable {
case title
case episodeTitle
case description
case posterUri
case artworkKey
case type
case duration
case lastPlaybackPosition
@@ -48,7 +53,7 @@ private struct TopShelfCachePayload: Decodable {
title = try container.decode(String.self, forKey: .title)
episodeTitle = try container.decodeIfPresent(String.self, forKey: .episodeTitle)
description = try container.decodeIfPresent(String.self, forKey: .description)
posterUri = try container.decodeIfPresent(String.self, forKey: .posterUri)
artworkKey = try container.decodeIfPresent(String.self, forKey: .artworkKey)
type = try container.decodeIfPresent(String.self, forKey: .type)
duration = container.decodeFlexibleDoubleIfPresent(.duration)
lastPlaybackPosition = container.decodeFlexibleDoubleIfPresent(.lastPlaybackPosition)
@@ -57,6 +62,8 @@ private struct TopShelfCachePayload: Decodable {
}
}
let schemaVersion: Int
let ownerId: String
let sections: [Section]
}
@@ -64,58 +71,42 @@ private extension KeyedDecodingContainer {
func decodeFlexibleDoubleIfPresent(_ key: Key) -> Double? {
if let value = try? decodeIfPresent(Double.self, forKey: key) { return value }
if let value = try? decodeIfPresent(Int.self, forKey: key) { return Double(value) }
if let value = try? decodeIfPresent(String.self, forKey: key) { return Double(value) }
return nil
}
func decodeFlexibleIntIfPresent(_ key: Key) -> Int? {
if let value = try? decodeIfPresent(Int.self, forKey: key) { return value }
if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) }
if let value = try? decodeIfPresent(String.self, forKey: key) { return Int(value) }
return nil
}
}
final class TopShelfProvider: TVTopShelfContentProvider {
override func loadTopShelfContent() async -> (any TVTopShelfContent)? {
return buildContent()
}
override func loadTopShelfContent() async -> (any TVTopShelfContent)? { buildContent() }
private func buildContent() -> TVTopShelfContent? {
guard let defaults = TopShelfShared.sharedDefaults else {
return nil
}
guard let defaults = TopShelfShared.sharedDefaults,
let data = defaults.data(forKey: TopShelfShared.cacheDataKey)
else { return nil }
guard let data = defaults.data(forKey: TopShelfShared.cacheDataKey) else {
return nil
}
let payload: TopShelfCachePayload
do {
payload = try JSONDecoder().decode(TopShelfCachePayload.self, from: data)
} catch {
return nil
}
guard let payload = try? JSONDecoder().decode(TopShelfCachePayload.self, from: data),
payload.schemaVersion == TopShelfShared.schemaVersion,
!payload.ownerId.isEmpty
else { return nil }
let sections = payload.sections.compactMap { section -> TVTopShelfItemCollection<TVTopShelfSectionedItem>? in
let items = section.items.compactMap(makeTopShelfItem)
let items = section.items.compactMap { makeTopShelfItem($0, ownerId: payload.ownerId) }
guard !items.isEmpty else { return nil }
let collection = TVTopShelfItemCollection(items: items)
collection.title = section.title
return collection
}
guard !sections.isEmpty else {
return nil
}
guard !sections.isEmpty else { return nil }
return TVTopShelfSectionedContent(sections: sections)
}
private func makeTopShelfItem(_ cacheItem: TopShelfCachePayload.Item) -> TVTopShelfSectionedItem? {
private func makeTopShelfItem(_ cacheItem: TopShelfCachePayload.Item, ownerId: String) -> TVTopShelfSectionedItem? {
guard !cacheItem.contentId.isEmpty else { return nil }
let item = TVTopShelfSectionedItem(identifier: cacheItem.contentId)
item.title = displayTitle(for: cacheItem)
item.imageShape = .hdtv
@@ -125,39 +116,48 @@ final class TopShelfProvider: TVTopShelfContentProvider {
{
item.playbackProgress = min(max(position / duration, 0), 1)
}
if let url = deepLinkURL(contentId: cacheItem.contentId) {
let action = TVTopShelfAction(url: url)
item.displayAction = action
item.playAction = action
}
if let posterUri = cacheItem.posterUri, let imageURL = URL(string: posterUri) {
item.setImageURL(imageURL, for: .screenScale1x)
item.setImageURL(imageURL, for: .screenScale2x)
if let key = cacheItem.artworkKey, let localURL = localArtworkURL(ownerId: ownerId, key: key) {
item.setImageURL(localURL, for: .screenScale1x)
item.setImageURL(localURL, for: .screenScale2x)
}
return item
}
private func displayTitle(for item: TopShelfCachePayload.Item) -> String {
guard let episodeTitle = item.episodeTitle, !episodeTitle.isEmpty else {
return item.title
private func localArtworkURL(ownerId: String, key: String) -> URL? {
guard key.range(of: "^[a-f0-9]{32}\\.art$", options: .regularExpression) != nil,
let root = TopShelfShared.artworkRoot
else { return nil }
let ownerHash = SHA256.hash(data: Data(ownerId.utf8)).map { String(format: "%02x", $0) }.joined()
let canonicalRoot = root.standardizedFileURL.resolvingSymlinksInPath()
let ownerDirectory = canonicalRoot.appendingPathComponent(ownerHash, isDirectory: true)
.standardizedFileURL.resolvingSymlinksInPath()
guard ownerDirectory.deletingLastPathComponent() == canonicalRoot else { return nil }
let candidate = ownerDirectory.appendingPathComponent(key, isDirectory: false)
.standardizedFileURL.resolvingSymlinksInPath()
guard candidate.deletingLastPathComponent() == ownerDirectory else { return nil }
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: candidate.path, isDirectory: &isDirectory), !isDirectory.boolValue
else {
return nil
}
return candidate
}
private func displayTitle(for item: TopShelfCachePayload.Item) -> String {
guard let episodeTitle = item.episodeTitle, !episodeTitle.isEmpty else { return item.title }
let episodePrefix: String? = {
if let seasonNumber = item.seasonNumber, let episodeNumber = item.episodeNumber {
return "S\(seasonNumber) E\(episodeNumber)"
}
if let episodeNumber = item.episodeNumber {
return "E\(episodeNumber)"
}
if let episodeNumber = item.episodeNumber { return "E\(episodeNumber)" }
return nil
}()
if let episodePrefix {
return "\(item.title) - \(episodePrefix) - \(episodeTitle)"
}
if let episodePrefix { return "\(item.title) - \(episodePrefix) - \(episodeTitle)" }
return "\(item.title) - \(episodeTitle)"
}