Lilliput is a native Swift framework for working with binary data.
- Does not support streaming data, it only works with data that can fit in memory.
- Allows types to have multiple encodings/decodings, this means that a UInt32 could be easily represented as little endian, big endian, or some custom compressed encoding.
- Does not support the idea of "native" endianness, when working with data where endianess matters you must always be specific.
- Makes use of the swift-system package for reading/writing files.
import Lilliput
// Allocate enough storage for a 32-bit unsigned integer
let buffer = ByteBuffer(count: 4)
try buffer.slice(...) { bytes in
// Write a UInt32 to that storage in little endian encoding
var writer = ByteSpanWriter(bytes)
try writer.write(UInt32(100), as: UInt32.LittleEndian.self)
// Read a UInt32 from that stroage in little endian encoding
var reader = ByteSpanReader(bytes)
let value = try reader.read(UInt32.LittleEndian.self)
}
struct MyType {
var value0: Int
var value1: Int8
var value3: Double
}
extension MyType: ByteDecoder {
static func decode<R: Reader>(from reader: inout R) throws -> MyType {
return MyType(
value0: Int(try reader.read(UInt32.LittleEndian.self)),
value1: try reader.read(Int8.self),
value2: try reader.read(Float64.LittleEndian.self)
)
}
}
extension MyType: ByteEncoder {
static func encode<W: Writer>(_ value: MyType, to writer: inout W) throws {
try writer.write(UInt32(value.value0), as: UInt32.LittleEndian.self)
try writer.write(value.value1)
try writer.write(value.value2, as: Float64.LittleEndian.self)
}
}
extension MyType {
@frozen enum Custom {}
}
extension MyType.Custom: ByteDecoder {
static func decode<R: Reader>(from reader: inout R) throws -> MyType {
// Values are loaded/stored in reverse order from the type definition and using big endian
let value2 = try reader.read(Float64.BigEndian.self)
let value1 = try reader.read(Int8.self)
try reader.alignTo(4) // Make sure next read occurs after three padding bytes
let value0 = Int(try reader.read(UInt32.BigEndian.self))
return MyType(
value0: value0,
value1: value1,
value2: value2
)
}
}
extension MyType.Custom: ByteEncoder {
static func encode<W: Writer>(_ value: MyType, to writer: inout W) throws {
// Values are loaded/stored in reverse order from the type definition and using big endian
try writer.write(value.value2, as: Float64.BigEndian.self)
try writer.write(value.value1)
try writer.write((0, 0, 0), as: UInt8.Tuple3.self) // Add three padding bytes
try writer.write(UInt32(value.value0), as: UInt32.BigEndian.self)
}
}
import Lilliput
import SystemPackage
let file = try FileDescriptor.open(path, .readOnly)
let buffer = ByteBuffer(count: 13)
let myType = try buffer.slice(...) { bytes in
try file.readAll(into: bytes)
var reader = ByteSpanReader(bytes)
return try reader.read(MyType.self)
}
If the data was stored on disk using the Custom encoding instead
return try reader.read(MyType.Custom.self)
import Lilliput
import SystemPackage
let myType = MyType(value0: 100, value1: 8, value2: 300.56)
let buffer = ByteBuffer(count: 13)
try buffer.slice(...) { bytes in
var writer = SpanByteWriter(buffer)
try writer.write(myType)
}
let file = try FileDescriptor.open(path, .writeOnly)
file.writeAll(buffer)
If we wanted to store the data using the Custom encoding instead
try writer.write(myType, as: MyType.Custom.self)
let count = Int(try reader.read(UInt32.LittleEndian.self))
let arrayOfMyType = try reader.read(Element<MyType>.self, count: count)
try writer.write(UInt32(arrayOfMyType.count), as: UInt32.LittleEndian.self)
try writer.write(arrayOfMyType, as: Element<MyType>.self)
Note: This is not built in to the library due to decisions on how to encode the count value could differ wildly across use cases.
@frozen struct MyArray<E> {}
extension MyArray: ByteDecoder where E: ByteDecoder {
static func decode<R: Reader>(from reader: inout R) throws -> [E.Decodable] {
let count = Int(try reader.read(UInt32.LittleEndian.self))
return try reader.read(Element<E>.self, count: count)
}
}
extension MyArray: ByteEncoder where E: ByteEncoder {
static func encode<W: ByteWriter>(_ value: [E.Encodable], to writer: inout W) throws {
try writer.write(UInt32(value.count), as: UInt32.LittleEndian.self)
for element in value {
try writer.write(element, as: E.self)
}
}
}
Usage (note that the count value is handled without extra work now)
let arrayOfMyType = try reader.read(MyArray<MyType>.self)
try writer.write(arrayOfMyType, as: MyArray<MyType>.self)
let data = Data(...) // Get data somehow
var reader = DataReader(data: data, maxReadCount: 8)
let myType = try reader.read(MyType.self)
Choose a value for maxReadCount
that represents the maximum amount of bytes read in one call to read
. In the example above the longest read is a Double
which is 8 bytes. In the worst case you can omit the maxReadCount
parameter and then the entire length of the Data would be used as the default.
All of the read
methods on ByteReader
and write
methods on ByteWriter
that do not involve a ByteDecoder
/ByteEncoder
no longer check to see if there are enough bytes to complete the operation. This is now handled separately be calling ensure
. For example try reader.ensure(5)
will throw if there are not enough bytes left to read.
A side effect of this change is that when you write a single UInt8
it no longer triggers the UInt8
implementation of ByteEncoder
.
Previously you would do this and it would throw if there was not enough space to write:
try writer.write(UInt8(7))
Now the new write
method on ByteWriter
that takes a single UInt8
superceeds this.
To get the new behavior do this:
try ensure(1)
writer.write(UInt8(7))
or do this which triggers the ByteEncoder
implementation manually:
try writer.write(UInt8(7), as: UInt8.self)
To use the Lilliput
library in a SwiftPM project, add the following line to the dependencies in your Package.swift
file:
.package(url: "https://github.com/jkolb/Lilliput.git", from: "12.0.0"),
Finally, include "Lilliput"
as a dependency for your target:
let package = Package(
// name, platforms, products, etc.
dependencies: [
.package(url: "https://github.com/jkolb/Lilliput.git", from: "12.0.0"),
// other dependencies
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "Lilliput", package: "Lilliput"),
]),
// other targets
]
)
Lilliput is available under the MIT license. See the LICENSE file for more info.