Created
June 10, 2022 11:35
-
-
Save afarber/459cfeb41a55f38488201d6ce2c46e80 to your computer and use it in GitHub Desktop.
Parse numeric version - to decide which of 4 possible Protobuf parsers to apply
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
using System; | |
using System.Collections.Generic; | |
namespace MyName | |
{ | |
using static ProtobufParserBase; | |
/// <summary> | |
/// This class provides methods for analyzing a byte array and get the protobuf version out of it. | |
/// See https://developers.google.com/protocol-buffers/docs/encoding for details of protobuf encoding | |
/// </summary> | |
public static class ProtobufVersionParser | |
{ | |
/// <summary> | |
/// Gets protobuf version from a configuration request. | |
/// </summary> | |
/// <param name="configurationRequestBytes">The byte array representation of the configuration request</param> | |
/// <returns>The protobuf version of the configuration request</returns> | |
public static uint GetVersionFromConfigurationRequest(byte[] configurationRequestBytes) | |
{ | |
// TODO this method should accept ReadOnlySequence<byte> data as parameter, instead of byte array. | |
// Then no data would be copied around and Request.BodyReader.PipeReader could be used. | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
do | |
{ | |
fieldInfo = GetFieldInfo(configurationRequestBytes, ref startPos); | |
if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < configurationRequestBytes.Length && fieldInfo.FieldNumber != 5); | |
// In a configuration request, the field number of the version is always 5, regardless the protobuf version | |
return (fieldInfo.FieldNumber == 5) ? fieldInfo.Value : 0; | |
} | |
/// <summary> | |
/// Get the protobuf version from an upload request | |
/// </summary> | |
/// <param name="uploadRequestBytes">The byte array representation of the upload request</param> | |
/// <returns>The protobuf version of the upload request</returns> | |
public static uint GetVersionFromUploadRequest(byte[] uploadRequestBytes) | |
{ | |
// TODO this method should accept ReadOnlySequence<byte> data as parameter, instead of byte array | |
// Then no data would be copied around and Request.BodyReader.PipeReader could be used. | |
uint protobufVersion = 0; | |
// The data structures of the upload requests are very different in the various protobuf versions, | |
// and the version field has an individual location everywhere. | |
// So we must try one protobuf version after the other until we are successful and the returned value is != 0 | |
protobufVersion = TryGetUploadRequestVersion_Protobuf1Or2(uploadRequestBytes); | |
if (protobufVersion == 0) | |
{ | |
protobufVersion = TryGetUploadRequestVersion_Protobuf3(uploadRequestBytes); | |
} | |
if (protobufVersion == 0) | |
{ | |
protobufVersion = TryGetUploadRequestVersion_Protobuf4(uploadRequestBytes); | |
} | |
return protobufVersion; | |
} | |
/// <summary> | |
/// Tries to get the protobuf version from a byte array that represents an upload request, assuming that this request is protobuf version 1 or 2, | |
/// </summary> | |
/// <param name="uploadRequestBytes"></param> | |
/// <returns>1 or 2 if the byte array represents a protobuf version 1 0r 2 upload request. 0 otherwise.</returns> | |
private static uint TryGetUploadRequestVersion_Protobuf1Or2(byte[] uploadRequestBytes) | |
{ | |
uint protobufVersion = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadRequestBytes, ref startPos); | |
// In v1 and v2 upload requests, the field number of the version is always 7 | |
if (fieldInfo.FieldNumber == 7 && fieldInfo.WireType == WireType.VarInt) | |
{ | |
protobufVersion = fieldInfo.Value; | |
break; | |
} | |
if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < uploadRequestBytes.Length && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return (protobufVersion == 1 || protobufVersion == 2) ? protobufVersion : 0; | |
} | |
/// <summary> | |
/// Tries to get the protobuf version from a byte array that represents an upload request, assuming that this request is protobuf version 3, | |
/// </summary> | |
/// <param name="uploadRequestBytes"></param> | |
/// <returns>3 if the byte array represents a protobuf version 3 upload request. 0 otherwise.</returns> | |
private static uint TryGetUploadRequestVersion_Protobuf3(byte[] uploadRequestBytes) | |
{ | |
uint protobufVersion = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
uint targetFieldNumber; | |
fieldInfo = GetFieldInfo(uploadRequestBytes, ref startPos); | |
if (fieldInfo.WireType != WireType.LengthDelimited) | |
{ | |
return protobufVersion; | |
} | |
// In v3, the ADAS upload message is either a Ping or a Container | |
if (fieldInfo.FieldNumber == 1) | |
{ | |
// The message type is Ping. | |
// In a v3 Ping message, the field number of the version is 2 | |
targetFieldNumber = 2; | |
} | |
else if (fieldInfo.FieldNumber == 2) | |
{ | |
// The message type is a container. | |
// In a v3 container, the field number of the verion is 7 | |
targetFieldNumber = 7; | |
} | |
else | |
{ | |
return protobufVersion; | |
} | |
// Now look for the field with the required field number | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadRequestBytes, ref startPos); | |
if (fieldInfo.FieldNumber == targetFieldNumber && fieldInfo.WireType == WireType.VarInt) | |
{ | |
// We found what we have been looking for | |
protobufVersion = fieldInfo.Value; | |
break; | |
} | |
if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < uploadRequestBytes.Length && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return (protobufVersion == 3) ? protobufVersion : 0; | |
} | |
/// <summary> | |
/// Tries to get the protobuf version from a byte array that represents an upload request, assuming that this request is protobuf version 3, | |
/// </summary> | |
/// <param name="uploadRequestBytes"></param> | |
/// <returns>4 if the byte array represents a protobuf version 3 upload request. 0 otherwise.</returns> | |
private static uint TryGetUploadRequestVersion_Protobuf4(byte[] uploadRequestBytes) | |
{ | |
uint protobufVersion = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
uint targetFieldNumber; | |
fieldInfo = GetFieldInfo(uploadRequestBytes, ref startPos); | |
if (fieldInfo.WireType != WireType.LengthDelimited) | |
{ | |
return protobufVersion; | |
} | |
// If we have met a Ping (3) or Containers (4) field, in both cases the field number of the verion is 2 | |
if (fieldInfo.FieldNumber == 3 || fieldInfo.FieldNumber == 4) | |
{ | |
targetFieldNumber = 2; | |
} | |
else | |
{ | |
return protobufVersion; | |
} | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadRequestBytes, ref startPos); | |
if (fieldInfo.FieldNumber == targetFieldNumber && fieldInfo.WireType == WireType.VarInt) | |
{ | |
protobufVersion = fieldInfo.Value; | |
break; | |
} | |
if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < uploadRequestBytes.Length && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return (protobufVersion == 4) ? protobufVersion : 0; | |
} | |
} | |
/// <summary> | |
/// This class provides methods for getting the container sizes of the upload messages | |
/// </summary> | |
public static class ContainerSizeParser | |
{ | |
public static List<uint> GetContainerSizes(uint version, byte[] uploadMessageBytes) | |
{ | |
List<uint> containerSizes = null; | |
uint singleContainerSize = 0; | |
switch (version) | |
{ | |
case 1: | |
case 2: | |
singleContainerSize = GetContainerSize_V1OrV2(uploadMessageBytes); | |
containerSizes = new List<uint> { singleContainerSize }; | |
break; | |
case 3: | |
singleContainerSize = GetContainerSize_V3(uploadMessageBytes); | |
containerSizes = new List<uint> { singleContainerSize }; | |
break; | |
case 4: | |
containerSizes = GetContainerSizes_V4(uploadMessageBytes); | |
break; | |
default: | |
break; | |
} | |
return containerSizes; | |
} | |
private static uint GetContainerSize_V1OrV2(byte[] uploadMessageBytes) | |
{ | |
uint containerSize = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
if (fieldInfo.FieldNumber == 5 && fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
containerSize = fieldInfo.ValueLength; | |
break; | |
} | |
else if(fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < uploadMessageBytes.Length && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return containerSize; | |
} | |
private static uint GetContainerSize_V3(byte[] uploadMessageBytes) | |
{ | |
uint containerSize = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
if (fieldInfo.WireType != WireType.LengthDelimited) | |
{ | |
return containerSize; | |
} | |
if (fieldInfo.FieldNumber != 2) | |
{ | |
return containerSize; | |
} | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
if (fieldInfo.FieldNumber == 5 && fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
containerSize = fieldInfo.ValueLength; | |
break; | |
} | |
else if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < uploadMessageBytes.Length && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return containerSize; | |
} | |
private static List<uint> GetContainerSizes_V4(byte[] uploadMessageBytes) | |
{ | |
List<uint> containerSizes = new(); | |
uint singleContainerSize = 0; | |
FieldInfo fieldInfo; | |
uint startPos = 0; | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
if (fieldInfo.WireType != WireType.LengthDelimited) | |
{ | |
return containerSizes; | |
} | |
if (fieldInfo.FieldNumber != 4) | |
{ | |
return containerSizes; | |
} | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
} while (startPos < uploadMessageBytes.Length && fieldInfo.FieldNumber != 3 && fieldInfo.WireType != WireType.LengthDelimited); | |
if (startPos >= uploadMessageBytes.Length) | |
{ | |
return containerSizes; | |
} | |
while (startPos < uploadMessageBytes.Length && fieldInfo.FieldNumber == 3 && fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
singleContainerSize = GetsingleContainerSize_V4(uploadMessageBytes, startPos, startPos + fieldInfo.ValueLength); | |
containerSizes.Add(singleContainerSize); | |
startPos += fieldInfo.ValueLength; | |
// read the next field info for the next iteration | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
} | |
return containerSizes; | |
} | |
private static uint GetsingleContainerSize_V4(byte[] uploadMessageBytes, uint startPosition, uint endPosition) | |
{ | |
uint singleContainerSize = 0; | |
FieldInfo fieldInfo; | |
uint startPos = startPosition; | |
do | |
{ | |
fieldInfo = GetFieldInfo(uploadMessageBytes, ref startPos); | |
if (fieldInfo.FieldNumber == 5 && fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
singleContainerSize = fieldInfo.ValueLength; | |
break; | |
} | |
else if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
startPos += fieldInfo.ValueLength; | |
} | |
} while (startPos < endPosition && (fieldInfo.WireType == WireType.VarInt || fieldInfo.WireType == WireType.LengthDelimited) && fieldInfo.FieldNumber != 0); | |
return singleContainerSize; | |
} | |
} | |
/// <summary> | |
/// This class provides basic methods and data types for the parsing | |
/// </summary> | |
internal static class ProtobufParserBase | |
{ | |
/// <summary> | |
/// An enum representation of the various data in Google protobuf. | |
/// See https://developers.google.com/protocol-buffers/docs/encoding for details of protobuf encoding | |
/// </summary> | |
public enum WireType | |
{ | |
// 'Unknown' is not defined in Google protobuf specification. Just to make our life easier here. | |
Unknown = -1, | |
// The other values are defined in the protobuf spec: | |
// Numeric value with a variable number of bytes. This is the data type used for the version!!! | |
VarInt = 0, | |
// Numeric value with a fixed number of bytes. Not used for version determination. | |
Fixed64Bits, | |
// String, Byte Array, Repeated Field, Data Structure. | |
LengthDelimited, | |
// Deprecated. Not used for version determination. | |
StartGroup, | |
// Deprecated. Not used for version determination. | |
EndGroup, | |
// Numeric value with a fixed number of bytes. Not used for version determination. | |
Fixed32Bits | |
} | |
/// <summary> | |
/// The number, type and value or length of a single field in a protobuf data structure | |
/// </summary> | |
public struct FieldInfo | |
{ | |
// The data type | |
public WireType WireType { get; set; } | |
// The field number | |
public uint FieldNumber { get; set; } | |
// The length in bytes if the WireType is LenghtDelimited. 0 otherwise | |
public uint ValueLength { get; set; } | |
// The numeric value of the field if the WireType is VarInt. 0 otherwise | |
public uint Value { get; set; } | |
} | |
private static readonly uint WireTypeMax = (uint)WireType.Fixed32Bits; | |
/// <summary> | |
/// Gets the next field information from a byte array | |
/// See https://developers.google.com/protocol-buffers/docs/encoding for details of protobuf encoding | |
/// </summary> | |
/// <param name="protobufBytes">The byte array to get the field info from</param> | |
/// <param name="startPos">The position index from where to start getting the field info</param> | |
/// <returns></returns> | |
public static FieldInfo GetFieldInfo(byte[] protobufBytes, ref uint startPos) | |
{ | |
FieldInfo fieldInfo = new() { FieldNumber = 0, Value = 0, ValueLength = 0, WireType = WireType.Unknown }; | |
// A protobuf field always starts with a VarInt that contains the WireType and the field number | |
uint varInt = GetNextVarInt(protobufBytes, ref startPos); | |
// Bits 0 to 2 of the VarInt represent the WireType | |
uint wireTypeAsUInt = varInt & 0x07; | |
fieldInfo.WireType = (wireTypeAsUInt <= WireTypeMax) ? (WireType)(wireTypeAsUInt) : WireType.Unknown; | |
// The Bits 3 and higher represent the field number: | |
fieldInfo.FieldNumber = varInt >> 3; | |
if (fieldInfo.WireType == WireType.LengthDelimited) | |
{ | |
// The field we just arrived at is a complex data type, repeated field, string, ... | |
// We just return its length. | |
// The caller then can skip the field, or step into it. | |
fieldInfo.ValueLength = GetNextVarInt(protobufBytes, ref startPos); | |
} | |
else | |
{ | |
// The field we just arrived at is a numeric value | |
fieldInfo.Value = GetNextVarInt(protobufBytes, ref startPos); | |
} | |
return fieldInfo; | |
} | |
/// <summary> | |
/// Gets the next Base128 VarInt value from an array of bytes. | |
/// See https://developers.google.com/protocol-buffers/docs/encoding for details of protobuf encoding | |
/// </summary> | |
/// <param name="protobufBytes">The byte array from which to get the VarInt</param> | |
/// <param name="startPos">The position index where to start getting the VarInt</param> | |
/// <returns></returns> | |
private static uint GetNextVarInt(byte[] protobufBytes, ref uint startPos) | |
{ | |
uint varInt = 0; | |
List<byte> varIntBytes = new(); | |
bool varIntFound = false; | |
do | |
{ | |
varIntBytes.Add(protobufBytes[startPos]); | |
// The most significant bit of the byte we just read is not a part of the numeric value. We must reset it. | |
varIntBytes[varIntBytes.Count - 1] &= 0x7F; | |
if ((protobufBytes[startPos] & 0x80) == 0) | |
{ | |
// MSB is 0, which indicates that we found the last byte that is part of the VarInt | |
startPos++; | |
varIntFound = true; | |
break; | |
} | |
startPos++; | |
} while (startPos < protobufBytes.Length); | |
if (!varIntFound) | |
{ | |
// Something is wrong! | |
// We reached the end of the array, but the byte there was not the last one of a VarInt. | |
// We will return a value that will most probably be invalid for the caller. | |
// For example, if the returned value is taken as a FieldInfo, the resulting WireType will be an invalid value (bits 0-2 are 111, which is 7, but only 0-5 are valid). | |
return UInt32.MaxValue; | |
} | |
// Now as we have collected all the bytes belonging to the VarInt, the next step is to make a numeric value of them. | |
// Remember: protobuf VarInt values are based on 128, because the MSB is used to indicate if more bytes are following! | |
uint powerOf128 = 1; | |
for (int i = 0; i < varIntBytes.Count; ++i) | |
{ | |
varInt += varIntBytes[i] * powerOf128; | |
powerOf128 *= 128; | |
} | |
return varInt; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment