Files
mobile-ios/Rosetta/Core/Utils/AttachmentPreviewCodec.swift

177 lines
6.7 KiB
Swift

import Foundation
/// Shared parser/composer for attachment preview fields.
///
/// Cross-platform canonical formats:
/// - image/avatar: `tag::blurhash` or `::blurhash` (placeholder before upload)
/// - file: `tag::size::name` or `size::name`
/// - legacy/local-only: raw preview payload without `tag::` prefix
enum AttachmentPreviewCodec {
// Legacy iOS upload IDs were generated from [a-z0-9] with fixed length 8.
// We intentionally keep this strict to avoid stripping arbitrary prefixes
// from valid blurhash payloads that may contain "::".
private static let legacyTransportIdRegex = try! NSRegularExpression(
pattern: "^[a-z0-9]{8}$",
options: []
)
struct ParsedFilePreview: Equatable {
let downloadTag: String
let fileSize: Int
let fileName: String
let payload: String
}
static func isDownloadTag(_ value: String) -> Bool {
UUID(uuidString: value.trimmingCharacters(in: .whitespacesAndNewlines)) != nil
}
static func downloadTag(from preview: String) -> String {
let firstPart = preview.components(separatedBy: "::").first ?? ""
return isDownloadTag(firstPart) ? firstPart : ""
}
static func payload(from preview: String) -> String {
let parts = preview.components(separatedBy: "::")
guard parts.isEmpty == false else { return "" }
if isDownloadTag(parts[0]) {
return normalizePayload(parts.dropFirst().joined(separator: "::"))
}
// Placeholder preview before upload (`::blurhash` / `::size::name`).
if parts[0].isEmpty {
return normalizePayload(parts.dropFirst().joined(separator: "::"))
}
return normalizePayload(preview)
}
static func blurHash(from preview: String) -> String {
let raw = payload(from: preview)
// Legacy compatibility: older iOS builds could embed an 8-char upload
// id in preview, e.g. "jbov1nac::blurhash". We strip only this known
// prefix shape to avoid corrupting valid blurhash values that may
// legitimately contain "::".
var candidate = raw
if let sep = raw.range(of: "::") {
let prefix = String(raw[..<sep.lowerBound])
let suffix = String(raw[sep.upperBound...])
if isLegacyTransportId(prefix), !suffix.isEmpty {
candidate = suffix
}
}
// Strip trailing "|WxH" dimension suffix if present.
if let pipeIdx = candidate.lastIndex(of: "|") {
return String(candidate[candidate.startIndex..<pipeIdx])
}
return candidate
}
/// Extract pixel dimensions encoded as `|WxH` suffix in image preview.
/// Format: "tag::blurhash|WxH" "|" separator avoids breaking desktop's `::` parsing.
/// Returns nil if no dimensions found (legacy messages).
static func imageDimensions(from preview: String) -> CGSize? {
let raw = payload(from: preview)
guard let pipeIdx = raw.lastIndex(of: "|") else { return nil }
let dimStr = raw[raw.index(after: pipeIdx)...]
guard let xIdx = dimStr.firstIndex(of: "x"),
let w = Int(dimStr[dimStr.startIndex..<xIdx]),
let h = Int(dimStr[dimStr.index(after: xIdx)...]),
w >= 10, h >= 10 else { return nil }
return CGSize(width: CGFloat(w), height: CGFloat(h))
}
static func parseFilePreview(
_ preview: String,
fallbackFileName: String = "file",
fallbackFileSize: Int = 0
) -> ParsedFilePreview {
let tag = downloadTag(from: preview)
let normalizedPayload = payload(from: preview)
let components = normalizedPayload.components(separatedBy: "::")
var fileSize = max(fallbackFileSize, 0)
var fileName = fallbackFileName
if components.count >= 2, let parsedSize = Int(components[0]) {
fileSize = max(parsedSize, 0)
let joinedName = components.dropFirst().joined(separator: "::")
if joinedName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
fileName = joinedName
}
} else if components.count >= 2 {
// Legacy payload without explicit size.
let joinedName = components.joined(separator: "::")
if joinedName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
fileName = joinedName
}
} else if let onlyComponent = components.first,
onlyComponent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
fileName = onlyComponent
}
return ParsedFilePreview(
downloadTag: tag,
fileSize: fileSize,
fileName: fileName,
payload: normalizedPayload
)
}
static func compose(downloadTag: String, payload: String) -> String {
let tag = downloadTag.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedPayload = normalizePayload(payload)
if tag.isEmpty { return normalizedPayload }
return "\(tag)::\(normalizedPayload)"
}
static func parseCallDurationSeconds(_ preview: String) -> Int {
let normalized = payload(from: preview)
.trimmingCharacters(in: .whitespacesAndNewlines)
if let direct = Int(normalized) {
return max(direct, 0)
}
let patterns = [
#"duration(?:Sec|Seconds)?\s*[:=]\s*(\d+)"#,
#"duration\s+(\d+)"#,
]
for pattern in patterns {
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
continue
}
let nsRange = NSRange(normalized.startIndex..<normalized.endIndex, in: normalized)
guard let match = regex.firstMatch(in: normalized, options: [], range: nsRange),
match.numberOfRanges > 1,
let valueRange = Range(match.range(at: 1), in: normalized),
let value = Int(normalized[valueRange])
else {
continue
}
return max(value, 0)
}
return 0
}
private static func normalizePayload(_ payload: String) -> String {
var value = payload.trimmingCharacters(in: .whitespacesAndNewlines)
while value.hasPrefix("::") {
value.removeFirst(2)
}
return value
}
private static func isLegacyTransportId(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
let range = NSRange(trimmed.startIndex..<trimmed.endIndex, in: trimmed)
return legacyTransportIdRegex.firstMatch(in: trimmed, options: [], range: range) != nil
}
}