New version. Quite happy with it. Comments/improvements welcome.
import Foundation
public enum Endian {
case big, little
}
public enum DataConversionOption {
case encoding(String.Encoding)
case endian(Endian)
case nbrFromString
}
extension DataConversionOption: Hashable {
public var hashValue: Int {
switch self {
case .encoding: return 0
case .endian: return 1
case .nbrFromString: return 2
}
}
public static func ==(lhs: DataConversionOption, rhs: DataConversionOption) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
public typealias DataConversionOptions = Set<DataConversionOption>
public enum OccursDependingOn {
case occursDepending
}
extension String {
func leftPadding(toLength: Int, withPad: String = " ") -> String {
guard toLength > count else { return self }
let padding = String(repeating: withPad, count: toLength - count)
return padding + self
}
}
extension String.Encoding {
public static let ebcdic = String.Encoding(rawValue: 0x8000_0C02)
#if os(zOS)
public static var native = String.Encoding.ebcdic037
#else
public static var native = String.Encoding.ascii
#endif
fileprivate init(conversionOptions: DataConversionOptions) {
for case .encoding(let newEncoding) in conversionOptions {
self = newEncoding
return
}
self = String.Encoding.native
}
}
extension MutableCollection {
mutating func updateEach(update: (inout Element) -> ()) {
for index in indices {
update(&self[index])
}
}
}
/// A protocol for types that can be converted to and from a Data "record buffer".
public protocol DataRecordConvertable {
static var defaultValue: Self { get }
init?(buffer: Data, conversionOptions: DataConversionOptions)
func toDataRecord(buffer: inout Data, conversionOptions: DataConversionOptions)
}
extension String: DataRecordConvertable {
public static var defaultValue: String { return "" }
public init?(buffer: Data, conversionOptions options: DataConversionOptions) {
let firstNonNul = buffer.first { $0 != 0 }
guard let _ = firstNonNul
else { return nil } // all bytes in buffer (slice) are nuls
guard var decodedString = String(data: buffer, encoding: Encoding(conversionOptions: options))
else { fatalError("Decoding error") }
/* remove trailing spaces */
while decodedString.hasSuffix(" ") {
decodedString.removeLast()
}
self = decodedString
}
public func toDataRecord(buffer: inout Data, conversionOptions options: DataConversionOptions) {
/* make sure string is "same size" as buffer */
func rightSize() -> String {
var str = self
let diff = buffer.count - count
if diff > 0 {
str.append(String(repeating: " ", count: diff)) // append trailing spaces
}
else {
str.removeLast(0 - diff) // truncate to proper length
}
return str
}
if let dataRecord = rightSize().data(using: Encoding(conversionOptions: options)) {
buffer = dataRecord
}
}
}
extension Character: DataRecordConvertable {
public static var defaultValue: Character { return Character("\0") }
public init?(buffer: Data, conversionOptions options: DataConversionOptions) {
guard let asString = String(buffer: buffer, conversionOptions: options)
else { return nil }
self = Character(asString.isEmpty ? " " : asString)
}
public func toDataRecord(buffer: inout Data, conversionOptions options: DataConversionOptions) {
String(self).toDataRecord(buffer: &buffer, conversionOptions: options)
}
}
/* DataRecordConvertable implementation for all integer types */
extension FixedWidthInteger {
public static var defaultValue: Self { return Self(0) }
public init(buffer: Data, conversionOptions options: DataConversionOptions) {
for case .nbrFromString in options {
guard let str = String(buffer: buffer, conversionOptions: options)
else { fatalError("data is nil...") }
guard let myself = Self(str)
else { fatalError("Data \(Array(buffer)) ('\(str)') failed converstion to numeric value") }
self = myself
return
}
self = Self.endianConverted(buffer.withUnsafeBytes { $0.pointee }, conversionOptions: options)
}
public func toDataRecord(buffer: inout Data, conversionOptions options: DataConversionOptions) {
for case .nbrFromString in options {
String(self).leftPadding(toLength: buffer.count, withPad: "0")
.toDataRecord(buffer: &buffer, conversionOptions: options)
return
}
var myself = Self.endianConverted(self, conversionOptions: options)
buffer = Data(bytes: &myself, count: MemoryLayout<Self>.size)
}
private static func endianConverted(_ number: Self, conversionOptions: DataConversionOptions) -> Self {
for case .endian(let endian) in conversionOptions {
return (endian == .big) ? number.bigEndian : number.littleEndian
}
return number
}
}
extension Int: DataRecordConvertable {}
extension UInt: DataRecordConvertable {}
extension Int64: DataRecordConvertable {}
extension UInt64: DataRecordConvertable {}
extension Int32: DataRecordConvertable {}
extension UInt32: DataRecordConvertable {}
extension Int16: DataRecordConvertable {}
extension UInt16: DataRecordConvertable {}
extension Int8: DataRecordConvertable {}
extension UInt8: DataRecordConvertable {}
public struct RecordPropertyMap<Record: RecordPropertyMapable> {
fileprivate let fromData: (inout Record, Data, inout Int) -> Void
fileprivate let toData: (Record, inout Data, inout Int) -> Void
init(padLength len: Int = 1) {
precondition(len > 0, "Length must be positive value")
fromData = { $2 += len }
toData = { $2 += len }
}
init<Value: FixedWidthInteger>(padField _: Value.Type) {
let len = MemoryLayout<Value>.size
fromData = { $2 += len }
toData = { $2 += len }
}
/* init for non-Optionals */
init<Value: DataRecordConvertable>(_ property: WritableKeyPath<Record, Value>,
length len: Int = 1,
conversionOptions options: DataConversionOptions = []) {
precondition(len > 0, "Length must be positive value")
let options = RecordPropertyMap.insertDefaults(to: options)
fromData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
if let value = Value(buffer: buffer[lower ..< upper], conversionOptions: options) {
record[keyPath: property] = value
}
}
toData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
record[keyPath: property].toDataRecord(buffer: &buffer[lower ..< upper], conversionOptions: options)
}
}
/* init for Optionals */
init<Value: DataRecordConvertable>(_ property: WritableKeyPath<Record, Value?>,
length len: Int = 1,
conversionOptions options: DataConversionOptions = []) {
precondition(len > 0, "Length must be positive value")
let options = RecordPropertyMap.insertDefaults(to: options)
fromData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
record[keyPath: property] = Value(buffer: buffer[lower ..< upper], conversionOptions: options)
}
toData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
record[keyPath: property]?.toDataRecord(buffer: &buffer[lower ..< upper], conversionOptions: options)
}
}
/* Fixed-count Array */
init<Value: DataRecordConvertable>(_ property: WritableKeyPath<Record, [Value]>,
length len: Int = 1,
occurs elementCount: Int,
conversionOptions options: DataConversionOptions = []) {
precondition(len > 0, "Length must be positive value")
let options = RecordPropertyMap.insertDefaults(to: options)
fromData = { record, buffer, start in
record[keyPath: property].reserveCapacity(elementCount)
record[keyPath: property] = (0 ..< elementCount).flatMap { _ in
let (lower, upper) = (start, start + len)
start = upper // next start
if let value = Value(buffer: buffer[lower ..< upper], conversionOptions: options) {
return value
}
return nil
}
}
toData = { record, buffer, start in
precondition(elementCount == record[keyPath: property].count, "Unexpected condition")
record[keyPath: property].forEach {
let (lower, upper) = (start, start + len)
start = upper // next start
$0.toDataRecord(buffer: &buffer[lower ..< upper], conversionOptions: options)
}
}
}
/* Prefixed Array */
init<Value: DataRecordConvertable,
Prefix: FixedWidthInteger>(_ property: WritableKeyPath<Record, [Value]>,
length len: Int = 1,
occursPrefixedBy _: Prefix.Type,
conversionOptions options: DataConversionOptions = []) {
precondition(len > 0, "Length must be positive value")
let options = RecordPropertyMap.insertDefaults(to: options)
fromData = { record, buffer, start in
/* read prefix that contains the number of elements to follow */
let (first, last) = (start, start + MemoryLayout<Prefix>.size)
start = last
let elementCount = Int(Prefix(buffer: buffer[first ..< last], conversionOptions: options))
/* read the elements themselves */
record[keyPath: property].reserveCapacity(elementCount)
record[keyPath: property] = (0 ..< elementCount).flatMap { _ in
let (lower, upper) = (start, start + len)
start = upper // next start
if let value = Value(buffer: buffer[lower ..< upper], conversionOptions: options) {
return value
}
return nil
}
}
toData = { record, buffer, start in
/* write out the prefix */
let prefixLength = MemoryLayout<Prefix>.size
let (first, last) = (start, start + prefixLength)
start = last
let elementCount = Prefix(record[keyPath: property].count)
elementCount.toDataRecord(buffer: &buffer[first ..< last], conversionOptions: options)
/* write out the individual elements */
record[keyPath: property].forEach {
let (lower, upper) = (start, start + len)
start = upper // next start
$0.toDataRecord(buffer: &buffer[lower ..< upper], conversionOptions: options)
}
}
}
/* this represents the "occurance" (count) value of the field referred to in the 'ref' parameter */
init<Value: DataRecordConvertable,
Count: FixedWidthInteger>(ref: WritableKeyPath<Record, [Value]>,
is _: Count.Type,
conversionOptions options: DataConversionOptions = []) {
let options = RecordPropertyMap.insertDefaults(to: options)
let len = MemoryLayout<Count>.size
fromData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
/* */
record[keyPath: ref] = [Value](repeating: .defaultValue,
count: Int(Count(buffer: buffer[lower ..< upper],
conversionOptions: options)))
}
toData = { record, buffer, start in
let (lower, upper) = (start, start + len)
start = upper // next start
Count(record[keyPath: ref].count).toDataRecord(buffer: &buffer[lower ..< upper],
conversionOptions: options)
}
}
/* This is the actual field, where the occurance count was already set via the 'ref' initializer */
init<Value: DataRecordConvertable>(_ property: WritableKeyPath<Record, [Value]>,
length len: Int = 1,
_: OccursDependingOn = .occursDepending,
conversionOptions options: DataConversionOptions = []) {
precondition(len > 0, "Length must be positive value")
let options = RecordPropertyMap.insertDefaults(to: options)
fromData = { record, buffer, start in
/* the number of elements has already been set with dummy (default) values,
so here we simply replace them with the values from the buffer area */
record[keyPath: property].updateEach { element in
let (lower, upper) = (start, start + len)
start = upper // next start
if let value = Value(buffer: buffer[lower ..< upper], conversionOptions: options) {
element = value
}
}
}
toData = { record, buffer, start in
record[keyPath: property].forEach { element in
let (lower, upper) = (start, start + len)
start = upper // next start
element.toDataRecord(buffer: &buffer[lower ..< upper], conversionOptions: options)
}
}
}
/* specialized inits for FixedWidthIntegers; we can calculate the field length. */
init<Value: FixedWidthInteger & DataRecordConvertable>(_ property: WritableKeyPath<Record, Value>,
conversionOptions options: DataConversionOptions = []) {
self.init(property, length: MemoryLayout<Value>.size, conversionOptions: options)
}
init<Value: FixedWidthInteger & DataRecordConvertable>(_ property: WritableKeyPath<Record, Value?>,
conversionOptions options: DataConversionOptions = []) {
self.init(property, length: MemoryLayout<Value>.size, conversionOptions: options)
}
init<Value: FixedWidthInteger & DataRecordConvertable>(_ property: WritableKeyPath<Record, [Value]>,
occurs elementCount: Int,
conversionOptions options: DataConversionOptions = []) {
self.init(property, length: MemoryLayout<Value>.size, occurs: elementCount, conversionOptions: options)
}
init<Value: FixedWidthInteger & DataRecordConvertable,
Prefix: FixedWidthInteger>(_ property: WritableKeyPath<Record, [Value]>,
occursPrefixedBy prefixType: Prefix.Type,
conversionOptions options: DataConversionOptions = []) {
self.init(property, length: MemoryLayout<Value>.size, occursPrefixedBy: prefixType, conversionOptions: options)
}
/* this inserts the default option (if there is one) if an option of that type has not already been set */
private static func insertDefaults(to options: DataConversionOptions) -> DataConversionOptions {
var options = options
options.insert(.endian(Record.binaryEndianess))
options.insert(.encoding(Record.stringEncoding))
return options
}
}
public protocol RecordPropertyMapable {
static var stringEncoding: String.Encoding { get }
static var binaryEndianess: Endian { get }
static var recordPropertyMaps: [RecordPropertyMap<Self>] { get }
init()
}
extension RecordPropertyMapable {
public static var stringEncoding: String.Encoding { return .native }
#if os(zOS) // z/OS is an EBCDIC big endian platform
public static var binaryEndianess: Endian { return .big }
#else
public static var binaryEndianess: Endian { return .little }
#endif
public init(recordBuffer buffer: Data) {
self.init()
self.apply(recordBuffer: buffer)
}
public mutating func apply(recordBuffer buffer: Data) {
var offset = buffer.startIndex
Self.recordPropertyMaps.forEach { $0.fromData(&self, buffer, &offset) }
}
public func recordBuffer(basedOn buffer: Data) -> Data {
var buffer = buffer
var offset = buffer.startIndex
Self.recordPropertyMaps.forEach { $0.toData(self, &buffer, &offset) }
return buffer
}
public func recordBuffer(size: Int) -> Data {
return recordBuffer(basedOn: Data(count: size))
}
}
Conforming structs:
public struct Stuff {
var name: String = "wxyz"
var nbr: UInt32? = 0
var char: Character = "?"
var shorts: [Int16] = []
var optStr: String? = "not empty"
var nbrs: [UInt32] = []
var nbr1: UInt32 = 0
var bytes: [UInt8] = []
var chars: [Character] = []
var strings1: [String] = []
var strings2: [String] = []
var strings3: [String] = []
var other: [OtherStuff] = []
public init() {} // declare default initializer public
}
extension Stuff: RecordPropertyMapable {
public static let binaryEndianess = Endian.little
public static let stringEncoding = String.Encoding.ascii
public static let recordPropertyMaps: [RecordPropertyMap<Stuff>] = [
RecordPropertyMap(\.name, length: 5),
RecordPropertyMap(\.nbr),
RecordPropertyMap(\.char, conversionOptions: [.encoding(.ebcdic)]),
RecordPropertyMap(ref: \.bytes, is: UInt32.self, conversionOptions: [.endian(.big)]),
RecordPropertyMap(ref: \.chars, is: UInt32.self),
RecordPropertyMap(\.shorts, occurs: 3),
RecordPropertyMap(\.nbrs, occursPrefixedBy: UInt32.self),
RecordPropertyMap(padField: UInt32.self),
RecordPropertyMap(padLength: 5),
RecordPropertyMap(\.nbr1, length: 8, conversionOptions: [.nbrFromString]),
RecordPropertyMap(\.bytes, .occursDepending),
RecordPropertyMap(\.chars, .occursDepending),
RecordPropertyMap(ref: \.strings3, is: UInt32.self),
RecordPropertyMap(\.strings1, length: 8, occurs: 2),
RecordPropertyMap(\.strings2, length: 8, occursPrefixedBy: UInt32.self),
RecordPropertyMap(\.strings3, length: 8, .occursDepending),
RecordPropertyMap(\.other, length: 12, occurs: 3),
]
}
public struct OtherStuff {
var i: Int32 = 0
var s: String = ""
public init() {} // declare default initializer public
}
extension OtherStuff: RecordPropertyMapable {
public static let binaryEndianess = Endian.little
public static let stringEncoding = String.Encoding.ascii
public static let recordPropertyMaps: [RecordPropertyMap<OtherStuff>] = [
RecordPropertyMap(\.i),
RecordPropertyMap(\.s, length: 8)
]
}
/* I hope to be able to make these three items a default implementation
of the DataRecordConvertable protocol itself, but I've not gotten
there quite yet... */
extension OtherStuff: DataRecordConvertable {
public static var defaultValue: OtherStuff = OtherStuff()
public init?(buffer: Data, conversionOptions options: DataConversionOptions) {
self.init(recordBuffer: buffer)
}
public func toDataRecord(buffer: inout Data, conversionOptions: DataConversionOptions) {
buffer = self.recordBuffer(basedOn: buffer)
}
}
Tests:
let stuffBuffer = Data([
0x39, 0x38, 0x37, 0x36, 0x20, // ASCII "9876 "
0xD2, 0x02, 0x96, 0x49, // little-endian 1234567890 / 0x499602D2
0x5A, // EBCDIC "!"
0, 0, 0, 3,
4, 0, 0, 0,
1, 0,
2, 0,
3, 0,
2, 0, 0, 0,
99, 0, 0, 0,
100, 0, 0, 0,
0,0,0,0,0,0,0,0,0, // "padding"; ignore
0x30, 0x30, 0x30, 0x30, 0x39, 0x38, 0x37, 0x38, // ASCII "00009876"
0x31, 0x32, 0x33,
0x39, 0x38, 0x37, 0x36,
1, 0, 0, 0,
0x41, 0x42, 0x43, 0x44, 0x20, 0x20, 0x20, 0x20, // ASCII "ABCD "
0x61, 0x62, 0x63, 0x64, 0x20, 0x20, 0x20, 0x20, // ASCII "abcd "
2, 0, 0, 0,
0x61, 0x62, 0x63, 0x64, 0x20, 0x20, 0x20, 0x20, // ASCII "abcd "
0x41, 0x42, 0x43, 0x44, 0x20, 0x20, 0x20, 0x20, // ASCII "ABCD "
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, // ASCII "STUVWXYZ"
0xFF,0,0,0,
0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x20, // ASCII "XXXXXXX "
0x0F,0,0,0,
0x59, 0x59, 0x59, 0x59, 0x59, 0x59, 0x20, 0x20, // ASCII "YYYYYY "
0xF0,0,0,0,
0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x20, 0x20, 0x20, // ASCII "ZZZZZ "
])
let sbSize = stuffBuffer.count * MemoryLayout.size(ofValue: stuffBuffer[0])
print(sbSize)
print(Array(stuffBuffer))
print(NSData(data: stuffBuffer))
/* two step initialization */
var stuff1 = Stuff()
stuff1.apply(recordBuffer: stuffBuffer)
print(stuff1)
/* one step initialization */
let stuff2 = Stuff(recordBuffer: stuffBuffer)
print(stuff2)
/* convert back and print */
let stuffBufferNew = stuff1.recordBuffer(size: sbSize)
print(Array(stuffBufferNew))
print(NSData(data: stuffBufferNew))
print("match: \(stuffBufferNew == stuffBuffer)") // false if mapping has non-nul padding bytes
stuff1.name = "abcdefg" // will be truncated when converted
stuff1.optStr = "2112"
let stuffBuffer3 = stuff1.recordBuffer(size: sbSize)
print(Array(stuffBuffer3))
print(NSData(data: stuffBuffer3))
stuff1.name = "vwxyz"
stuff1.optStr = nil // will not update record when converted
stuff1.other[0].i = 98765
print(stuff1)
print(Array(stuff1.recordBuffer(basedOn: stuffBuffer3)))
print(NSData(data: stuff1.recordBuffer(basedOn: stuffBuffer3)))