forked from foxglove/mcap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Swift streamed reader (foxglove#348)
**Public-Facing Changes** Adds `MCAPStreamedReader` class and conformance test runner for Swift.
- Loading branch information
Showing
15 changed files
with
548 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,3 +109,4 @@ overrides: | |
words: | ||
- subrange | ||
- unkeyed | ||
- lowercased |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] ...") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Oops, something went wrong.