Browse Source

add swime, a minimal mimetype parser

cyberta 2 years ago
parent
commit
f22389a226

+ 17 - 1
deltachat-ios.xcodeproj/project.pbxproj

@@ -103,6 +103,8 @@
 		30B2BD02278F1C1900889AA4 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3011E8042787365D00214221 /* KeychainManager.swift */; };
 		30B2BD02278F1C1900889AA4 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3011E8042787365D00214221 /* KeychainManager.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
 		30C2BFFE27032375005505DA /* ChatSearchAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */; };
 		30C2BFFE27032375005505DA /* ChatSearchAccessoryBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */; };
+		30C67BE428D0EB4A0090E162 /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C67BE328D0EB4A0090E162 /* FileType.swift */; };
+		30C67BE728D0EC0E0090E162 /* Swime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C67BE628D0EC0E0090E162 /* Swime.swift */; };
 		30DAF71C275901610073C154 /* SettingsBackgroundSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */; };
 		30DAF71C275901610073C154 /* SettingsBackgroundSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */; };
 		30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348DE24F3F819005C93D1 /* ChatTableView.swift */; };
 		30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348DE24F3F819005C93D1 /* ChatTableView.swift */; };
 		30E348E124F53772005C93D1 /* ImageTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348E024F53772005C93D1 /* ImageTextCell.swift */; };
 		30E348E124F53772005C93D1 /* ImageTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348E024F53772005C93D1 /* ImageTextCell.swift */; };
@@ -380,6 +382,8 @@
 		30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEphemeralMessageController.swift; sourceTree = "<group>"; };
 		30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEphemeralMessageController.swift; sourceTree = "<group>"; };
 		30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateCheckController.swift; sourceTree = "<group>"; };
 		30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateCheckController.swift; sourceTree = "<group>"; };
 		30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSearchAccessoryBar.swift; sourceTree = "<group>"; };
 		30C2BFFD27032375005505DA /* ChatSearchAccessoryBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSearchAccessoryBar.swift; sourceTree = "<group>"; };
+		30C67BE328D0EB4A0090E162 /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
+		30C67BE628D0EC0E0090E162 /* Swime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swime.swift; sourceTree = "<group>"; };
 		30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackgroundSelectionController.swift; sourceTree = "<group>"; };
 		30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackgroundSelectionController.swift; sourceTree = "<group>"; };
 		30E348DE24F3F819005C93D1 /* ChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableView.swift; sourceTree = "<group>"; };
 		30E348DE24F3F819005C93D1 /* ChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableView.swift; sourceTree = "<group>"; };
 		30E348E024F53772005C93D1 /* ImageTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTextCell.swift; sourceTree = "<group>"; };
 		30E348E024F53772005C93D1 /* ImageTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTextCell.swift; sourceTree = "<group>"; };
@@ -705,6 +709,15 @@
 			path = Extensions;
 			path = Extensions;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		30C67BE528D0EBCB0090E162 /* swime */ = {
+			isa = PBXGroup;
+			children = (
+				30C67BE328D0EB4A0090E162 /* FileType.swift */,
+				30C67BE628D0EC0E0090E162 /* Swime.swift */,
+			);
+			path = swime;
+			sourceTree = "<group>";
+		};
 		30E8F2112447285600CE2C90 /* DcShare */ = {
 		30E8F2112447285600CE2C90 /* DcShare */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -971,6 +984,8 @@
 		AE851AC2227C695000ED86F0 /* Helper */ = {
 		AE851AC2227C695000ED86F0 /* Helper */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				30C67BE528D0EBCB0090E162 /* swime */,
+				30238CFE28A5554C00EF14AC /* FileHelper.swift */,
 				3067AAC52667F3FE00525036 /* ImageFormat.swift */,
 				3067AAC52667F3FE00525036 /* ImageFormat.swift */,
 				305702A024C6453700D84EFC /* TypeAlias.swift */,
 				305702A024C6453700D84EFC /* TypeAlias.swift */,
 				AEACE2E21FB32B5C00DCDD78 /* Constants.swift */,
 				AEACE2E21FB32B5C00DCDD78 /* Constants.swift */,
@@ -986,7 +1001,6 @@
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
 				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 				3011E8042787365D00214221 /* KeychainManager.swift */,
 				3011E8042787365D00214221 /* KeychainManager.swift */,
-				30238CFE28A5554C00EF14AC /* FileHelper.swift */,
 				30E83EFC289BF32C0035614C /* ShortcutManager.swift */,
 				30E83EFC289BF32C0035614C /* ShortcutManager.swift */,
 			);
 			);
 			path = Helper;
 			path = Helper;
