Swift中如何校验文件为真实有效MP3而非仅匹配.mp3后缀
MP3文件合法性校验实现方案
原有方案缺陷
当前使用的校验逻辑仅通过文件后缀匹配UTType规则,完全未校验文件本身的二进制内容,只要将任意文件后缀修改为.mp3就会被误判,无法识别伪造后缀的非MP3文件。
校验原理
不需要解码或播放音频,仅通过MP3格式固定的二进制文件头特征即可完成校验,完全匹配「仅确认文件为合法MP3格式、不校验可播放性」的需求:
- 带ID3v2元数据标签的MP3:文件起始3个字节固定为ASCII字符
ID3,对应十六进制值0x49 0x44 0x33,是目前绝大多数MP3文件的头特征 - 不带ID3v2标签的裸MP3帧:文件起始为MPEG音频帧同步字,第一个字节固定为
0xFF,第二个字节高3位固定为111(即第二个字节与0xE0做位与运算结果等于0xE0)
校验时仅需要读取文件前10个字节即可完成判断,不需要加载整个文件,性能开销极低。
修正后代码
替换原有的isValidMP3函数,同时优化目录遍历逻辑,修复沙盒环境下的文件访问权限问题、路径拼接不规范问题:
import SwiftUI import UniformTypeIdentifiers struct ContentView: View { @State private var fileImporterIsPresented: Bool = false var body: some View { Button("Select your Folder") { fileImporterIsPresented = true } .fileImporter(isPresented: $fileImporterIsPresented, allowedContentTypes: [.folder], allowsMultipleSelection: false, onCompletion: { result in switch result { case .success(let urls): if let folderURL = urls.first { // 沙盒环境下必须先获取目录访问权限 guard folderURL.startAccessingSecurityScopedResource() else { print("No permission to access folder") return } defer { folderURL.stopAccessingSecurityScopedResource() } if let contents = try? FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil) { contents.forEach { fileURL in if isValidMP3(filePath: fileURL.path) { print(fileURL.lastPathComponent) } } } } case .failure(let error): print("Error selecting file \(error.localizedDescription)") } }) } } func isValidMP3(filePath: String) -> Bool { let fileURL = URL(fileURLWithPath: filePath) // 过滤目录,仅校验普通文件 var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory), !isDirectory.boolValue else { return false } // 后缀预过滤,减少不必要的文件IO操作 guard let utType = UTType(filenameExtension: fileURL.pathExtension), utType == .mp3 else { return false } // 读取文件前10字节校验头特征 guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else { return false } defer { try? fileHandle.close() } let headerData = fileHandle.readData(ofLength: 10) guard headerData.count >= 3 else { return false } let headerBytes = [UInt8](headerData) // 匹配ID3v2头 if headerBytes[0] == 0x49, headerBytes[1] == 0x44, headerBytes[2] == 0x33 { return true } // 匹配裸MPEG帧同步字 if headerBytes[0] == 0xFF, (headerBytes[1] & 0xE0) == 0xE0 { return true } return false }
补充说明
- 校验逻辑不会判断MP3文件是否损坏、是否能正常播放,仅确认文件符合MP3格式的二进制结构要求
- 后缀预过滤逻辑可以大幅减少需要读取文件的数量,遍历大文件夹时性能更好
- 新增的安全作用域访问逻辑,解决了macOS/iOS沙盒环境下选中文件夹后无法读取内部文件的隐藏问题
内容的提问来源于stack exchange,提问作者swiftPunk
相关产品推荐
相关产品推荐

