9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
200 lines
6.1 KiB
Swift
200 lines
6.1 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
private enum MediaSanitizationError: Error {
|
|
case invalidPng
|
|
case invalidJpeg
|
|
}
|
|
|
|
enum MediaSanitizer {
|
|
private static let pngSignature = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
|
|
private static let allowedPngAncillaryChunks: Set<String> = [
|
|
"cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT",
|
|
]
|
|
|
|
static func sanitizeImage(_ image: UIImage, mimeType: String) throws -> Data? {
|
|
switch mimeType {
|
|
case "image/png":
|
|
guard let image = renderInSRGB(image), let encoded = image.pngData() else { return nil }
|
|
return try scrubPng(encoded)
|
|
case "image/jpeg":
|
|
return try encodeJpeg(image)
|
|
case "image/webp":
|
|
guard let image = renderInSRGB(image), let encoded = image.pngData() else { return nil }
|
|
return try scrubPng(encoded)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func encodeJpeg(_ image: UIImage) throws -> Data? {
|
|
guard
|
|
let image = renderInSRGB(image),
|
|
let encoded = image.jpegData(compressionQuality: 1.0)
|
|
else {
|
|
return nil
|
|
}
|
|
return try scrubJpeg(encoded)
|
|
}
|
|
|
|
private static func renderInSRGB(_ image: UIImage) -> UIImage? {
|
|
guard image.size.width > 0, image.size.height > 0 else { return nil }
|
|
let format = UIGraphicsImageRendererFormat()
|
|
format.scale = image.scale
|
|
format.opaque = false
|
|
format.preferredRange = .standard
|
|
return UIGraphicsImageRenderer(size: image.size, format: format).image { _ in
|
|
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
}
|
|
}
|
|
|
|
static func scrubPng(_ data: Data) throws -> Data {
|
|
let data = Data(data)
|
|
guard data.count >= pngSignature.count, data.prefix(pngSignature.count) == pngSignature else {
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
|
|
var output = pngSignature
|
|
var offset = pngSignature.count
|
|
while offset < data.count {
|
|
guard data.count - offset >= 12 else {
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
|
|
let payloadLengthValue = try readUInt32BigEndian(data, at: offset)
|
|
guard
|
|
let payloadLength = Int(exactly: payloadLengthValue),
|
|
payloadLength <= data.count - offset - 12
|
|
else {
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
let chunkLength = payloadLength + 12
|
|
let typeStart = offset + 4
|
|
let typeEnd = typeStart + 4
|
|
let typeBytes = data[typeStart..<typeEnd]
|
|
guard let type = String(bytes: typeBytes, encoding: .ascii) else {
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
|
|
let isAncillary = typeBytes[typeBytes.startIndex] & 0x20 != 0
|
|
if !isAncillary || allowedPngAncillaryChunks.contains(type) {
|
|
output.append(data[offset..<(offset + chunkLength)])
|
|
}
|
|
|
|
offset += chunkLength
|
|
if type == "IEND" {
|
|
return output
|
|
}
|
|
}
|
|
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
|
|
static func scrubJpeg(_ data: Data) throws -> Data {
|
|
let data = Data(data)
|
|
guard data.count >= 2, data[0] == 0xFF, data[1] == 0xD8 else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
|
|
var output = Data([0xFF, 0xD8])
|
|
var offset = 2
|
|
var inScan = false
|
|
while offset < data.count {
|
|
if inScan, data[offset] != 0xFF {
|
|
let nextMarker = data[offset...].firstIndex(of: 0xFF) ?? data.endIndex
|
|
output.append(data[offset..<nextMarker])
|
|
offset = nextMarker
|
|
continue
|
|
}
|
|
guard data[offset] == 0xFF else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
|
|
let markerStart = offset
|
|
while offset < data.count, data[offset] == 0xFF {
|
|
offset += 1
|
|
}
|
|
guard offset < data.count else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
|
|
let marker = data[offset]
|
|
offset += 1
|
|
if inScan, marker == 0x00 {
|
|
output.append(data[markerStart..<offset])
|
|
continue
|
|
}
|
|
if (0xD0...0xD7).contains(marker) || marker == 0x01 {
|
|
output.append(data[markerStart..<offset])
|
|
continue
|
|
}
|
|
if marker == 0xD9 {
|
|
output.append(data[markerStart..<offset])
|
|
return output
|
|
}
|
|
guard marker != 0xD8, data.count - offset >= 2 else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
|
|
let segmentLength = try readUInt16BigEndian(data, at: offset)
|
|
guard segmentLength >= 2, Int(segmentLength) <= data.count - offset else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
let segmentEnd = offset + Int(segmentLength)
|
|
if shouldKeepJpegSegment(marker, data: data, payload: (offset + 2)..<segmentEnd) {
|
|
output.append(data[markerStart..<segmentEnd])
|
|
}
|
|
|
|
offset = segmentEnd
|
|
inScan = marker == 0xDA
|
|
}
|
|
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
|
|
private static func shouldKeepJpegSegment(
|
|
_ marker: UInt8,
|
|
data: Data,
|
|
payload: Range<Int>
|
|
) -> Bool {
|
|
switch marker {
|
|
case 0xE0:
|
|
guard
|
|
payload.count >= 14,
|
|
data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
|
0x4A, 0x46, 0x49, 0x46, 0x00,
|
|
])
|
|
else {
|
|
return false
|
|
}
|
|
let thumbnailWidth = Int(data[payload.lowerBound + 12])
|
|
let thumbnailHeight = Int(data[payload.lowerBound + 13])
|
|
return payload.count == 14 + 3 * thumbnailWidth * thumbnailHeight
|
|
case 0xEE:
|
|
return payload.count == 12
|
|
&& data[payload.lowerBound..<(payload.lowerBound + 5)].elementsEqual([
|
|
0x41, 0x64, 0x6F, 0x62, 0x65,
|
|
])
|
|
case 0xE1...0xED, 0xEF, 0xFE:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
private static func readUInt16BigEndian(_ data: Data, at offset: Int) throws -> UInt16 {
|
|
guard data.count - offset >= 2 else {
|
|
throw MediaSanitizationError.invalidJpeg
|
|
}
|
|
return UInt16(data[offset]) << 8 | UInt16(data[offset + 1])
|
|
}
|
|
|
|
private static func readUInt32BigEndian(_ data: Data, at offset: Int) throws -> UInt32 {
|
|
guard data.count - offset >= 4 else {
|
|
throw MediaSanitizationError.invalidPng
|
|
}
|
|
return UInt32(data[offset]) << 24 | UInt32(data[offset + 1]) << 16
|
|
| UInt32(data[offset + 2]) << 8 | UInt32(data[offset + 3])
|
|
}
|
|
}
|