@@ -1427,6 +1441,7 @@
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
 				AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
 				AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
+				30C67BE428D0EB4A0090E162 /* FileType.swift in Sources */,
 				3080A037277DE30100E74565 /* UITextView+Extensions.swift in Sources */,
 				3080A037277DE30100E74565 /* UITextView+Extensions.swift in Sources */,
 				3080A027277DE12D00E74565 /* InputBarButtonItem.swift in Sources */,
 				3080A027277DE12D00E74565 /* InputBarButtonItem.swift in Sources */,
 				30238CFB28A501C300EF14AC /* WebxdcSelector.swift in Sources */,
 				30238CFB28A501C300EF14AC /* WebxdcSelector.swift in Sources */,
@@ -1467,6 +1482,7 @@
 				303492CB257A814200A523D0 /* DraftArea.swift in Sources */,
 				303492CB257A814200A523D0 /* DraftArea.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
+				30C67BE728D0EC0E0090E162 /* Swime.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
 				30E348E524F6647D005C93D1 /* FileTextCell.swift in Sources */,
 				30E348E524F6647D005C93D1 /* FileTextCell.swift in Sources */,
 				30238CFD28A5028300EF14AC /* WebxdcGridCell.swift in Sources */,
 				30238CFD28A5028300EF14AC /* WebxdcGridCell.swift in Sources */,

+ 765 - 0
deltachat-ios/Helper/swime/FileType.swift

