Files

156 lines
6.2 KiB
Swift

import Foundation
/// 白名单订阅资料,不含服务器连接字段、凭据或原始响应。
struct SubscriptionInformation: Codable, Equatable, Sendable {
var bytesUsed: UInt64?
var bytesRemaining: UInt64?
var expiresAt: Date?
var trafficResetAt: Date?
var isEmpty: Bool {
bytesUsed == nil && bytesRemaining == nil && expiresAt == nil && trafficResetAt == nil
}
/// 与服务器及分组扩展的解码分别处理;无效附加资料只影响该项。
static func parse(_ data: Data) -> Self {
let root = objectFields(data, names: ["bytes_used", "bytes_remaining", "x_shadowsocksx_ng"])
let extensionFields = objectFields(
root["x_shadowsocksx_ng"] ?? Data(),
names: ["schema_version", "expires_at", "traffic_reset_at"])
let schemaVersion = extensionFields["schema_version"].flatMap {
try? JSONDecoder().decode(Int.self, from: $0)
}
return Self(
bytesUsed: byteCount(root["bytes_used"]),
bytesRemaining: byteCount(root["bytes_remaining"]),
expiresAt: schemaVersion == 1 ? date(extensionFields["expires_at"]) : nil,
trafficResetAt: schemaVersion == 1 ? date(extensionFields["traffic_reset_at"]) : nil)
}
private static func byteCount(_ value: Data?) -> UInt64? {
guard let value, let raw = String(bytes: value, encoding: .utf8) else { return nil }
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, text.utf8.allSatisfy({ (48...57).contains($0) }) else { return nil }
return UInt64(text)
}
private static func date(_ value: Data?) -> Date? {
guard let value, let text = try? JSONDecoder().decode(String.self, from: value) else {
return nil
}
let normalized = text.uppercased()
let pattern =
#"\A[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T"#
+ #"([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?"#
+ #"(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])\z"#
guard normalized.range(of: pattern, options: .regularExpression) != nil else { return nil }
// RFC 3339 的偏移可达 23:59,超出 Foundation 时区解析器的 18 小时范围。
// 先解析原地时间,再应用数值偏移;不创建会失败的 TimeZone。
let localText = normalized.hasSuffix("Z") ? normalized : String(normalized.dropLast(6)) + "Z"
guard
let local = try? Date.ISO8601FormatStyle(
includingFractionalSeconds: normalized.contains(".")
).parse(localText)
else { return nil }
let parsed = local.addingTimeInterval(-Double(offsetSeconds(normalized)))
// Foundation 会把 2 月 30 日等无效日期向后归一化;按原偏移回读以拒绝这些值。
let parts = normalized.prefix(19).split(whereSeparator: { "-T:".contains($0) }).compactMap {
Int($0)
}
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .gmt
let isLeapSecond = parts[5] == 60
let checked = isLeapSecond ? local.addingTimeInterval(-1) : local
let fields: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components = calendar.dateComponents(fields, from: checked)
guard components.year == parts[0], components.month == parts[1],
components.day == parts[2], components.hour == parts[3], components.minute == parts[4],
components.second == (isLeapSecond ? 59 : parts[5])
else { return nil }
if isLeapSecond {
let utc = calendar.dateComponents(fields, from: parsed.addingTimeInterval(-1))
guard utc.hour == 23, utc.minute == 59,
(utc.month == 6 && utc.day == 30) || (utc.month == 12 && utc.day == 31)
else { return nil }
}
return parsed
}
private static func offsetSeconds(_ text: String) -> Int {
guard !text.hasSuffix("Z") else { return 0 }
let offset = text.suffix(6)
let hours = Int(offset.dropFirst().prefix(2))!
let minutes = Int(offset.suffix(2))!
return (hours * 60 + minutes) * 60 * (offset.first == "-" ? -1 : 1)
}
/// JSON 结构已由订阅文档解析器校验。这里只切出对象直接成员中白名单值的原始片段,
/// 不把整个正文转换成 Any;某项超大数字因此不会损坏其他项的解码。
private static func objectFields(_ data: Data, names: Set<String>) -> [String: Data] {
let bytes = Array(data)
var result: [String: Data] = [:]
// 与现有 JSONDecoder 一致,接受可选的 UTF-8 BOM。
var cursor = bytes.starts(with: [0xEF, 0xBB, 0xBF]) ? 3 : 0
skipWhitespace(bytes, cursor: &cursor)
guard cursor < bytes.count, bytes[cursor] == 123 else { return result }
cursor += 1
while cursor < bytes.count {
skipWhitespace(bytes, cursor: &cursor)
guard cursor < bytes.count, bytes[cursor] == 34 else { break }
let keyStart = cursor
skipString(bytes, cursor: &cursor)
let key = try? JSONDecoder().decode(String.self, from: Data(bytes[keyStart..<cursor]))
skipWhitespace(bytes, cursor: &cursor)
guard cursor < bytes.count, bytes[cursor] == 58 else { break }
cursor += 1
let valueStart = cursor
skipValue(bytes, cursor: &cursor)
if let key, names.contains(key) {
result[key] = Data(bytes[valueStart..<cursor])
}
guard cursor < bytes.count, bytes[cursor] == 44 else { break }
cursor += 1
}
return result
}
private static func skipValue(_ bytes: [UInt8], cursor: inout Int) {
var depth = 0
while cursor < bytes.count {
switch bytes[cursor] {
case 34:
skipString(bytes, cursor: &cursor)
continue
case 123, 91:
depth += 1
case 125, 93:
guard depth > 0 else { return }
depth -= 1
case 44:
if depth == 0 { return }
default:
break
}
cursor += 1
}
}
private static func skipWhitespace(_ bytes: [UInt8], cursor: inout Int) {
while cursor < bytes.count, [9, 10, 13, 32].contains(bytes[cursor]) { cursor += 1 }
}
private static func skipString(_ bytes: [UInt8], cursor: inout Int) {
cursor += 1
while cursor < bytes.count {
if bytes[cursor] == 92 {
cursor = min(cursor + 2, bytes.count)
} else if bytes[cursor] == 34 {
cursor += 1
return
} else {
cursor += 1
}
}
}
}