├── .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 |  
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 |
--------------------------------------------------------------------------------