@@ -0,0 +1,765 @@
+import Foundation
+
+// (The MIT License)
+// Copyright (c) 2017 Sendy Halim <sendyhalim93@gmail.com>
+
+
+/// List of type shorthands
+/// with this enum we can check mime type with addition of swift type checker
+/// ```
+/// let swime = Swime(data: data)
+/// swime.type
+/// ```
+public enum FileType {
+  case aac
+  case amr
+  case ar
+  case avi
+  case bmp
+  case bz2
+  case cab
+  case cr2
+  case crx
+  case deb
+  case dmg
+  case eot
+  case epub
+  case exe
+  case flac
+  case flif
+  case flv
+  case gif
+  case gz
+  case ico
+  case jpg
+  case jxr
+  case lz
+  case m4a
+  case m4v
+  case mid
+  case mkv
+  case mov
+  case mp3
+  case mp4
+  case mpg
+  case msi
+  case mxf
+  case nes
+  case ogg
+  case opus
+  case otf
+  case pdf
+  case png
+  case ps
+  case psd
+  case rar
+  case rpm
+  case rtf
+  case sevenZ // 7z, Swift does not let us define enum that starts with a digit
+  case sqlite
+  case swf
+  case tar
+  case tif
+  case ttf
+  case wav
+  case webm
+  case webp
+  case wmv
+  case woff
+  case woff2
+  case xpi
+  case xz
+  case z
+  case zip
+  case heic
+}
+
+public struct MimeType {
+  /// Mime type string representation. For example "application/pdf"
+  public let mime: String
+
+  /// Mime type extension. For example "pdf"
+  public let ext: String
+
+  /// Mime type shorthand representation. For example `.pdf`
+  public let type: FileType
+
+  /// Number of bytes required for `MimeType` to be able to check if the
+  /// given bytes match with its mime type magic number specifications.
+  fileprivate let bytesCount: Int
+
+  /// A function to check if the bytes match the `MimeType` specifications.
+  fileprivate let matches: ([UInt8], Swime) -> Bool
+
+  ///  Check if the given bytes matches with `MimeType`
+  ///  it will check for the `bytes.count` first before delegating the
+  ///  checker function to `matches` property
+  ///
+  ///  - parameter bytes: Bytes represented with `[UInt8]`
+  ///  - parameter swime: Swime instance
+  ///
+  ///  - returns: Bool
+  public func matches(bytes: [UInt8], swime: Swime) -> Bool {
+    return bytes.count >= bytesCount && matches(bytes, swime)
+  }
+
+  /// List of all supported `MimeType`s
+  public static let all: [MimeType] = [
+    MimeType(
+      mime: "audio/aac",
+      ext: "aac",
+      type: .aac,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return bytes[0...1] == [0xFF, 0xF1]
+      }
+    ),
+    MimeType(
+      mime: "image/jpeg",
+      ext: "jpg",
+      type: .jpg,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return bytes[0...2] == [0xFF, 0xD8, 0xFF]
+      }
+    ),
+    MimeType(
+      mime: "image/png",
+      ext: "png",
+      type: .png,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x89, 0x50, 0x4E, 0x47]
+      }
+    ),
+    MimeType(
+      mime: "image/gif",
+      ext: "gif",
+      type: .gif,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return bytes[0...2] == [0x47, 0x49, 0x46]
+      }
+    ),
+    MimeType(
+      mime: "image/webp",
+      ext: "webp",
+      type: .webp,
+      bytesCount: 12,
+      matches: { bytes, _ in
+        return bytes[8...11] == [0x57, 0x45, 0x42, 0x50]
+      }
+    ),
+    MimeType(
+      mime: "image/flif",
+      ext: "flif",
+      type: .flif,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x46, 0x4C, 0x49, 0x46]
+      }
+    ),
+    MimeType(
+      mime: "image/x-canon-cr2",
+      ext: "cr2",
+      type: .cr2,
+      bytesCount: 10,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x49, 0x49, 0x2A, 0x00] || bytes[0...3] == [0x4D, 0x4D, 0x00, 0x2A]) &&
+          (bytes[8...9] == [0x43, 0x52])
+      }
+    ),
+    MimeType(
+      mime: "image/tiff",
+      ext: "tif",
+      type: .tif,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x49, 0x49, 0x2A, 0x00]) ||
+          (bytes[0...3] == [0x4D, 0x4D, 0x00, 0x2A])
+      }
+    ),
+    MimeType(
+      mime: "image/bmp",
+      ext: "bmp",
+      type: .bmp,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return bytes[0...1] == [0x42, 0x4D]
+      }
+    ),
+    MimeType(
+      mime: "image/vnd.ms-photo",
+      ext: "jxr",
+      type: .jxr,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return bytes[0...2] == [0x49, 0x49, 0xBC]
+      }
+    ),
+    MimeType(
+      mime: "image/vnd.adobe.photoshop",
+      ext: "psd",
+      type: .psd,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x38, 0x42, 0x50, 0x53]
+      }
+    ),
+    MimeType(
+      mime: "application/epub+zip",
+      ext: "epub",
+      type: .epub,
+      bytesCount: 58,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x50, 0x4B, 0x03, 0x04]) &&
+          (bytes[30...57] == [
+            0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C,
+            0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x65, 0x70, 0x75, 0x62,
+            0x2B, 0x7A, 0x69, 0x70
+          ])
+      }
+    ),
+
+    // Needs to be before `zip` check
+    // assumes signed .xpi from addons.mozilla.org
+    MimeType(
+      mime: "application/x-xpinstall",
+      ext: "xpi",
+      type: .xpi,
+      bytesCount: 50,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x50, 0x4B, 0x03, 0x04]) &&
+        (bytes[30...49] == [
+          0x4D, 0x45, 0x54, 0x41, 0x2D, 0x49, 0x4E, 0x46, 0x2F, 0x6D, 0x6F, 0x7A,
+          0x69, 0x6C, 0x6C, 0x61, 0x2E, 0x72, 0x73, 0x61
+        ])
+      }
+    ),
+    MimeType(
+      mime: "application/zip",
+      ext: "zip",
+      type: .zip,
+      bytesCount: 50,
+      matches: { bytes, _ in
+        return (bytes[0...1] == [0x50, 0x4B]) &&
+          (bytes[2] == 0x3 || bytes[2] == 0x5 || bytes[2] == 0x7) &&
+          (bytes[3] == 0x4 || bytes[3] == 0x6 || bytes[3] == 0x8)
+      }
+    ),
+    MimeType(
+      mime: "application/x-tar",
+      ext: "tar",
+      type: .tar,
+      bytesCount: 262,
+      matches: { bytes, _ in
+        return bytes[257...261] == [0x75, 0x73, 0x74, 0x61, 0x72]
+      }
+    ),
+    MimeType(
+      mime: "application/x-rar-compressed",
+      ext: "rar",
+      type: .rar,
+      bytesCount: 7,
+      matches: { bytes, _ in
+        return (bytes[0...5] == [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07]) &&
+          (bytes[6] == 0x0 || bytes[6] == 0x1)
+      }
+    ),
+    MimeType(
+      mime: "application/gzip",
+      ext: "gz",
+      type: .gz,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return bytes[0...2] == [0x1F, 0x8B, 0x08]
+      }
+    ),
+    MimeType(
+      mime: "application/x-bzip2",
+      ext: "bz2",
+      type: .bz2,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return bytes[0...2] == [0x42, 0x5A, 0x68]
+      }
+    ),
+    MimeType(
+      mime: "application/x-7z-compressed",
+      ext: "7z",
+      type: .sevenZ,
+      bytesCount: 6,
+      matches: { bytes, _ in
+        return bytes[0...5] == [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]
+      }
+    ),
+    MimeType(
+      mime: "application/x-apple-diskimage",
+      ext: "dmg",
+      type: .dmg,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return bytes[0...1] == [0x78, 0x01]
+      }
+    ),
+    MimeType(
+      mime: "video/mp4",
+      ext: "mp4",
+      type: .mp4,
+      bytesCount: 28,
+      matches: { bytes, _ in
+        return (bytes[0...2] == [0x00, 0x00, 0x00] && (bytes[3] == 0x18 || bytes[3] == 0x20) && bytes[4...7] == [0x66, 0x74, 0x79, 0x70]) ||
+          (bytes[0...3] == [0x33, 0x67, 0x70, 0x35]) ||
+          (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32] &&
+            bytes[16...27] == [0x6D, 0x70, 0x34, 0x31, 0x6D, 0x70, 0x34, 0x32, 0x69, 0x73, 0x6F, 0x6D]) ||
+          (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D]) ||
+          (bytes[0...11] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32])
+      }
+    ),
+    MimeType(
+      mime: "video/x-m4v",
+      ext: "m4v",
+      type: .m4v,
+      bytesCount: 11,
+      matches: { bytes, _ in
+        return bytes[0...10] == [0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x56]
+      }
+    ),
+    MimeType(
+      mime: "audio/midi",
+      ext: "mid",
+      type: .mid,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x4D, 0x54, 0x68, 0x64]
+      }
+    ),
+    MimeType(
+      mime: "video/x-matroska",
+      ext: "mkv",
+      type: .mkv,
+      bytesCount: 4,
+      matches: { bytes, swime in
+        guard bytes[0...3] == [0x1A, 0x45, 0xDF, 0xA3] else {
+          return false
+        }
+
+        let bytes = Array(swime.readBytes(count: 4100)[4 ..< 4100])
+        var idPos = -1
+
+        for i in 0 ..< (bytes.count - 1) {
+          if bytes[i] == 0x42 && bytes[i + 1] == 0x82 {
+            idPos = i
+            break
+          }
+        }
+
+        guard idPos > -1 else {
+          return false
+        }
+
+        let docTypePos = idPos + 3
+        let findDocType: (String) -> Bool = { type in
+          for i in 0 ..< type.count {
+            let index = type.index(type.startIndex, offsetBy: i)
+            let scalars = String(type[index]).unicodeScalars
+
+            if bytes[docTypePos + i] != UInt8(scalars[scalars.startIndex].value) {
+              return false
+            }
+          }
+
+          return true
+        }
+
+        return findDocType("matroska")
+      }
+    ),
+    MimeType(
+      mime: "video/webm",
+      ext: "webm",
+      type: .webm,
+      bytesCount: 4,
+      matches: { bytes, swime in
+        guard bytes[0...3] == [0x1A, 0x45, 0xDF, 0xA3] else {
+          return false
+        }
+
+        let bytes = Array(swime.readBytes(count: 4100)[4 ..< 4100])
+        var idPos = -1
+
+        for i in 0 ..< (bytes.count - 1) {
+          if bytes[i] == 0x42 && bytes[i + 1] == 0x82 {
+            idPos = i
+            break
+          }
+        }
+
+        guard idPos > -1 else {
+          return false
+        }
+
+        let docTypePos = idPos + 3
+        let findDocType: (String) -> Bool = { type in
+          for i in 0 ..< type.count {
+            let index = type.index(type.startIndex, offsetBy: i)
+            let scalars = String(type[index]).unicodeScalars
+
+            if bytes[docTypePos + i] != UInt8(scalars[scalars.startIndex].value) {
+              return false
+            }
+          }
+
+          return true
+        }
+
+        return findDocType("webm")
+      }
+    ),
+    MimeType(
+      mime: "video/quicktime",
+      ext: "mov",
+      type: .mov,
+      bytesCount: 8,
+      matches: { bytes, _ in
+        return bytes[0...7] == [0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70]
+      }
+    ),
+    MimeType(
+      mime: "video/x-msvideo",
+      ext: "avi",
+      type: .avi,
+      bytesCount: 11,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x52, 0x49, 0x46, 0x46]) &&
+          (bytes[8...10] == [0x41, 0x56, 0x49])
+      }
+    ),
+    MimeType(
+      mime: "video/x-ms-wmv",
+      ext: "wmv",
+      type: .wmv,
+      bytesCount: 10,
+      matches: { bytes, _ in
+        return bytes[0...9] == [0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9]
+      }
+    ),
+    MimeType(
+      mime: "video/mpeg",
+      ext: "mpg",
+      type: .mpg,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        guard bytes[0...2] == [0x00, 0x00, 0x01]  else {
+          return false
+        }
+
+        let hexCode = String(format: "%2X", bytes[3])
+
+        return hexCode.first != nil && hexCode.first! == "B"
+      }
+    ),
+    MimeType(
+      mime: "audio/mpeg",
+      ext: "mp3",
+      type: .mp3,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return (bytes[0...2] == [0x49, 0x44, 0x33]) ||
+          (bytes[0...1] == [0xFF, 0xFB])
+      }
+    ),
+    MimeType(
+      mime: "audio/m4a",
+      ext: "m4a",
+      type: .m4a,
+      bytesCount: 11,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x4D, 0x34, 0x41, 0x20]) ||
+          (bytes[4...10] == [0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41])
+      }
+    ),
+
+    // Needs to be before `ogg` check
+    MimeType(
+      mime: "audio/opus",
+      ext: "opus",
+      type: .opus,
+      bytesCount: 36,
+      matches: { bytes, _ in
+        return bytes[28...35] == [0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]
+      }
+    ),
+    MimeType(
+      mime: "audio/ogg",
+      ext: "ogg",
+      type: .ogg,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x4F, 0x67, 0x67, 0x53]
+      }
+    ),
+    MimeType(
+      mime: "audio/x-flac",
+      ext: "flac",
+      type: .flac,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x66, 0x4C, 0x61, 0x43]
+      }
+    ),
+    MimeType(
+      mime: "audio/x-wav",
+      ext: "wav",
+      type: .wav,
+      bytesCount: 12,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x52, 0x49, 0x46, 0x46]) &&
+          (bytes[8...11] == [0x57, 0x41, 0x56, 0x45])
+      }
+    ),
+    MimeType(
+      mime: "audio/amr",
+      ext: "amr",
+      type: .amr,
+      bytesCount: 6,
+      matches: { bytes, _ in
+        return bytes[0...5] == [0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A]
+      }
+    ),
+    MimeType(
+      mime: "application/pdf",
+      ext: "pdf",
+      type: .pdf,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x25, 0x50, 0x44, 0x46]
+      }
+    ),
+    MimeType(
+      mime: "application/x-msdownload",
+      ext: "exe",
+      type: .exe,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return bytes[0...1] == [0x4D, 0x5A]
+      }
+    ),
+    MimeType(
+      mime: "application/x-shockwave-flash",
+      ext: "swf",
+      type: .swf,
+      bytesCount: 3,
+      matches: { bytes, _ in
+        return (bytes[0] == 0x43 || bytes[0] == 0x46) && (bytes[1...2] == [0x57, 0x53])
+      }
+    ),
+    MimeType(
+      mime: "application/rtf",
+      ext: "rtf",
+      type: .rtf,
+      bytesCount: 5,
+      matches: { bytes, _ in
+        return bytes[0...4] == [0x7B, 0x5C, 0x72, 0x74, 0x66]
+      }
+    ),
+    MimeType(
+      mime: "application/font-woff",
+      ext: "woff",
+      type: .woff,
+      bytesCount: 8,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x77, 0x4F, 0x46, 0x46]) &&
+          ((bytes[4...7] == [0x00, 0x01, 0x00, 0x00]) || (bytes[4...7] == [0x4F, 0x54, 0x54, 0x4F]))
+      }
+    ),
+    MimeType(
+      mime: "application/font-woff",
+      ext: "woff2",
+      type: .woff2,
+      bytesCount: 8,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x77, 0x4F, 0x46,  0x32]) &&
+          ((bytes[4...7] == [0x00, 0x01, 0x00, 0x00]) || (bytes[4...7] == [0x4F, 0x54, 0x54, 0x4F]))
+      }
+    ),
+    MimeType(
+      mime: "application/vnd.ms-fontobject",
+      ext: "eot",
+      type: .eot,
+      bytesCount: 82,
+      matches: { bytes, _ in
+        return bytes[34...35] == [0x4c, 0x50] &&
+        Array(bytes[64...79]) == Array(repeating: 0x00, count: 16) &&
+        bytes[82] != 0x00
+      }
+    ),
+    MimeType(
+      mime: "application/font-sfnt",
+      ext: "ttf",
+      type: .ttf,
+      bytesCount: 5,
+      matches: { bytes, _ in
+        return bytes[0...4] == [0x00, 0x01, 0x00, 0x00, 0x00]
+      }
+    ),
+    MimeType(
+      mime: "application/font-sfnt",
+      ext: "otf",
+      type: .otf,
+      bytesCount: 5,
+      matches: { bytes, _ in
+        return bytes[0...4] == [0x4F, 0x54, 0x54, 0x4F, 0x00]
+      }
+    ),
+    MimeType(
+      mime: "image/x-icon",
+      ext: "ico",
+      type: .ico,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x00, 0x00, 0x01, 0x00]
+      }
+    ),
+    MimeType(
+      mime: "video/x-flv",
+      ext: "flv",
+      type: .flv,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x46, 0x4C, 0x56, 0x01]
+      }
+    ),
+    MimeType(
+      mime: "application/postscript",
+      ext: "ps",
+      type: .ps,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return bytes[0...1] == [0x25, 0x21]
+      }
+    ),
+    MimeType(
+      mime: "application/x-xz",
+      ext: "xz",
+      type: .xz,
+      bytesCount: 6,
+      matches: { bytes, _ in
+        return bytes[0...5] == [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]
+      }
+    ),
+    MimeType(
+      mime: "application/x-sqlite3",
+      ext: "sqlite",
+      type: .sqlite,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x53, 0x51, 0x4C, 0x69]
+      }
+    ),
+    MimeType(
+      mime: "application/x-nintendo-nes-rom",
+      ext: "nes",
+      type: .nes,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x4E, 0x45, 0x53, 0x1A]
+      }
+    ),
+    MimeType(
+      mime: "application/x-google-chrome-extension",
+      ext: "crx",
+      type: .crx,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x43, 0x72, 0x32, 0x34]
+      }
+    ),
+    MimeType(
+      mime: "application/vnd.ms-cab-compressed",
+      ext: "cab",
+      type: .cab,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return (bytes[0...3] == [0x4D, 0x53, 0x43, 0x46]) || (bytes[0...3] == [0x49, 0x53, 0x63, 0x28])
+      }
+    ),
+
+    // Needs to be before `ar` check
+    MimeType(
+      mime: "application/x-deb",
+      ext: "deb",
+      type: .deb,
+      bytesCount: 21,
+      matches: { bytes, _ in
+        return bytes[0...20] == [
+          0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A, 0x64, 0x65, 0x62, 0x69,
+          0x61, 0x6E, 0x2D, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79
+        ]
+      }
+    ),
+    MimeType(
+      mime: "application/x-unix-archive",
+      ext: "ar",
+      type: .ar,
+      bytesCount: 7,
+      matches: { bytes, _ in
+        return bytes[0...6] == [0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E]
+      }
+    ),
+    MimeType(
+      mime: "application/x-rpm",
+      ext: "rpm",
+      type: .rpm,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0xED, 0xAB, 0xEE, 0xDB]
+      }
+    ),
+    MimeType(
+      mime: "application/x-compress",
+      ext: "Z",
+      type: .z,
+      bytesCount: 2,
+      matches: { bytes, _ in
+        return (bytes[0...1] == [0x1F, 0xA0]) || (bytes[0...1] == [0x1F, 0x9D])
+      }
+    ),
+    MimeType(
+      mime: "application/x-lzip",
+      ext: "lz",
+      type: .lz,
+      bytesCount: 4,
+      matches: { bytes, _ in
+        return bytes[0...3] == [0x4C, 0x5A, 0x49, 0x50]
+      }
+    ),
+    MimeType(
+      mime: "application/x-msi",
+      ext: "msi",
+      type: .msi,
+      bytesCount: 8,
+      matches: { bytes, _ in
+        return bytes[0...7] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
+      }
+    ),
+    MimeType(
+      mime: "application/mxf",
+      ext: "mxf",
+      type: .mxf,
+      bytesCount: 14,
+      matches: { bytes, _ in
+        return bytes[0...13] == [0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02 ]
+      }
+    ),
+    MimeType(
+        mime: "application/heic",
+        ext: "heic",
+        type: .heic,
+        bytesCount: 12,
+        matches: { bytes, _ in
+            return bytes[8...11] == [0x68, 0x65, 0x69, 0x63] || bytes[8...11] == [0x68, 0x65, 0x69, 0x78]
+        }
+    )
+  ]
+}

