Skip to content

Instantly share code, notes, and snippets.

@afarber
Created June 10, 2022 11:35
Show Gist options
  • Save afarber/459cfeb41a55f38488201d6ce2c46e80 to your computer and use it in GitHub Desktop.
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
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