Add Swift app for iPod contacts and calendar export
Replaces the bash/AppleScript/Python app bundle with a SwiftUI app backed by a testable library target. The app now reads Contacts and EventKit directly rather than sanitizing files someone exported by hand, so the properties that break click-wheel firmware are never written instead of being stripped afterwards: no PHOTO, no X-, no VALARM, no VTIMEZONE, no TZID. Recurring events arrive from EventKit already expanded into occurrences, so no RRULE is emitted either - the open question from the previous version no longer applies. Sanitizing existing .vcf/.ics files is kept as a second path, and gains handling the Python lacked: vCard 2.1 quoted-printable soft line breaks, Apple's item1. property groups, nested STANDARD/DAYLIGHT components inside VTIMEZONE, and UTC times converted to local floating times. Output is laid out as Contacts/ and Calendars/ subfolders mirroring the device, one file per contact and one per calendar. Built via SwiftPM plus Scripts/bundle.sh, which assembles and ad-hoc signs the bundle. Ad-hoc signing is required rather than cosmetic: Contacts and Calendars key permissions off the code signature. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C5X1tYo9oxAfvoiFMh1QQr
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
|
||||
/// Reads text files of unknown provenance.
|
||||
///
|
||||
/// `.vcf` and `.ics` files reaching the app may have come from any address book
|
||||
/// or calendar going back twenty years, so the declared encoding — if there is
|
||||
/// one — cannot be trusted. Candidates are tried in order of likelihood.
|
||||
public enum TextFile {
|
||||
public static func read(contentsOf url: URL) throws -> String {
|
||||
let data = try Data(contentsOf: url)
|
||||
return decode(data)
|
||||
}
|
||||
|
||||
static func decode(_ data: Data) -> String {
|
||||
// A UTF-8 BOM would otherwise survive into the first property name.
|
||||
var bytes = data
|
||||
if bytes.starts(with: [0xEF, 0xBB, 0xBF]) {
|
||||
bytes = bytes.dropFirst(3)
|
||||
}
|
||||
|
||||
for encoding in [String.Encoding.utf8, .windowsCP1252, .isoLatin1] {
|
||||
if let text = String(data: bytes, encoding: encoding) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return String(decoding: bytes, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
/// One output file produced by sanitizing.
|
||||
public struct SanitizedFile: Sendable, Equatable {
|
||||
/// Base name without extension; the caller allocates the final filename.
|
||||
public var suggestedName: String
|
||||
public var contents: String
|
||||
|
||||
public init(suggestedName: String, contents: String) {
|
||||
self.suggestedName = suggestedName
|
||||
self.contents = contents
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome of sanitizing one input file.
|
||||
public struct FileSanitizationResult: Sendable, Equatable {
|
||||
public var files: [SanitizedFile]
|
||||
public var warnings: [String]
|
||||
|
||||
public init(files: [SanitizedFile], warnings: [String] = []) {
|
||||
self.files = files
|
||||
self.warnings = warnings
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user