├── .gitignore ├── Tests ├── LinuxMain.swift └── HexHexHexTests │ ├── RecordTests.swift │ └── HEXParserTests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .github └── workflows │ ├── linux-ci.yml │ └── macos-ci.yml ├── Package.swift ├── LICENSE.txt ├── Sources └── HexHexHex │ ├── Address.swift │ ├── IntegerToString.swift │ ├── HEXFile.swift │ ├── Record.swift │ └── HEXParser.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Run the tests with `swift test --enable-test-discovery`.") 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/linux-ci.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | on: [push] 3 | 4 | jobs: 5 | linux: 6 | name: Linux 7 | runs-on: ubuntu-18.04 8 | container: swift:5.1 9 | steps: 10 | - uses: actions/checkout@v1 11 | - run: swift test --enable-test-discovery 12 | -------------------------------------------------------------------------------- /.github/workflows/macos-ci.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | on: [push] 3 | 4 | jobs: 5 | macos: 6 | name: macOS 7 | # I'd like to specify macOS-10.15 explicitly, but GitHub only 8 | # supports macOS-latest. 9 | runs-on: macOS-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - run: swift test 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "HexHexHex", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "HexHexHex", 12 | targets: ["HexHexHex"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "HexHexHex", 23 | dependencies: []), 24 | .testTarget( 25 | name: "HexHexHexTests", 26 | dependencies: ["HexHexHex"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ole Begemann. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/HexHexHex/Address.swift: -------------------------------------------------------------------------------- 1 | /// A 16-bit address. 2 | public struct Address16: RawRepresentable, Hashable, Codable { 3 | public var rawValue: UInt16 4 | 5 | public init(rawValue: UInt16) { 6 | self.rawValue = rawValue 7 | } 8 | } 9 | 10 | extension Address16: ExpressibleByIntegerLiteral { 11 | public init(integerLiteral literal: UInt16) { 12 | self.rawValue = literal 13 | } 14 | } 15 | 16 | extension Address16: CustomStringConvertible, CustomDebugStringConvertible { 17 | public var description: String { 18 | "0x\(rawValue.hex(padTo: 2, uppercase: true))" 19 | } 20 | 21 | public var debugDescription: String { 22 | "" 23 | } 24 | } 25 | 26 | /// A 32-bit address. 27 | public struct Address32: RawRepresentable, Hashable, Codable { 28 | public var rawValue: UInt32 29 | 30 | public init(rawValue: UInt32) { 31 | self.rawValue = rawValue 32 | } 33 | } 34 | 35 | extension Address32: ExpressibleByIntegerLiteral { 36 | public init(integerLiteral literal: UInt32) { 37 | self.rawValue = literal 38 | } 39 | } 40 | 41 | extension Address32: CustomStringConvertible, CustomDebugStringConvertible { 42 | public var description: String { 43 | "0x\(rawValue.hex(padTo: 4, uppercase: true))" 44 | } 45 | 46 | public var debugDescription: String { 47 | "" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/HexHexHexTests/RecordTests.swift: -------------------------------------------------------------------------------- 1 | import HexHexHex 2 | import XCTest 3 | 4 | final class RecordTests: XCTestCase { 5 | func testDataDescription() { 6 | let record = Record.data(0xabcd, [0x12, 0xef, 0xc7]) 7 | XCTAssertEqual(String(describing: record), "00 data – address: ABCD, data: 12 EF C7") 8 | } 9 | 10 | func testEndOfFileDescription() { 11 | let record = Record.endOfFile 12 | XCTAssertEqual(String(describing: record), "01 end of file") 13 | } 14 | 15 | func testExtendedSegmentAddressDescription() { 16 | let record = Record.extendedSegmentAddress(0xabcd) 17 | XCTAssertEqual(String(describing: record), "02 extended segment address – ABCD") 18 | } 19 | 20 | func testStartSegmentAddressDescription() { 21 | let record = Record.startSegmentAddress(codeSegment: 0xabcd, instructionPointer: 0xcd12) 22 | XCTAssertEqual(String(describing: record), "03 start segment address – CS: ABCD, IP: CD12") 23 | } 24 | 25 | func testExtendedLinearAddressDescription() { 26 | let record = Record.extendedLinearAddress(upperBits: 0xabcd) 27 | XCTAssertEqual(String(describing: record), "04 extended linear address – ABCD") 28 | } 29 | 30 | func testStartLinearAddressDescription() { 31 | let record = Record.startLinearAddress(0xab3400ff) 32 | XCTAssertEqual(String(describing: record), "05 start linear address – AB3400FF") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/HexHexHex/IntegerToString.swift: -------------------------------------------------------------------------------- 1 | extension UnsignedInteger { 2 | /// Formats the number as a string of hexadecimal digits, 3 | /// with optional padding with "0". 4 | /// 5 | /// - Parameter length: The total length the returned string should have. 6 | /// If the converted number is shorter than `length`, the string will be 7 | /// left-padded with zeros. If the converted number is longer than `length`, 8 | /// the full length will be returned. If `length` is `nil`, no padding 9 | /// will be applied. 10 | public func hex(padTo length: Int? = nil, uppercase: Bool = false) -> String { 11 | let hex = String(self, radix: 16, uppercase: uppercase) 12 | if let length = length, hex.count < length { 13 | return String(repeating: "0", count: length - hex.count) + hex 14 | } else { 15 | return hex 16 | } 17 | } 18 | 19 | /// Formats the number as a string of binary digits, 20 | /// with optional padding with "0". 21 | /// 22 | /// - Parameter length: The total length the returned string should have. 23 | /// If the converted number is shorter than `length`, the string will be 24 | /// left-padded with zeros. If the converted number is longer than `length`, 25 | /// the full length will be returned. If `length` is `nil`, no padding 26 | /// will be applied. 27 | public func binary(padTo length: Int? = nil) -> String { 28 | let binary = String(self, radix: 2) 29 | if let length = length, binary.count < length { 30 | return String(repeating: "0", count: length - binary.count) + binary 31 | } else { 32 | return binary 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HexHexHex 2 | 3 | A parser for the [Intel Hexadecimal Object File Format (.hex)](https://en.wikipedia.org/wiki/Intel_HEX), written in Swift. 4 | 5 | ## Status 6 | 7 | Very experimental and untested. 8 | 9 | ![](https://github.com/ole/HexHexHex/workflows/macOS/badge.svg) ![](https://github.com/ole/HexHexHex/workflows/Linux/badge.svg) 10 | 11 | ## Usage 12 | 13 | ### Add HexHexHex as a Swift Package Manager dependency 14 | 15 | ``` 16 | dependencies: [ 17 | .package(url: "https://github.com/ole/HexHexHex.git", from: "0.1.0"), 18 | ], 19 | ``` 20 | 21 | ### Code 22 | 23 | Creating a [`HEXFile`](Sources/HexHexHex/HEXFile.swift) value from the text of a .hex file: 24 | 25 | ```swift 26 | import HexHexHex 27 | 28 | let hexText = """ 29 | :020000040000FA 30 | :1000000025001C0C0200080C06006306590AE306D2 31 | :0C00B000590A000C3000070C2600030069 32 | :00000001FF 33 | """ 34 | let hexFile = try HEXFile(text: hexText) 35 | ``` 36 | 37 | The `HEXFile` value contains an array of [`Record`](Sources/HexHexHex/Record.swift) values that represent the records in the .hex file: 38 | 39 | ```swift 40 | debugPrint(hexFile) 41 | /* 42 | HEXFile (4 records) 43 | 04 extended linear address – 0000 44 | 00 data – address: 0000, data: 25 00 1C 0C 02 00 08 0C 06 00 63 06 59 0A E3 06 45 | 00 data – address: 00B0, data: 59 0A 00 0C 30 00 07 0C 26 00 03 00 46 | 01 end of file 47 | */ 48 | 49 | // Get all data records in the file 50 | let dataRecords = hexFile.records.filter { $0.kind == .data } 51 | print(dataRecords) 52 | 53 | // Print out the addresses of all data records in the file 54 | for case .data(let address, _) in hexFile.records { 55 | print(address) 56 | } 57 | ``` 58 | 59 | ## Author 60 | 61 | Ole Begemann, [oleb.net](https://oleb.net). 62 | 63 | ## License 64 | 65 | MIT. See [LICENSE.txt](LICENSE.txt). 66 | -------------------------------------------------------------------------------- /Sources/HexHexHex/HEXFile.swift: -------------------------------------------------------------------------------- 1 | /// A value representing the contents of an Intel Hexadecimal Object File Format (.hex). 2 | /// 3 | /// - SeeAlso: https://en.wikipedia.org/wiki/Intel_HEX 4 | public struct HEXFile { 5 | /// The records the file contains. Each line in the .hex file corresponds to one record. 6 | public var records: [Record] 7 | 8 | /// Initializes a `HEXFile` value with the text contents of a .hex file. 9 | /// 10 | /// - Parameter text: A string containing the contents of a .hex file. 11 | /// Example of a .hex file containing one data record and one end-of-file record: 12 | /// 13 | /// :10010000214601360121470136007EFE09D2190140 14 | /// :00000001FF 15 | /// 16 | /// - Throws: Throws a `HEXParser.Error` if `text` does not contain a valid .hex file 17 | /// or the string cannot be parsed. 18 | public init(text: String) throws { 19 | let parser = HEXParser(text: text) 20 | self.records = try parser.parse() 21 | } 22 | 23 | /// Initializes a `HEXFile` value with a collection of ASCII bytes. 24 | /// 25 | /// - Parameter bytes: A collection of bytes containing the contents of a .hex file. 26 | /// The bytes must be an ASCII-encoded string. 27 | /// 28 | /// - Throws: Throws a `HEXParser.Error` if `bytes` does not contain a valid .hex file 29 | /// or the string cannot be parsed. 30 | public init(bytes: C) throws where C.Element == UInt8 { 31 | let text = String(decoding: bytes, as: UTF8.self) 32 | try self.init(text: text) 33 | } 34 | } 35 | 36 | extension HEXFile: CustomStringConvertible, CustomDebugStringConvertible { 37 | public var description: String { 38 | "HEXFile (\(records.count) records)" 39 | } 40 | 41 | public var debugDescription: String { 42 | """ 43 | HEXFile (\(records.count) records) 44 | \(records.map(String.init(describing:)).joined(separator: "\n ")) 45 | """ 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/HexHexHexTests/HEXParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import HexHexHex 3 | 4 | final class HEXParserTests: XCTestCase { 5 | func testParseEndOfFileRecord() throws { 6 | let parser = HEXParser(text: ":00000001FF\n") 7 | let records = try parser.parse() 8 | XCTAssertEqual(records, [.endOfFile]) 9 | } 10 | 11 | func testParseDataRecord() throws { 12 | let parser = HEXParser(text: ":0201FE00FF0FF1\n") 13 | let records = try parser.parse() 14 | XCTAssertEqual(records, [.data(0x01fe, [0xff, 0x0f])]) 15 | } 16 | 17 | func testInvalidChecksumThrows() throws { 18 | let parser = HEXParser(text: ":00000001FE\n") 19 | XCTAssertThrowsError(try parser.parse()) { error in 20 | XCTAssertEqual((error as? HEXParser.Error)?.kind, .invalidChecksum) 21 | } 22 | } 23 | 24 | func testParseExtendedLinearAddressRecord() throws { 25 | let parser = HEXParser(text: ":020000040000FA\n") 26 | let records = try parser.parse() 27 | XCTAssertEqual(records, [.extendedLinearAddress(upperBits: 0x0000)]) 28 | } 29 | 30 | func testMultipleRecords() throws { 31 | let hex = """ 32 | :020000040000FA 33 | :1000000025001C0C0200080C06006306590AE306D2 34 | :100010000A0A0E0A3006590A3005110A3006200A6B 35 | :0C00B000590A000C3000070C2600030069 36 | :0400BC000008000830 37 | :021FFE00EF0FE3 38 | :00000001FF 39 | """ 40 | let parser = HEXParser(text: hex) 41 | let records = try parser.parse() 42 | XCTAssertEqual(records, [ 43 | .extendedLinearAddress(upperBits: 0x0000), 44 | .data(0x0000, [0x25, 0x00, 0x1C, 0x0C, 0x02, 0x00, 0x08, 0x0C, 0x06, 0x00, 0x63, 0x06, 0x59, 0x0A, 0xE3, 0x06]), 45 | .data(0x0010, [0x0A, 0x0A, 0x0E, 0x0A, 0x30, 0x06, 0x59, 0x0A, 0x30, 0x05, 0x11, 0x0A, 0x30, 0x06, 0x20, 0x0A]), 46 | .data(0x00b0, [0x59, 0x0A, 0x00, 0x0C, 0x30, 0x00, 0x07, 0x0C, 0x26, 0x00, 0x03, 0x00]), 47 | .data(0x00bc, [0x00, 0x08, 0x00, 0x08]), 48 | .data(0x1ffe, [0xEF, 0x0F]), 49 | .endOfFile, 50 | ]) 51 | } 52 | 53 | func testThrowsIfEndOfFileIsNotLastRecord() { 54 | let hex = """ 55 | :020000040000FA 56 | :1000000025001C0C0200080C06006306590AE306D2 57 | :00000001FF 58 | :021FFE00EF0FE3 59 | """ 60 | let parser = HEXParser(text: hex) 61 | XCTAssertThrowsError(try parser.parse()) { error in 62 | XCTAssertEqual((error as? HEXParser.Error)?.kind, .fileContinuesAfterEndOfFile) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/HexHexHex/Record.swift: -------------------------------------------------------------------------------- 1 | /// A record in the Intel Hexadecimal Object File Format (.hex). 2 | /// 3 | /// - SeeAlso: https://en.wikipedia.org/wiki/Intel_HEX 4 | public enum Record: Equatable { 5 | case data(Address16, [UInt8]) 6 | case endOfFile 7 | case extendedSegmentAddress(Address16) 8 | case startSegmentAddress(codeSegment: Address16, instructionPointer: Address16) 9 | case extendedLinearAddress(upperBits: Address16) 10 | case startLinearAddress(Address32) 11 | 12 | public var kind: Record.Kind { 13 | switch self { 14 | case .data: return .data 15 | case .endOfFile: return .endOfFile 16 | case .extendedSegmentAddress: return .extendedSegmentAddress 17 | case .startSegmentAddress: return .startSegmentAddress 18 | case .extendedLinearAddress: return .extendedLinearAddress 19 | case .startLinearAddress: return .startLinearAddress 20 | } 21 | } 22 | } 23 | 24 | extension Record { 25 | /// https://en.wikipedia.org/wiki/Intel_HEX#Record_types 26 | public enum Kind: Int { 27 | /// Contains data and a 16-bit starting address for the data. 28 | /// The byte count specifies number of data bytes in the record. 29 | case data = 0x00 30 | /// Must occur exactly once per file in the last line of the file. 31 | /// The data field is empty (thus byte count is 00) and the address field is typically 0000. 32 | case endOfFile = 0x01 33 | /// The data field contains a 16-bit segment base address (thus byte count is always 02) 34 | /// compatible with 80x86 real mode addressing. The address field (typically 0000) is ignored. 35 | /// The segment address from the most recent 02 record is multiplied by 16 and added to each 36 | /// subsequent data record address to form the physical starting address for the data. 37 | /// This allows addressing up to one megabyte of address space. 38 | case extendedSegmentAddress = 0x02 39 | /// For 80x86 processors, specifies the initial content of the CS:IP registers 40 | /// (i.e., the starting execution address). The address field is 0000, the byte count is 41 | /// always 04, the first two data bytes are the CS value, the latter two are the IP value. 42 | case startSegmentAddress = 0x03 43 | /// Allows for 32 bit addressing (up to 4GiB). The record's address field is ignored 44 | /// (typically 0000) and its byte count is always 02. The two data bytes (big endian) specify 45 | /// the upper 16 bits of the 32 bit absolute address for all subsequent type 00 records; 46 | /// these upper address bits apply until the next 04 record. The absolute address for a 47 | /// type 00 record is formed by combining the upper 16 address bits of the most recent 04 record 48 | /// with the low 16 address bits of the 00 record. If a type 00 record is not preceded by any 49 | /// type 04 records then its upper 16 address bits default to 0000. 50 | case extendedLinearAddress = 0x04 51 | /// The address field is 0000 (not used) and the byte count is always 04. The four data bytes 52 | /// represent a 32-bit address value (big-endian). In the case of 80386 and higher CPUs, 53 | /// this address is loaded into the EIP register. 54 | case startLinearAddress = 0x05 55 | } 56 | } 57 | 58 | extension Record: CustomStringConvertible { 59 | public var description: String { 60 | var desc = "\(UInt(kind.rawValue).hex(padTo: 2)) " 61 | switch self { 62 | case .data(let address, let bytes): 63 | let addressString = address.rawValue.hex(padTo: 4, uppercase: true) 64 | let byteString = bytes.map { $0.hex(padTo: 2, uppercase: true) }.joined(separator: " ") 65 | desc += "data – address: \(addressString), data: \(byteString)" 66 | case .endOfFile: 67 | desc += "end of file" 68 | case .extendedSegmentAddress(let address): 69 | let addressString = address.rawValue.hex(padTo: 4, uppercase: true) 70 | desc += "extended segment address – \(addressString)" 71 | case .startSegmentAddress(let codeSegment, let instructionPointer): 72 | let codeSegmentString = codeSegment.rawValue.hex(padTo: 4, uppercase: true) 73 | let instructionPointerString = instructionPointer.rawValue.hex(padTo: 4, uppercase: true) 74 | desc += "start segment address – CS: \(codeSegmentString), IP: \(instructionPointerString)" 75 | case .extendedLinearAddress(let address): 76 | let addressString = address.rawValue.hex(padTo: 4, uppercase: true) 77 | desc += "extended linear address – \(addressString)" 78 | case .startLinearAddress(let address): 79 | let addressString = address.rawValue.hex(padTo: 8, uppercase: true) 80 | desc += "start linear address – \(addressString)" 81 | } 82 | return desc 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/HexHexHex/HEXParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A parser for the Intel Hexadecimal Object File Format (.hex). 4 | /// 5 | /// - SeeAlso: https://en.wikipedia.org/wiki/Intel_HEX 6 | public struct HEXParser { 7 | var text: String 8 | 9 | public init(text: String) { 10 | self.text = text 11 | } 12 | 13 | public func parse() throws -> [Record] { 14 | var consumableText = text[...].utf8 15 | var records: [Record] = [] 16 | while !consumableText.isEmpty { 17 | let recordStart = consumableText.startIndex 18 | let record = try Self.parseRecord(in: &consumableText) 19 | if let previousRecord = records.last, previousRecord.kind == .endOfFile { 20 | throw Error(kind: .fileContinuesAfterEndOfFile, position: recordStart) 21 | } 22 | records.append(record) 23 | } 24 | return records 25 | } 26 | } 27 | 28 | extension HEXParser { 29 | /// Record format: 30 | /// 31 | /// | Record mark | Length (n) | Address | Type | Data | Checksum | 32 | /// | ----------- | ------------ | ------------ | ------------ | ------------- | ------------ | 33 | /// | ':' (colon) | 2 hex digits | 4 hex digits | 2 hex digits | 2n hex digits | 2 hex digits | 34 | /// | | (1 byte) | (2 bytes) | (1 byte) | (n bytes) | (1 byte) | 35 | /// 36 | private static func parseRecord(in text: inout Substring.UTF8View) throws -> Record { 37 | try parseRecordMark(in: &text) 38 | let byteCountIndex = text.startIndex 39 | let byteCount = try parseHexDigits(count: 2, in: &text) 40 | let addressBytes = try parseHexBytes(count: 2, in: &text) 41 | let recordType = try parseRecordType(in: &text) 42 | let dataBytes = try parseHexBytes(count: byteCount, in: &text) 43 | let checksumIndex = text.startIndex 44 | let checksum = try parseHexDigits(count: 2, in: &text) 45 | try parseLineBreakOrEndOfFile(in: &text) 46 | 47 | // Verify checksum 48 | let byteSum: UInt8 = UInt8(byteCount) 49 | &+ addressBytes.reduce(0, &+) 50 | &+ UInt8(recordType.rawValue) 51 | &+ dataBytes.reduce(0, &+) 52 | guard byteSum &+ UInt8(checksum) == 0 else { 53 | throw Error(kind: .invalidChecksum, position: checksumIndex) 54 | } 55 | 56 | switch recordType { 57 | case .data: 58 | let address = addressBytes.reduce(0) { acc, byte in acc << 8 + UInt16(byte) } 59 | return .data(Address16(rawValue: address), dataBytes) 60 | case .endOfFile: 61 | return .endOfFile 62 | case .extendedSegmentAddress: 63 | guard byteCount == 2 else { 64 | throw Error(kind: .expectedDifferentByteCount(expected: 2, actual: byteCount), position: byteCountIndex) 65 | } 66 | let address = dataBytes.reduce(0) { acc, byte in acc << 8 + UInt16(byte) } 67 | return .extendedSegmentAddress(Address16(rawValue: address)) 68 | case .startSegmentAddress: 69 | guard byteCount == 4 else { 70 | throw Error(kind: .expectedDifferentByteCount(expected: 4, actual: byteCount), position: byteCountIndex) 71 | } 72 | let codeSegment = dataBytes.prefix(2).reduce(0) { acc, byte in acc << 8 + UInt16(byte) } 73 | let instructionPointer = dataBytes.dropFirst(2).reduce(0) { acc, byte in acc << 8 + UInt16(byte) } 74 | return .startSegmentAddress(codeSegment: Address16(rawValue: codeSegment), instructionPointer: Address16(rawValue: instructionPointer)) 75 | case .extendedLinearAddress: 76 | guard byteCount == 2 else { 77 | throw Error(kind: .expectedDifferentByteCount(expected: 2, actual: byteCount), position: byteCountIndex) 78 | } 79 | let address = dataBytes.reduce(0) { acc, byte in acc << 8 + UInt16(byte) } 80 | return .extendedLinearAddress(upperBits: Address16(rawValue: address)) 81 | case .startLinearAddress: 82 | guard byteCount == 4 else { 83 | throw Error(kind: .expectedDifferentByteCount(expected: 4, actual: byteCount), position: byteCountIndex) 84 | } 85 | let address = dataBytes.reduce(0) { acc, byte in acc << 8 + UInt32(byte) } 86 | return .startLinearAddress(Address32(rawValue: address)) 87 | } 88 | } 89 | 90 | /// Parses the magic colon ':' at the start of a record. 91 | /// 92 | /// - Throws: Throws an error if the text doesn't start with the magic start code. 93 | private static func parseRecordMark(in text: inout Substring.UTF8View) throws { 94 | guard text.first == ASCII.colon else { 95 | throw Error(kind: .expectedColon, position: text.startIndex) 96 | } 97 | text = text.dropFirst() 98 | } 99 | 100 | private static func parseRecordType(in text: inout Substring.UTF8View) throws -> Record.Kind { 101 | let sourcePosition = text.startIndex 102 | let rawValue = try parseHexDigits(count: 2, in: &text) 103 | guard let recordType = Record.Kind(rawValue: rawValue) else { 104 | throw Error(kind: .invalidRecordType(rawValue), position: sourcePosition) 105 | } 106 | return recordType 107 | } 108 | 109 | private static func parseLineBreakOrEndOfFile(in text: inout Substring.UTF8View) throws { 110 | switch text.first { 111 | case nil: 112 | // End of file, nothing to do 113 | break 114 | case ASCII.lineFeed: 115 | text = text.dropFirst() 116 | case ASCII.carriageReturn: 117 | text = text.dropFirst() 118 | if text.first == ASCII.lineFeed { 119 | text = text.dropFirst() 120 | } 121 | default: 122 | throw Error(kind: .expectedLineBreak, position: text.startIndex) 123 | } 124 | } 125 | 126 | /// Parses ASCII text as a sequence of hexadecimal bytes. 127 | /// 128 | /// - Parameters: 129 | /// - count: The number of bytes (2 hex digits per byte) to parse. 130 | /// - text: The source text. Parsing starts at the start of the text and consumes the parsed 131 | /// characters 132 | private static func parseHexBytes(count: Int, in text: inout Substring.UTF8View) throws -> [UInt8] { 133 | var bytes: [UInt8] = [] 134 | for _ in 0 ..< count { 135 | try bytes.append(UInt8(parseHexDigits(count: 2, in: &text))) 136 | } 137 | return bytes 138 | } 139 | 140 | /// Parses ASCII text as one or more hexadecimal digits and returns them as an integer value. 141 | /// 142 | /// - Parameters: 143 | /// - count: The number of digits (== the number of ASCII characters) to parse. 144 | /// - text: The source text. Parsing starts at the start of the text and consumes the parsed 145 | /// characters 146 | private static func parseHexDigits(count: Int, in text: inout Substring.UTF8View) throws -> Int { 147 | guard text.count >= count else { 148 | throw Error(kind: .expectedHexDigits(count: count), position: text.startIndex) 149 | } 150 | guard let digits = String(text.prefix(count)), 151 | let value = Int(digits, radix: 16) else { 152 | throw Error(kind: .expectedHexDigits(count: count), position: text.startIndex) 153 | } 154 | text = text.dropFirst(count) 155 | return value 156 | } 157 | } 158 | 159 | extension HEXParser { 160 | public struct SourcePosition: Equatable { 161 | var position: String.Index 162 | } 163 | } 164 | 165 | extension HEXParser { 166 | public struct Error: Swift.Error, Equatable { 167 | public var kind: Kind 168 | public var sourcePosition: SourcePosition 169 | 170 | init(kind: Kind, position: String.Index) { 171 | self.kind = kind 172 | self.sourcePosition = SourcePosition(position: position) 173 | } 174 | } 175 | } 176 | 177 | extension HEXParser.Error { 178 | public enum Kind: Equatable, CustomStringConvertible { 179 | case expectedColon 180 | case expectedHexDigits(count: Int) 181 | case invalidRecordType(Int) 182 | case expectedLineBreak 183 | case invalidChecksum 184 | case expectedDifferentByteCount(expected: Int, actual: Int) 185 | case fileContinuesAfterEndOfFile 186 | 187 | var errorCode: Int { 188 | switch self { 189 | case .expectedColon: return 1 190 | case .expectedHexDigits: return 2 191 | case .invalidRecordType: return 3 192 | case .expectedLineBreak: return 4 193 | case .invalidChecksum: return 5 194 | case .expectedDifferentByteCount: return 6 195 | case .fileContinuesAfterEndOfFile: return 7 196 | } 197 | } 198 | 199 | public var description: String { 200 | switch self { 201 | case .expectedColon: return "expectedColon" 202 | case .expectedHexDigits(let count): return "expectedHexDigits: \(count)" 203 | case .invalidRecordType(_): return "invalidRecordType" 204 | case .expectedLineBreak: return "expectedLineBreak" 205 | case .invalidChecksum: return "invalidChecksum" 206 | case .expectedDifferentByteCount(let expected, let actual): return "expectedDifferentByteCount: expected: \(expected) actual: \(actual)" 207 | case .fileContinuesAfterEndOfFile: return "fileContinuesAfterEndOfFile" 208 | } 209 | } 210 | } 211 | } 212 | 213 | extension HEXParser.Error: CustomNSError { 214 | public static var errorDomain: String { "HEXParser.Error" } 215 | public var errorCode: Int { 1000 } 216 | public var errorUserInfo: [String : Any] { 217 | [NSLocalizedDescriptionKey: "HEXParser.Error: \(kind) \(sourcePosition)"] 218 | } 219 | } 220 | 221 | private enum ASCII { 222 | static let colon = UInt8(ascii: ":") 223 | static let carriageReturn = UInt8(ascii: "\r") 224 | static let lineFeed = UInt8(ascii: "\n") 225 | } 226 | --------------------------------------------------------------------------------