ios_client

This commit is contained in:
Filip
2026-03-14 12:43:56 +01:00
parent 5fd80e6dd6
commit 214da18779
74 changed files with 13136 additions and 284 deletions

View File

@@ -0,0 +1,168 @@
import Foundation
import CryptoKit
// MARK: - Data Hex
extension Data {
/// Convert data to lowercase hex string
var hexString: String {
map { String(format: "%02x", $0) }.joined()
}
/// Initialize Data from a hex string
init?(hexString: String) {
let hex = hexString.lowercased()
guard hex.count % 2 == 0 else { return nil }
var data = Data(capacity: hex.count / 2)
var index = hex.startIndex
while index < hex.endIndex {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(hex[index..<nextIndex], radix: 16) else { return nil }
data.append(byte)
index = nextIndex
}
self = data
}
/// Generate random bytes
static func randomBytes(_ count: Int) -> Data {
var data = Data(count: count)
data.withUnsafeMutableBytes { ptr in
_ = SecRandomCopyBytes(kSecRandomDefault, count, ptr.baseAddress!)
}
return data
}
}
// MARK: - Data Base64 (Protocol Wire Format)
extension Data {
/// Encode to standard base64 string (matching Python's base64.b64encode)
func base64EncodedString() -> String {
self.base64EncodedString(options: [])
}
/// Decode from base64 string
static func fromBase64(_ string: String) throws -> Data {
// Try standard base64 first, then URL-safe
if let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters) {
return data
}
throw CryptoError.invalidBase64
}
}
// MARK: - UInt32 Big-Endian
extension UInt32 {
var bigEndianData: Data {
var value = self.bigEndian
return Data(bytes: &value, count: 4)
}
}
// MARK: - CryptoKit Key Data
extension Curve25519.KeyAgreement.PublicKey {
nonisolated var rawData: Data {
Data(rawRepresentation)
}
}
extension Curve25519.KeyAgreement.PrivateKey {
nonisolated var rawData: Data {
Data(rawRepresentation)
}
}
extension Curve25519.Signing.PublicKey {
nonisolated var rawData: Data {
Data(rawRepresentation)
}
}
extension Curve25519.Signing.PrivateKey {
nonisolated var rawData: Data {
Data(rawRepresentation)
}
}
// MARK: - String helpers
extension String {
/// Trim whitespace and newlines
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}
// MARK: - Date Parsing (server sends ISO8601 with or without timezone)
enum DateParsing {
private static let iso8601WithTZ: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private static let iso8601Basic: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f
}()
private static let noTZ: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
f.timeZone = TimeZone(identifier: "UTC")
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
/// Parse ISO8601 date string handles with/without timezone, with/without fractional seconds
static func parse(_ string: String) -> Date? {
iso8601WithTZ.date(from: string)
?? iso8601Basic.date(from: string)
?? noTZ.date(from: string)
}
/// Format Date to ISO8601 string (for after_ts / since_ts parameters)
static func format(_ date: Date) -> String {
iso8601WithTZ.string(from: date)
}
}
// MARK: - Dictionary merge helper
extension Dictionary where Key == String, Value == Any {
nonisolated func string(for key: String) -> String? {
self[key] as? String
}
nonisolated func int(for key: String) -> Int? {
if let i = self[key] as? Int { return i }
if let s = self[key] as? String, let i = Int(s) { return i }
return nil
}
nonisolated func dict(for key: String) -> [String: Any]? {
self[key] as? [String: Any]
}
nonisolated func array(for key: String) -> [[String: Any]]? {
self[key] as? [[String: Any]]
}
nonisolated func data(for key: String) -> Data? {
if let hex = self[key] as? String {
return Data(hexString: hex)
}
return nil
}
nonisolated func bool(for key: String) -> Bool? {
if let b = self[key] as? Bool { return b }
if let i = self[key] as? Int { return i != 0 }
return nil
}
}