+ 59 - 0
deltachat-ios/Helper/swime/Swime.swift

@@ -0,0 +1,59 @@
+import Foundation
+// (The MIT License)
+// Copyright (c) 2017 Sendy Halim <sendyhalim93@gmail.com>
+
+
+public struct Swime {
+  /// File data
+  let data: Data
+
+  ///  A static method to get the `MimeType` that matches the given file data
+  ///
+  ///  - returns: Optional<MimeType>
+  static public func mimeType(data: Data) -> MimeType? {
+    return mimeType(swime: Swime(data: data))
+  }
+
+  ///  A static method to get the `MimeType` that matches the given bytes
+  ///
+  ///  - returns: Optional<MimeType>
+  static public func mimeType(bytes: [UInt8]) -> MimeType? {
+    return mimeType(swime: Swime(bytes: bytes))
+  }
+
+  ///  Get the `MimeType` that matches the given `Swime` instance
+  ///
+  ///  - returns: Optional<MimeType>
+  static public func mimeType(swime: Swime) -> MimeType? {
+    let bytes = swime.readBytes(count: min(swime.data.count, 262))
+
+    for mime in MimeType.all {
+      if mime.matches(bytes: bytes, swime: swime) {
+        return mime
+      }
+    }
+
+    return nil
+  }
+
+  public init(data: Data) {
+    self.data = data
+  }
+
+  public init(bytes: [UInt8]) {
+    self.init(data: Data(bytes))
+  }
+
+  ///  Read bytes from file data
+  ///
+  ///  - parameter count: Number of bytes to be read
+  ///
+  ///  - returns: Bytes represented with `[UInt8]`
+  internal func readBytes(count: Int) -> [UInt8] {
+    var bytes = [UInt8](repeating: 0, count: count)
+
+    data.copyBytes(to: &bytes, count: count)
+
+    return bytes
+  }
+}