|
| 1 | +import Foundation |
| 2 | + |
| 3 | +/// Inserts metadata into executable files. |
| 4 | +/// |
| 5 | +/// Swift Bundler inserts metadata at the end of your main executable file |
| 6 | +/// after compilation. The format is intended to be simple and portable to |
| 7 | +/// ensure that even if someone isn't using the Swift Bundler runtime they |
| 8 | +/// can easily parse the metadata at runtime. If the metadata format ever |
| 9 | +/// gets extended, it will be extended in such a way that current metadata |
| 10 | +/// remains valid, and future metadata is backwards compatible. |
| 11 | +enum MetadataInserter { |
| 12 | + /// If an executable ends with this string, it probably contains Swift |
| 13 | + /// Bundler metadata. |
| 14 | + static let magicBytes: [UInt8] = Array("SBUNMETA".utf8) |
| 15 | + |
| 16 | + /// Metadata appended to the end of executable files built with Swift |
| 17 | + /// Bundler. |
| 18 | + struct Metadata: Codable { |
| 19 | + /// The app's identifier. |
| 20 | + var appIdentifier: String |
| 21 | + /// The app's version. |
| 22 | + var appVersion: String |
| 23 | + } |
| 24 | + |
| 25 | + /// Generates an app's metadata from its configuration. |
| 26 | + static func metadata(for configuration: AppConfiguration) -> Metadata { |
| 27 | + Metadata( |
| 28 | + appIdentifier: configuration.identifier, |
| 29 | + appVersion: configuration.version |
| 30 | + ) |
| 31 | + } |
| 32 | + |
| 33 | + /// Inserts metadata at the end of the given executable file. |
| 34 | + static func insert( |
| 35 | + _ metadata: Metadata, |
| 36 | + into executableFile: URL |
| 37 | + ) -> Result<(), MetadataInserterError> { |
| 38 | + Data.read(from: executableFile) |
| 39 | + .mapError { error in |
| 40 | + .failedToReadExecutableFile(executableFile, error) |
| 41 | + } |
| 42 | + .andThen { data in |
| 43 | + JSONEncoder().encode(metadata) |
| 44 | + .mapError { error in |
| 45 | + .failedToEncodeMetadata(error) |
| 46 | + } |
| 47 | + .map { encodedMetadata in |
| 48 | + var data = data |
| 49 | + |
| 50 | + // A tag representing the type of the next metadata entry. For now |
| 51 | + // it's just '0' meaning 'end'. This is purely to allow for the |
| 52 | + // format to be extended in the future (e.g. to include resources). |
| 53 | + // We could of course just keep adding more entries to the JSON, |
| 54 | + // but certain types of data just don't work well with JSON (e.g. |
| 55 | + // large amounts of binary data). |
| 56 | + // |
| 57 | + // For forwards compatibility, do NOT require this value to be one |
| 58 | + // you support. Simply stop parsing if you don't understand it. |
| 59 | + writeBigEndianUInt64(0, to: &data) |
| 60 | + |
| 61 | + // The default JSON metadata entry. For now this is the only type |
| 62 | + // of metadata entry. It would be suffixed with a tag type, however |
| 63 | + // this is guaranteed to always be the first entry (for backwards |
| 64 | + // compatibility), and I want to bake that into the format. |
| 65 | + data.append(contentsOf: encodedMetadata) |
| 66 | + // The data's length in bytes. |
| 67 | + writeBigEndianUInt64(UInt64(encodedMetadata.count), to: &data) |
| 68 | + |
| 69 | + // Magic bytes so that apps (and external tools) can know if they |
| 70 | + // contain any Swift Bundler metadata. Since it's technically |
| 71 | + // possible for false positives to occur, apps and tools should |
| 72 | + // always fail safely if the metadata is malformed. |
| 73 | + data.append(contentsOf: magicBytes) |
| 74 | + |
| 75 | + return data |
| 76 | + } |
| 77 | + } |
| 78 | + .andThen { (modifiedData: Data) in |
| 79 | + modifiedData.write(to: executableFile) |
| 80 | + .mapError { error in |
| 81 | + .failedToWriteExecutableFile(executableFile, error) |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + /// Writes a single UInt64 value to the end of a data buffer (in big endian |
| 87 | + /// order). |
| 88 | + private static func writeBigEndianUInt64(_ value: UInt64, to data: inout Data) { |
| 89 | + let count = MemoryLayout<UInt64>.size |
| 90 | + withUnsafePointer(to: value.bigEndian) { pointer in |
| 91 | + pointer.withMemoryRebound(to: UInt8.self, capacity: count) { pointer in |
| 92 | + data.append(pointer, count: count) |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | +} |
0 commit comments