Skip to content

Commit

Permalink
Add Swift streamed reader (foxglove#348)
Browse files Browse the repository at this point in the history
**Public-Facing Changes**
Adds `MCAPStreamedReader` class and conformance test runner for Swift.
  • Loading branch information
jtbandes authored Mar 28, 2022
1 parent ef92c78 commit 0a55b20
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
excluded:
- .build

disabled_rules:
- function_body_length
- type_body_length
Expand All @@ -6,3 +9,8 @@ disabled_rules:

trailing_comma:
mandatory_comma: true

identifier_name:
excluded:
- op
- id
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ overrides:
words:
- subrange
- unkeyed
- lowercased
16 changes: 16 additions & 0 deletions swift/conformance/Main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@main
enum Conformance {
static func main() async throws {
if CommandLine.arguments.count < 2 {
fatalError("Usage: conformance [read|write] ...")
}
switch CommandLine.arguments[1] {
case "read":
try await ReadConformanceRunner.main()
case "write":
try await WriteConformanceRunner.main()
default:
fatalError("Usage: conformance [read|write] ...")
}
}
}
90 changes: 90 additions & 0 deletions swift/conformance/ReadConformanceRunner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Foundation
import mcap

private extension String {
func camelToSnake() -> String {
var result = ""
var wordStart = startIndex
while wordStart != endIndex {
var wordEnd = index(after: wordStart)
while wordEnd != endIndex, self[wordEnd].isUppercase {
// handle all-uppercase words at the end of the string, e.g. schemaID and dataSectionCRC
// (does not handle correctly if the all-uppercase word is followed by another word)
formIndex(after: &wordEnd)
}
while wordEnd != endIndex, self[wordEnd].isLowercase {
formIndex(after: &wordEnd)
}
if !result.isEmpty {
result.append("_")
}
result.append(self[wordStart ..< wordEnd].lowercased())
wordStart = wordEnd
}
return result
}
}

private func toJson(_ record: Record) -> [String: Any] {
let mirror = Mirror(reflecting: record)
var fields: [String: Any] = [:]
for child in mirror.children {
var jsonValue: Any
switch child.value {
case let value as String:
jsonValue = value
case let value as Data:
jsonValue = value.map { String($0) }
case let value as UInt8:
jsonValue = String(value)
case let value as UInt16:
jsonValue = String(value)
case let value as UInt32:
jsonValue = String(value)
case let value as UInt64:
jsonValue = String(value)
case let value as [(UInt64, UInt64)]:
jsonValue = value.map { [String($0.0), String($0.1)] }
case let value as [String: String]:
jsonValue = value
case let value as [UInt16: UInt64]:
jsonValue = Dictionary(uniqueKeysWithValues: value.map { (String($0.key), String($0.value)) })
default:
fatalError("Unhandled type \(type(of: child.value))")
}
fields[child.label!.camelToSnake()] = jsonValue
}
return [
"type": String(describing: mirror.subjectType),
"fields": fields.sorted(by: { $0.key < $1.key }).map { [$0.key, $0.value] },
]
}

enum ReadConformanceRunner {
static func main() async throws {
if CommandLine.arguments.count < 3 {
fatalError("Usage: conformance read [test-data.mcap]")
}
let filename = CommandLine.arguments[2]
let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: filename))

var records: [Record] = []
let reader = MCAPStreamedReader()
while case let data = file.readData(ofLength: 4 * 1024), data.count != 0 {
reader.append(data)
while let record = try reader.nextRecord() {
if !(record is MessageIndex) {
records.append(record)
}
}
}

let data = try JSONSerialization.data(withJSONObject: ["records": records.map(toJson)], options: .prettyPrinted)

if #available(macOS 10.15.4, *) {
try FileHandle.standardOutput.write(contentsOf: data)
} else {
FileHandle.standardOutput.write(data)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ enum TestRecord {
}

// swiftlint:disable force_cast
@main
enum ConformanceRunner {
enum WriteConformanceRunner {
static func main() async throws {
if CommandLine.arguments.count < 2 {
fatalError("Usage: conformance [test-data.json]")
if CommandLine.arguments.count < 3 {
fatalError("Usage: conformance write [test-data.json]")
}
let filename = CommandLine.arguments[1]
let filename = CommandLine.arguments[2]
let data = try Data(contentsOf: URL(fileURLWithPath: filename))

let testData = try JSONSerialization.jsonObject(with: data) as! [String: Any]
Expand Down
138 changes: 138 additions & 0 deletions swift/mcap/MCAPStreamedReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import struct Foundation.Data

public class MCAPStreamedReader {
private let recordReader = RecordReader()
private var chunkReader: RecordReader?
private var readHeaderMagic = false

public init() {}

public func append(_ data: Data) {
recordReader.append(data)
}

public func nextRecord() throws -> Record? {
if !readHeaderMagic {
if try !recordReader.readMagic() {
return nil
}
readHeaderMagic = true
}

if chunkReader == nil {
let record = try recordReader.nextRecord()
switch record {
case let chunk as Chunk:
chunkReader = RecordReader(chunk.records)
default:
return record
}
}

if let chunkReader = chunkReader {
defer {
if chunkReader.isDone {
self.chunkReader = nil
}
}
if let record = try chunkReader.nextRecord() {
return record
}
throw MCAPReadError.extraneousDataInChunk
}

return nil
}
}

private class RecordReader {
private var buffer: Data
private var offset = 0

init(_ data: Data = Data()) {
buffer = data
}

func append(_ data: Data) {
_trim()
buffer.append(data)
}

var isDone: Bool {
offset == buffer.count
}

private func _trim() {
buffer.removeSubrange(..<offset)
offset = 0
}

public func readMagic() throws -> Bool {
if offset + 8 < buffer.count {
if !MCAP0_MAGIC.elementsEqual(buffer[offset ..< offset + 8]) {
throw MCAPReadError.invalidMagic
}
offset += 8
return true
}
return false
}

public func nextRecord() throws -> Record? {
try buffer.withUnsafeBytes { buf in
while offset + 9 < buf.count {
let op = buf[offset]
offset += 1
var recordLength: UInt64 = 0
withUnsafeMutableBytes(of: &recordLength) { rawLength in
_ = buf.copyBytes(to: rawLength, from: offset ..< offset + 8)
}
recordLength = UInt64(littleEndian: recordLength)
offset += 8
guard offset + Int(recordLength) <= buf.count else {
return nil
}
defer {
offset += Int(recordLength)
}
guard let op = Opcode(rawValue: op) else {
continue
}
let recordBuffer = UnsafeRawBufferPointer(rebasing: buf[offset ..< offset + Int(recordLength)])
switch op {
case .header:
return try Header(deserializingFieldsFrom: recordBuffer)
case .footer:
return try Footer(deserializingFieldsFrom: recordBuffer)
case .schema:
return try Schema(deserializingFieldsFrom: recordBuffer)
case .channel:
return try Channel(deserializingFieldsFrom: recordBuffer)
case .message:
return try Message(deserializingFieldsFrom: recordBuffer)
case .chunk:
return try Chunk(deserializingFieldsFrom: recordBuffer)
case .messageIndex:
return try MessageIndex(deserializingFieldsFrom: recordBuffer)
case .chunkIndex:
return try ChunkIndex(deserializingFieldsFrom: recordBuffer)
case .attachment:
return try Attachment(deserializingFieldsFrom: recordBuffer)
case .attachmentIndex:
return try AttachmentIndex(deserializingFieldsFrom: recordBuffer)
case .statistics:
return try Statistics(deserializingFieldsFrom: recordBuffer)
case .metadata:
return try Metadata(deserializingFieldsFrom: recordBuffer)
case .metadataIndex:
return try MetadataIndex(deserializingFieldsFrom: recordBuffer)
case .summaryOffset:
return try SummaryOffset(deserializingFieldsFrom: recordBuffer)
case .dataEnd:
return try DataEnd(deserializingFieldsFrom: recordBuffer)
}
}
return nil
}
}
}
2 changes: 1 addition & 1 deletion swift/mcap/MCAPWriter.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
import struct Foundation.Data

public protocol IWritable {
func position() -> UInt64
Expand Down
Loading

0 comments on commit 0a55b20

Please sign in to comment.