mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Merge 0e12f9e15b into 734361bb9d
This commit is contained in:
commit
8d75a7374f
2 changed files with 126 additions and 25 deletions
|
|
@ -17,13 +17,13 @@ enum PairingFileStore {
|
||||||
private static let legacyFileName = "rp_pairing_file.plist"
|
private static let legacyFileName = "rp_pairing_file.plist"
|
||||||
|
|
||||||
static var url: URL {
|
static var url: URL {
|
||||||
directoryURL.appendingPathComponent(fileName)
|
directoryURL(fileManager: .default).appendingPathComponent(fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
static func prepareURL(fileManager: FileManager = .default) -> URL {
|
static func prepareURL(fileManager: FileManager = .default) -> URL {
|
||||||
let destination = url
|
let destination = directoryURL(fileManager: fileManager).appendingPathComponent(fileName)
|
||||||
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
try? fileManager.createDirectory(at: directoryURL(fileManager: fileManager), withIntermediateDirectories: true)
|
||||||
|
|
||||||
if let documentURL = documentSourceURL(fileManager: fileManager) {
|
if let documentURL = documentSourceURL(fileManager: fileManager) {
|
||||||
if !fileManager.fileExists(atPath: destination.path) ||
|
if !fileManager.fileExists(atPath: destination.path) ||
|
||||||
|
|
@ -41,15 +41,16 @@ enum PairingFileStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
try fileManager.createDirectory(at: directoryURL(fileManager: fileManager), withIntermediateDirectories: true)
|
||||||
|
|
||||||
let documentsDestination = documentsURL
|
let documentsDestination = documentsURL(fileManager: fileManager)
|
||||||
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
|
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
|
||||||
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
|
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
|
let destination = directoryURL(fileManager: fileManager).appendingPathComponent(fileName)
|
||||||
protectPairingFile(at: url, fileManager: fileManager)
|
try replaceItem(at: destination, with: documentsDestination, fileManager: fileManager)
|
||||||
|
protectPairingFile(at: destination, fileManager: fileManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
|
|
@ -75,36 +76,35 @@ enum PairingFileStore {
|
||||||
removeLegacyCopies(fileManager: fileManager)
|
removeLegacyCopies(fileManager: fileManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var directoryURL: URL {
|
private static func directoryURL(fileManager: FileManager) -> URL {
|
||||||
FileManager.default
|
fileManager
|
||||||
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||||
.appendingPathComponent("Pairing", isDirectory: true)
|
.appendingPathComponent("Pairing", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var legacyURLs: [URL] {
|
private static func legacyURLs(fileManager: FileManager) -> [URL] {
|
||||||
[
|
[
|
||||||
directoryURL.appendingPathComponent(legacyFileName),
|
directoryURL(fileManager: fileManager).appendingPathComponent(legacyFileName),
|
||||||
documentsURL,
|
documentsURL(fileManager: fileManager),
|
||||||
URL.documentsDirectory.appendingPathComponent(legacyFileName)
|
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
.appendingPathComponent(legacyFileName)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var documentsURL: URL {
|
private static func documentsURL(fileManager: FileManager) -> URL {
|
||||||
URL.documentsDirectory.appendingPathComponent(fileName)
|
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
.appendingPathComponent(fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func documentSourceURL(fileManager: FileManager) -> URL? {
|
private static func documentSourceURL(fileManager: FileManager) -> URL? {
|
||||||
[documentsURL, URL.documentsDirectory.appendingPathComponent(legacyFileName)]
|
let canonical = documentsURL(fileManager: fileManager)
|
||||||
.filter { fileManager.fileExists(atPath: $0.path) }
|
let legacy = canonical.deletingLastPathComponent().appendingPathComponent(legacyFileName)
|
||||||
.max {
|
// Imports can preserve old timestamps; the canonical file still takes precedence.
|
||||||
let firstDate = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
|
return [canonical, legacy].first { fileManager.fileExists(atPath: $0.path) }
|
||||||
let secondDate = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
|
|
||||||
return firstDate < secondDate
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
|
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
|
||||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
for legacyURL in legacyURLs(fileManager: fileManager) where fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
do {
|
do {
|
||||||
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
|
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -123,8 +123,8 @@ enum PairingFileStore {
|
||||||
.deletingLastPathComponent()
|
.deletingLastPathComponent()
|
||||||
.appendingPathComponent(UUID().uuidString + ".tmp")
|
.appendingPathComponent(UUID().uuidString + ".tmp")
|
||||||
try? fileManager.removeItem(at: temporary)
|
try? fileManager.removeItem(at: temporary)
|
||||||
try fileManager.copyItem(at: source, to: temporary)
|
|
||||||
defer { try? fileManager.removeItem(at: temporary) }
|
defer { try? fileManager.removeItem(at: temporary) }
|
||||||
|
try fileManager.copyItem(at: source, to: temporary)
|
||||||
|
|
||||||
if fileManager.fileExists(atPath: destination.path) {
|
if fileManager.fileExists(atPath: destination.path) {
|
||||||
_ = try fileManager.replaceItemAt(destination, withItemAt: temporary)
|
_ = try fileManager.replaceItemAt(destination, withItemAt: temporary)
|
||||||
|
|
@ -134,7 +134,7 @@ enum PairingFileStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func removeLegacyCopies(fileManager: FileManager) {
|
private static func removeLegacyCopies(fileManager: FileManager) {
|
||||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
for legacyURL in legacyURLs(fileManager: fileManager) where fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
try? fileManager.removeItem(at: legacyURL)
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,86 @@ import Testing
|
||||||
|
|
||||||
struct StikDebugTests {
|
struct StikDebugTests {
|
||||||
|
|
||||||
|
@Test func pairingImportSurvivesNewerLegacyFile() throws {
|
||||||
|
let fileManager = try PairingFileManager()
|
||||||
|
defer { try? fileManager.removeItem(at: fileManager.root) }
|
||||||
|
let canonical = fileManager.documents.appendingPathComponent(PairingFileStore.fileName)
|
||||||
|
let legacy = fileManager.documents.appendingPathComponent("rp_pairing_file.plist")
|
||||||
|
let source = fileManager.root.appendingPathComponent("import.plist")
|
||||||
|
let imported = Data("imported pairing".utf8)
|
||||||
|
try Data("stale legacy pairing".utf8).write(to: legacy)
|
||||||
|
try fileManager.setAttributes([.modificationDate: Date(timeIntervalSince1970: 200)], ofItemAtPath: legacy.path)
|
||||||
|
try imported.write(to: source)
|
||||||
|
try fileManager.setAttributes([.modificationDate: Date(timeIntervalSince1970: 100)], ofItemAtPath: source.path)
|
||||||
|
|
||||||
|
try PairingFileStore.importFromPicker(source, fileManager: fileManager)
|
||||||
|
for _ in 0..<2 {
|
||||||
|
let active = PairingFileStore.prepareURL(fileManager: fileManager)
|
||||||
|
#expect(try Data(contentsOf: active) == imported)
|
||||||
|
#expect(try fileManager.attributesOfItem(atPath: active.path)[.posixPermissions] as? Int == 0o600)
|
||||||
|
}
|
||||||
|
try PairingFileStore.importFromPicker(canonical, fileManager: fileManager)
|
||||||
|
#expect(throws: (any Error).self) {
|
||||||
|
try PairingFileStore.importFromPicker(fileManager.root.appendingPathComponent("missing.plist"), fileManager: fileManager)
|
||||||
|
}
|
||||||
|
#expect(throws: (any Error).self) {
|
||||||
|
try PairingFileStore.replace(with: fileManager.root.appendingPathComponent("missing.plist"), fileManager: fileManager)
|
||||||
|
}
|
||||||
|
#expect(try Data(contentsOf: canonical) == imported)
|
||||||
|
let active = fileManager.support.appendingPathComponent("Pairing").appendingPathComponent(PairingFileStore.fileName)
|
||||||
|
#expect(try Data(contentsOf: active) == imported)
|
||||||
|
for directory in [fileManager.documents, active.deletingLastPathComponent()] {
|
||||||
|
#expect(try fileManager.contentsOfDirectory(atPath: directory.path).allSatisfy { !$0.hasSuffix(".tmp") })
|
||||||
|
}
|
||||||
|
try PairingFileStore.remove(fileManager: fileManager)
|
||||||
|
#expect(!fileManager.fileExists(atPath: active.path))
|
||||||
|
#expect(!fileManager.fileExists(atPath: canonical.path))
|
||||||
|
#expect(!fileManager.fileExists(atPath: legacy.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [100.0, 200.0, 300.0])
|
||||||
|
func canonicalPairingTakesPrecedence(modificationTime: TimeInterval) throws {
|
||||||
|
let fileManager = try PairingFileManager()
|
||||||
|
defer { try? fileManager.removeItem(at: fileManager.root) }
|
||||||
|
let canonical = fileManager.documents.appendingPathComponent(PairingFileStore.fileName)
|
||||||
|
let legacy = fileManager.documents.appendingPathComponent("rp_pairing_file.plist")
|
||||||
|
let active = PairingFileStore.prepareURL(fileManager: fileManager)
|
||||||
|
try Data("stale active pairing".utf8).write(to: active)
|
||||||
|
try Data("legacy pairing".utf8).write(to: legacy)
|
||||||
|
try fileManager.setAttributes([.modificationDate: Date(timeIntervalSince1970: 200)], ofItemAtPath: legacy.path)
|
||||||
|
let canonicalData = Data("canonical pairing".utf8)
|
||||||
|
try canonicalData.write(to: canonical)
|
||||||
|
try fileManager.setAttributes([.modificationDate: Date(timeIntervalSince1970: modificationTime)], ofItemAtPath: canonical.path)
|
||||||
|
#expect(try Data(contentsOf: PairingFileStore.prepareURL(fileManager: fileManager)) == canonicalData)
|
||||||
|
|
||||||
|
let edited = Data("edited through Files".utf8)
|
||||||
|
try edited.write(to: canonical)
|
||||||
|
try fileManager.setAttributes([.modificationDate: Date(timeIntervalSince1970: 50)], ofItemAtPath: canonical.path)
|
||||||
|
#expect(try Data(contentsOf: PairingFileStore.prepareURL(fileManager: fileManager)) == edited)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["documents", "support", "none"])
|
||||||
|
func pairingLegacyMigration(source: String) throws {
|
||||||
|
let fileManager = try PairingFileManager()
|
||||||
|
defer { try? fileManager.removeItem(at: fileManager.root) }
|
||||||
|
let pairingDirectory = fileManager.support.appendingPathComponent("Pairing")
|
||||||
|
let expected = pairingDirectory.appendingPathComponent(PairingFileStore.fileName)
|
||||||
|
let data = Data("legacy pairing".utf8)
|
||||||
|
if source != "none" {
|
||||||
|
let directory = source == "documents" ? fileManager.documents : pairingDirectory
|
||||||
|
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||||
|
try data.write(to: directory.appendingPathComponent("rp_pairing_file.plist"))
|
||||||
|
}
|
||||||
|
let active = PairingFileStore.prepareURL(fileManager: fileManager)
|
||||||
|
#expect(active == expected)
|
||||||
|
if source == "none" {
|
||||||
|
#expect(!fileManager.fileExists(atPath: active.path))
|
||||||
|
} else {
|
||||||
|
#expect(try Data(contentsOf: active) == data)
|
||||||
|
#expect(try fileManager.attributesOfItem(atPath: active.path)[.posixPermissions] as? Int == 0o600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func txmDetectionUsesClassicTXMBeforeIOS266() async throws {
|
@Test func txmDetectionUsesClassicTXMBeforeIOS266() async throws {
|
||||||
#expect(
|
#expect(
|
||||||
ProcessInfo.hasTXMSupport(
|
ProcessInfo.hasTXMSupport(
|
||||||
|
|
@ -79,3 +159,24 @@ struct StikDebugTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class PairingFileManager: FileManager, @unchecked Sendable {
|
||||||
|
let root: URL
|
||||||
|
var documents: URL { root.appendingPathComponent("Documents") }
|
||||||
|
var support: URL { root.appendingPathComponent("Application Support") }
|
||||||
|
|
||||||
|
init(root: URL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)) throws {
|
||||||
|
self.root = root
|
||||||
|
super.init()
|
||||||
|
try createDirectory(at: documents, withIntermediateDirectories: true)
|
||||||
|
try createDirectory(at: support, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL] {
|
||||||
|
switch directory {
|
||||||
|
case .documentDirectory: return [documents]
|
||||||
|
case .applicationSupportDirectory: return [support]
|
||||||
|
default: return super.urls(for: directory, in: domainMask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue