Skip to content

Commit f6f9a7b

Browse files
abradymeta-codesync[bot]
authored andcommitted
Add binary protocol reader/writer
Summary: Add binary protocol reader and writer implementing IThriftProtocolReader/IThriftProtocolWriter interfaces. Binary protocol uses big-endian encoding for all multi-byte values with strict UTF-8 validation, configurable string size limits, and stream-bounds checking. Includes comprehensive round-trip and wire encoding tests. Reviewed By: vitaut Differential Revision: D94941580 fbshipit-source-id: 1d935a4e76a3059b258880f180180b2891915960
1 parent 192d29c commit f6f9a7b

3 files changed

Lines changed: 1579 additions & 0 deletions

File tree

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Buffers;
19+
using System.Buffers.Binary;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
using System.Text;
23+
24+
namespace FBThrift
25+
{
26+
/// <summary>
27+
/// Reads Thrift data types using the binary protocol format.
28+
/// All multi-byte values are read in big-endian (network) byte order.
29+
/// </summary>
30+
public class ThriftBinaryReader : IThriftProtocolReader
31+
{
32+
private readonly Stream _stream;
33+
private readonly byte[] _buffer = new byte[8];
34+
35+
/// <summary>
36+
/// Strict UTF-8 encoding that throws on invalid byte sequences instead of
37+
/// silently replacing them with U+FFFD.
38+
/// </summary>
39+
private static readonly Encoding StrictUtf8 = new UTF8Encoding(
40+
encoderShouldEmitUTF8Identifier: false,
41+
throwOnInvalidBytes: true);
42+
43+
44+
public ThriftBinaryReader(Stream stream)
45+
{
46+
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
47+
}
48+
49+
/// <summary>
50+
/// Gets or sets the maximum allowed size for string and binary reads.
51+
/// When set to 0 (default), no configurable limit is enforced, but reads
52+
/// are still validated against remaining stream bytes.
53+
/// Mirrors the C++ thrift_cpp2_protocol_reader_string_limit gflag behavior.
54+
/// </summary>
55+
public int StringSizeLimit { get; set; }
56+
57+
/// <summary>
58+
/// Gets the underlying stream.
59+
/// </summary>
60+
public Stream BaseStream => _stream;
61+
62+
/// <summary>
63+
/// Gets the number of bytes remaining in the stream.
64+
/// Returns long.MaxValue for non-seekable streams.
65+
/// </summary>
66+
public long RemainingBytes => _stream.CanSeek
67+
? _stream.Length - _stream.Position
68+
: long.MaxValue;
69+
70+
/// <summary>
71+
/// Reads a field header and returns the field type and field ID.
72+
/// </summary>
73+
/// <returns>A tuple of (fieldType, fieldId). fieldType will be ThriftWireType.Stop (0) at end of struct.</returns>
74+
public (ThriftWireType fieldType, short fieldId) ReadFieldBegin()
75+
{
76+
var rawType = (byte)ReadByte();
77+
if (rawType == (byte)ThriftWireType.Stop)
78+
{
79+
return (ThriftWireType.Stop, 0);
80+
}
81+
if (!IsValidWireType(rawType))
82+
{
83+
throw new ThriftProtocolException(
84+
$"Unknown field type {rawType} at stream position {(_stream.CanSeek ? _stream.Position.ToString() : "unknown")}");
85+
}
86+
var fieldType = (ThriftWireType)rawType;
87+
var fieldId = ReadI16();
88+
return (fieldType, fieldId);
89+
}
90+
91+
/// <summary>
92+
/// Reads a boolean value (1 byte: 0x01 for true, 0x00 for false).
93+
/// </summary>
94+
public bool ReadBool()
95+
{
96+
var b = _stream.ReadByte();
97+
if (b < 0)
98+
{
99+
throw new EndOfStreamException("Unexpected end of stream while reading bool");
100+
}
101+
if (b != 0 && b != 1)
102+
{
103+
throw new ThriftProtocolException(
104+
$"Invalid bool value: 0x{b:X2} (expected 0x00 or 0x01)");
105+
}
106+
return b == 1;
107+
}
108+
109+
/// <summary>
110+
/// Reads a signed byte value.
111+
/// </summary>
112+
public sbyte ReadByte()
113+
{
114+
var b = _stream.ReadByte();
115+
if (b < 0)
116+
{
117+
throw new EndOfStreamException("Unexpected end of stream while reading byte");
118+
}
119+
return (sbyte)b;
120+
}
121+
122+
/// <summary>
123+
/// Reads a 16-bit signed integer in big-endian format.
124+
/// </summary>
125+
public short ReadI16()
126+
{
127+
ReadExact(_buffer, 2);
128+
return BinaryPrimitives.ReadInt16BigEndian(_buffer);
129+
}
130+
131+
/// <summary>
132+
/// Reads a 32-bit signed integer in big-endian format.
133+
/// </summary>
134+
public int ReadI32()
135+
{
136+
ReadExact(_buffer, 4);
137+
return BinaryPrimitives.ReadInt32BigEndian(_buffer);
138+
}
139+
140+
/// <summary>
141+
/// Reads a 64-bit signed integer in big-endian format.
142+
/// </summary>
143+
public long ReadI64()
144+
{
145+
ReadExact(_buffer, 8);
146+
return BinaryPrimitives.ReadInt64BigEndian(_buffer);
147+
}
148+
149+
/// <summary>
150+
/// Reads a 32-bit floating point value in big-endian format.
151+
/// </summary>
152+
public float ReadFloat()
153+
{
154+
var intBits = ReadI32();
155+
return BitConverter.Int32BitsToSingle(intBits);
156+
}
157+
158+
/// <summary>
159+
/// Reads a 64-bit floating point value in big-endian format.
160+
/// </summary>
161+
public double ReadDouble()
162+
{
163+
var longBits = ReadI64();
164+
return BitConverter.Int64BitsToDouble(longBits);
165+
}
166+
167+
/// <summary>
168+
/// Reads a string as a length-prefixed UTF-8 byte sequence.
169+
/// </summary>
170+
public string ReadString()
171+
{
172+
var length = ReadI32();
173+
CheckStringSize(length);
174+
if (length == 0)
175+
{
176+
return string.Empty;
177+
}
178+
179+
var rented = ArrayPool<byte>.Shared.Rent(length);
180+
try
181+
{
182+
ReadExact(rented, length);
183+
return StrictUtf8.GetString(rented, 0, length);
184+
}
185+
finally
186+
{
187+
ArrayPool<byte>.Shared.Return(rented);
188+
}
189+
}
190+
191+
/// <summary>
192+
/// Reads binary data as a length-prefixed byte sequence.
193+
/// </summary>
194+
public byte[] ReadBinary()
195+
{
196+
var length = ReadI32();
197+
CheckStringSize(length);
198+
if (length == 0)
199+
{
200+
return Array.Empty<byte>();
201+
}
202+
203+
var bytes = new byte[length];
204+
ReadExact(bytes, length);
205+
return bytes;
206+
}
207+
208+
/// <summary>
209+
/// Reads a list header and returns the element type and size.
210+
/// </summary>
211+
public (ThriftWireType elemType, int size) ReadListBegin()
212+
{
213+
var b = _stream.ReadByte();
214+
if (b < 0)
215+
{
216+
throw new EndOfStreamException("Unexpected end of stream while reading list element type");
217+
}
218+
var elemType = (ThriftWireType)b;
219+
var size = ReadI32();
220+
ValidateCollectionSize(size);
221+
return (elemType, size);
222+
}
223+
224+
/// <summary>
225+
/// Reads a set header and returns the element type and size.
226+
/// </summary>
227+
public (ThriftWireType elemType, int size) ReadSetBegin()
228+
{
229+
return ReadListBegin();
230+
}
231+
232+
/// <summary>
233+
/// Reads a map header and returns the key type, value type, and size.
234+
/// </summary>
235+
public (ThriftWireType keyType, ThriftWireType valType, int size) ReadMapBegin()
236+
{
237+
var b1 = _stream.ReadByte();
238+
if (b1 < 0)
239+
{
240+
throw new EndOfStreamException("Unexpected end of stream while reading map key type");
241+
}
242+
var keyType = (ThriftWireType)b1;
243+
var b2 = _stream.ReadByte();
244+
if (b2 < 0)
245+
{
246+
throw new EndOfStreamException("Unexpected end of stream while reading map value type");
247+
}
248+
var valType = (ThriftWireType)b2;
249+
var size = ReadI32();
250+
ValidateCollectionSize(size, minBytesPerElement: 2);
251+
return (keyType, valType, size);
252+
}
253+
254+
/// <summary>
255+
/// Reads a struct value by creating a new instance and calling its Read method.
256+
/// </summary>
257+
public T ReadStruct<T>() where T : IThriftSerializable, new()
258+
{
259+
var result = new T();
260+
result.Read(this);
261+
return result;
262+
}
263+
264+
/// <summary>
265+
/// Reads a set of values.
266+
/// </summary>
267+
public HashSet<T> ReadSet<T>()
268+
{
269+
var (_, size) = ReadSetBegin();
270+
var result = new HashSet<T>(size);
271+
for (var i = 0; i < size; i++)
272+
{
273+
result.Add(ReadValue<T>());
274+
}
275+
return result;
276+
}
277+
278+
/// <summary>
279+
/// Reads a list of values.
280+
/// </summary>
281+
public List<T> ReadList<T>()
282+
{
283+
var (_, size) = ReadListBegin();
284+
var result = new List<T>(size);
285+
for (var i = 0; i < size; i++)
286+
{
287+
result.Add(ReadValue<T>());
288+
}
289+
return result;
290+
}
291+
292+
/// <summary>
293+
/// Reads a map of key-value pairs.
294+
/// </summary>
295+
public Dictionary<K, V> ReadMap<K, V>()
296+
{
297+
var (_, _, size) = ReadMapBegin();
298+
var result = new Dictionary<K, V>(size);
299+
for (var i = 0; i < size; i++)
300+
{
301+
var key = ReadValue<K>();
302+
var val = ReadValue<V>();
303+
result[key] = val;
304+
}
305+
return result;
306+
}
307+
308+
/// <summary>
309+
/// Skips a value of the given type.
310+
/// </summary>
311+
public void Skip(ThriftWireType fieldType, short? fieldId = null) => ThriftProtocolHelper.Skip(this, fieldType, fieldId);
312+
313+
/// <summary>
314+
/// Generic method to read any supported value type.
315+
/// </summary>
316+
public T ReadValue<T>() => ThriftProtocolHelper.ReadValue<T>(this);
317+
318+
private void ReadExact(byte[] buffer, int count)
319+
{
320+
var offset = 0;
321+
while (offset < count)
322+
{
323+
var bytesRead = _stream.Read(buffer, offset, count - offset);
324+
if (bytesRead == 0)
325+
{
326+
throw new EndOfStreamException($"Unexpected end of stream, expected {count} bytes but got {offset}");
327+
}
328+
offset += bytesRead;
329+
}
330+
}
331+
332+
private void CheckStringSize(int size)
333+
{
334+
if (size < 0)
335+
{
336+
throw new ThriftProtocolException($"Negative string/binary length: {size}");
337+
}
338+
if (StringSizeLimit > 0 && size > StringSizeLimit)
339+
{
340+
throw new ThriftProtocolException(
341+
$"String/binary length {size} exceeds size limit {StringSizeLimit}");
342+
}
343+
if (size > RemainingBytes)
344+
{
345+
throw new ThriftProtocolException(
346+
$"String/binary length {size} exceeds remaining bytes {RemainingBytes}");
347+
}
348+
}
349+
350+
private void ValidateCollectionSize(int size, int minBytesPerElement = 1)
351+
{
352+
if (size < 0)
353+
{
354+
throw new ThriftProtocolException($"Negative collection size: {size}");
355+
}
356+
var minRequired = (long)size * minBytesPerElement;
357+
if (minRequired > RemainingBytes)
358+
{
359+
throw new ThriftProtocolException(
360+
$"Collection size {size} exceeds remaining bytes {RemainingBytes}");
361+
}
362+
}
363+
364+
private static bool IsValidWireType(byte value) => Enum.IsDefined(typeof(ThriftWireType), value);
365+
}
366+
}

0 commit comments

Comments
 (0)