Sources/TwitterApacheThrift/ThriftCompactBinary.swift (145 lines of code) (raw):
// Copyright 2021 Twitter, Inc.
// Licensed under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// ThriftBinary.swift
// TwitterApacheThrift
//
// Created on 9/23/21.
// Copyright © 2021 Twitter. All rights reserved.
//
import CoreFoundation
import Foundation
/// A class for reading values from thrift data
class ThriftCompactBinary: ThriftBinary {
override func readStruct(index: Int?) throws -> ThriftStruct {
var fields: [Int: ThriftValue] = [:]
var nextField = try readFieldMetadata(previousId: 0)
while nextField.type != .stop, let id = nextField.id {
let value = try readValue(index: id, type: nextField.type, isCollection: false)
fields[id] = ThriftValue(index: id, type: nextField.type, data: value)
nextField = try readFieldMetadata(previousId: id)
}
return ThriftStruct(index: index, fields: fields)
}
override func readValue(index: Int?, type: ThriftType, isCollection: Bool = false) throws -> ThriftObject {
switch type {
case .void: //Void is boolean true in compact thrift when used as a field.
return isCollection ? .stop : .data(Data([1]))
case .bool: //bool is boolean false in compact thrift when used as a field but is one byte when used in a collection
return isCollection ? .data(try Data([readByte()])) : .data(Data([0]))
case .byte:
return .data(try Data([readByte()]))
case .double:
return .data(try readingBuffer.read(size: 8))
case .int64, .int16, .int32:
return .data(try Data(unsignedLEBBytes()))
case .string:
return .data(try readBinary())
case .structure:
return .struct(try readStruct(index: index))
case .map:
var values: [ThriftKeyedCollection.Value] = []
let metadata = try readMapMetadata()
for _ in 0..<metadata.size {
let key = try readValue(index: nil, type: metadata.keyType, isCollection: true)
let value = try readValue(index: nil, type: metadata.valueType, isCollection: true)
values.append(.init(key: key, value: value))
}
return .keyedCollection(ThriftKeyedCollection(index: index,
count: metadata.size,
keyType:metadata.keyType,
elementType: metadata.valueType,
value: values))
case .list, .set:
var values: [ThriftObject] = []
let metadata = try readListMetadata()
for _ in 0..<metadata.size {
let value = try readValue(index: nil, type: metadata.elementType, isCollection: true)
values.append(value)
}
return .unkeyedCollection(ThriftUnkeyedCollection(index: index, count: metadata.size, elementType: type, value: values))
default:
return .stop
}
}
/// Reads a the field metadata from the thrift. This type is equivalent to a Dictionary
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The field thrift type. The thrift id if it is not a ThriftType.stop.
func readFieldMetadata(previousId: Int) throws -> (type: ThriftType, id: Int?) {
let binary = try readByte()
if binary == 0 {
return (.stop, nil)
}
let fieldIdDelta = UInt8((binary & 0xF0) >> 4)
let fieldType = UInt8(binary & 0x0F)
let type = try ThriftType(compactValue: fieldType)
if fieldIdDelta == 0 {
let fieldId = try Int16(zigZag: super.readInt16())
return (type, Int(fieldId))
}
return (type, Int(fieldIdDelta) + previousId)
}
override func readDouble() throws -> Double {
let buffer = try readingBuffer.read(size: 8)
let i64: UInt64 = buffer.withUnsafeBytes { $0.load(as: UInt64.self) }
let value = CFSwapInt64LittleToHost(i64)
return Double(bitPattern: value)
}
/// Reads the next UInt64 from the thrift
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The value decoded from the thrift
override func readUInt64() throws -> UInt64 {
let value: Int64 = decodeUnsignedLEB(from: readingBuffer.buffer)
return CFSwapInt64LittleToHost(UInt64(zigZag: value))
}
/// Reads the next Int64 from the thrift
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The value decoded from the thrift
override func readInt64() throws -> Int64 {
let value: UInt64 = decodeUnsignedLEB(from: readingBuffer.buffer)
return (Int64(zigZag: value))
}
/// Reads the next Int32 from the thrift
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The value decoded from the thrift
override func readInt32() throws -> Int32 {
let value: Int32 = decodeUnsignedLEB(from: readingBuffer.buffer)
return (Int32(zigZag: value))
}
/// Reads the next Int16 from the thrift
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The value decoded from the thrift
override func readInt16() throws -> Int16 {
let value = try readInt32()
return Int16(value)
}
/// Reads the next data from the thrift. The length of data is based on the next 4 bytes.
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The value decoded from the thrift
override func readBinary() throws -> Data {
let bytes = try unsignedLEBBytes()
let size: Int32 = decodeUnsignedLEB(from: bytes)
return try readingBuffer.read(size: Int(size))
}
/// Reads a map object metadata from the thrift. This type is equivalent to a Dictionary
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The type of key as a thrift type. The type of values as a thrift type. The amount of elements.
override func readMapMetadata() throws -> (keyType: ThriftType, valueType: ThriftType, size: Int) {
let binary = try readByte()
if binary == 0 {
//Empty map
return (.stop, .stop, 0)
}
let sizeBytes = try unsignedLEBBytes(startingByte: binary)
let size: Int32 = decodeUnsignedLEB(from: sizeBytes)
let types = try readByte()
let keyType = UInt8(types >> 4) & 0x0F
let elementType = UInt8(types & 0x0F)
return (try ThriftType(compactValue: keyType), try ThriftType(compactValue: elementType), Int(size))
}
/// Reads a list object metadata from the thrift. This type is equivalent to a Array
/// - Throws: ThriftDecoderError.readBufferOverflow when trying to read outside the range of data
/// - Returns: The type of values as a thrift type. The amount of elements.
override func readListMetadata() throws -> (elementType: ThriftType, size: Int) {
let binary = try readByte()
let compactSize = UInt8(binary >> 4) & 0x0F
let elementType = UInt8(binary & 0x0F)
let type = try ThriftType(compactValue: elementType)
//If size is 15 (1111) then it uses a different format
if compactSize == 0b1111 {
let sizeBytes = try unsignedLEBBytes()
let size: Int32 = decodeUnsignedLEB(from: sizeBytes)
return (type, Int(size))
}
return (type, Int(compactSize))
}
func unsignedLEBBytes(startingByte: UInt8? = nil) throws -> Data {
var bytes: [UInt8] = []
while true {
let byte: UInt8
if let firstByte = startingByte, bytes.isEmpty {
byte = firstByte
} else {
byte = try readByte()
}
bytes.append(byte)
if (byte & 0x80) == 0 {
break
}
}
return Data(bytes)
}
func decodeUnsignedLEB<T: BinaryInteger>(from bytes: Data) -> T {
var result: T = 0
var shift: T = 0
for byte in bytes {
result |= ((T(byte) & 0x7F) << shift)
shift += 7
}
return result
}
}