import CryptoKit import Foundation import XCTest @testable import VelodyPersistence final class MP3EmbeddedArtworkExtractorTests: XCTestCase { func testExtractorReadsEmbeddedPNGArtworkFromID3APICFrame() throws { let fileManager = FileManager.default let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true ) let fileURL = tempDirectory.appendingPathComponent("artwork-test.mp3") let extractor = MP3EmbeddedArtworkExtractor() let artworkData = sampleArtworkData() defer { try? fileManager.removeItem(at: tempDirectory) } try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true) try makeMP3FileWithEmbeddedArtwork( artworkData: artworkData, mimeType: "image/png", at: fileURL ) let extractedArtwork = try XCTUnwrap( extractor.extractArtwork(from: fileURL) ) XCTAssertEqual(extractedArtwork.data, artworkData) XCTAssertEqual(extractedArtwork.mimeType, "image/png") XCTAssertEqual(extractedArtwork.width, 1) XCTAssertEqual(extractedArtwork.height, 1) XCTAssertEqual(extractedArtwork.sha256, sha256Hex(artworkData)) } func testExtractorReturnsNilWhenMP3HasNoEmbeddedArtwork() throws { let fileManager = FileManager.default let tempDirectory = fileManager.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true ) let fileURL = tempDirectory.appendingPathComponent("no-artwork.mp3") let extractor = MP3EmbeddedArtworkExtractor() defer { try? fileManager.removeItem(at: tempDirectory) } try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true) try Data([ 0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]).write(to: fileURL) XCTAssertNil(try extractor.extractArtwork(from: fileURL)) } } private func makeMP3FileWithEmbeddedArtwork( artworkData: Data, mimeType: String, at fileURL: URL ) throws { let mimeTypeData = Data(mimeType.utf8) + Data([0x00]) let frameBody = Data([0x00]) // ISO-8859-1 encoding + mimeTypeData + Data([0x03]) // front cover + Data([0x00]) // empty description + artworkData let frameSize = UInt32(frameBody.count) let frameHeader = Data("APIC".utf8) + Data([ UInt8((frameSize >> 24) & 0xff), UInt8((frameSize >> 16) & 0xff), UInt8((frameSize >> 8) & 0xff), UInt8(frameSize & 0xff), 0x00, 0x00, ]) let tagBody = frameHeader + frameBody let tagSize = synchsafeBytes(for: tagBody.count) let id3Header = Data([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]) + tagSize let mp3Bytes = id3Header + tagBody + Data([ 0xff, 0xfb, 0x90, 0x64, 0x00, 0x00, 0x00, 0x00, ]) try mp3Bytes.write(to: fileURL) } private func synchsafeBytes(for value: Int) -> Data { Data([ UInt8((value >> 21) & 0x7f), UInt8((value >> 14) & 0x7f), UInt8((value >> 7) & 0x7f), UInt8(value & 0x7f), ]) } private func sampleArtworkData() -> Data { Data( base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg==" )! } private func sha256Hex(_ data: Data) -> String { SHA256.hash(data: data) .map { String(format: "%02x", $0) } .joined() }