Datové části pro čtení BinaryFormatter (NRBF)
BinaryFormatterpoužití vzdálené komunikace .NET: Binární formát pro serialization. Tento formát je znám zkratkou MS-NRBF nebo jen NRBF. Běžným problémem při migraci z BinaryFormatter této migrace je zpracování datových částí, které se uchovávají v úložišti, protože čtení těchto datových částí bylo dříve požadováno BinaryFormatter. Některé systémy si musí zachovat schopnost číst tyto datové části pro postupné migrace do nových serializátorů a zároveň se vyhnout odkazu na BinaryFormatter sebe sama.
V rámci .NET 9 byla zavedena nová třída NrbfDecoder pro dekódování datových částí NRBF bez provedení deserializace datové části. Toto rozhraní API se dá bezpečně použít k dekódování důvěryhodných nebo nedůvěryhodných datových částí bez jakýchkoli rizik, která BinaryFormatter deserializace nese. NrbfDecoder však pouze dekóduje data do struktur, které může aplikace dále zpracovat. Při použití nástroje NrbfDecoder je potřeba věnovat pozornost bezpečnému načtení dat do příslušných instancí.
Můžete si představit NrbfDecoder , že je ekvivalentem použití čtečky JSON/XML bez deserializátoru.
NrbfDecoder
NrbfDecoder je součástí nového balíčku NuGet System.Formats.Nrbf . Cílí nejen na .NET 9, ale také na starší monikery, jako jsou .NET Standard 2.0 a .NET Framework. Toto cílení na více verzí umožňuje všem uživatelům, kteří používají podporovanou verzi rozhraní .NET, migrovat mimo BinaryFormatter. NrbfDecoder může číst datové části, které byly serializovány pomocí BinaryFormatter použití FormatterTypeStyle.TypesAlways (výchozí).
NrbfDecoder je navržený tak, aby se všemi vstupy zacházet jako s nedůvěryhodnými. Jako takový má tyto zásady:
- Žádné načítání typu (abyste se vyhnuli rizikům, jako je vzdálené spuštění kódu).
- Žádná rekurze jakéhokoli druhu (abyste se vyhnuli nevázaným rekurzím, přetečení zásobníku a odepření služby).
- Pokud je datová část příliš malá na to, aby obsahovala slíbená data (aby nedocházelo k nedostatku paměti a odepření služby), není předběžné přidělení vyrovnávací paměti založené na velikosti zadané v datové části.
- Dekódujte všechny části vstupu jenom jednou (aby bylo možné provést stejnou práci jako potenciální útočník, který datovou část vytvořil).
- Pomocí randomizovaného hash odolného proti kolizi můžete ukládat záznamy odkazované jinými záznamy (abyste se vyhnuli nedostatku paměti pro slovník zálohovaný polem, jehož velikost závisí na počtu kolizí kódu hash).
- Implicitně lze vytvořit instance pouze primitivních typů. Pole je možné vytvořit instanci na vyžádání. Jiné typy se nikdy nedají vytvořit instance.
Pokud používáte NrbfDecoder, je důležité tyto funkce v kódu pro obecné účely znovu nezavést, protože by tak negovaly tyto záruky.
Deserializace uzavřené sady typů
NrbfDecoder je užitečné pouze v případě, že seznam serializovaných typů je známá uzavřená sada. Abyste to udělali jiným způsobem, musíte předem vědět, co chcete číst, protože potřebujete také vytvořit instance těchto typů a naplnit je daty, která byla načtena z datové části. Zvažte dva opačné příklady:
- Všechny
[Serializable]
typy z Quartz.NET , které lze zachovat v samotné knihovně, jsousealed
. Neexistují tedy žádné vlastní typy, které by uživatelé mohli vytvořit, a datová část může obsahovat pouze známé typy. Typy také poskytují veřejné konstruktory, takže je možné tyto typy vytvořit znovu na základě informací načtených z datové části. - Typ SettingsPropertyValue zveřejňuje vlastnost PropertyValue typu
object
, která může interně použít BinaryFormatter k serializaci a deserializaci libovolného objektu, který byl uložen v konfiguračním souboru. Dá se použít k uložení celého čísla, vlastního typu, slovníku nebo doslova čehokoli. Z tohoto důvodu není možné migrovat tuto knihovnu, aniž byste do rozhraní API zavedli zásadní změny.
Identifikace datových částí NRBF
NrbfDecoder poskytuje dvě StartsWithPayloadHeader metody, které umožňují zkontrolovat, jestli daný datový proud nebo vyrovnávací paměť začíná hlavičkou NRBF. Při migraci datových částí trvalých BinaryFormatter na jiný serializátor se doporučuje použít tyto metody:
- Zkontrolujte, jestli datová část čtení z úložiště představuje datovou část NRBF.NrbfDecoder.StartsWithPayloadHeader
- Pokud ano, přečtěte si ji NrbfDecoder.Decode, serializovat ji zpět pomocí nového serializátoru a přepsat data v úložišti.
- Pokud ne, použijte nový serializátor k deserializaci dat.
internal static T LoadFromFile<T>(string path)
{
bool update = false;
T value;
using (FileStream stream = File.OpenRead(path))
{
if (NrbfDecoder.StartsWithPayloadHeader(stream))
{
value = LoadLegacyValue<T>(stream);
update = true;
}
else
{
value = LoadNewValue<T>(stream);
}
}
if (update)
{
File.WriteAllBytes(path, NewSerializer(value));
}
return value;
}
Bezpečné čtení datových částí NRBF
Datová část NRBF se skládá ze serialization záznamů, které představují serializované objekty a jejich metadata. Pokud chcete přečíst celou datovou část a získat kořenový objekt, musíte metodu Decode volat.
Metoda Decode vrátí SerializationRecord instanci. SerializationRecord je abstraktní třída, která představuje serialization záznam a poskytuje tři vlastnosti popisující sebe: Id, RecordTypea TypeName. Zveřejňuje jednu metodu, TypeNameMatcheskterá porovnává název typu načtený z datové části (a vystavený prostřednictvím TypeName vlastnosti) se zadaným typem. Tato metoda ignoruje názvy sestavení, takže se uživatelé nemusí starat o předávání typů a správu verzí sestavení. Také nebere v úvahu názvy členů ani jejich typy (protože získání těchto informací by vyžadovalo načtení typu).
using System.Formats.Nrbf;
static T Pseudocode<T>(Stream payload)
{
SerializationRecord record = NrbfDecoder.Read(payload);
if (!record.TypeNameMatches(typeof(T))
{
throw new Exception($"Expected the record to match type name `{typeof(T).AssemblyQualifiedName}`, but got `{record.TypeName.AssemblyQualifiedName}`."
}
}
Existuje více než deset různých serializationtypů záznamů. Tato knihovna poskytuje sadu abstrakcí, takže se potřebujete naučit jen několik z nich:
- PrimitiveTypeRecord<T>: popisuje všechny primitivní typy nativně podporované NRBF (
string
, , ,byte
,char
sbyte
,short
,ushort
,int
,uint
,long
,ulong
,float
,double
,decimal
TimeSpan
, aDateTime
).bool
- Zpřístupňuje hodnotu prostřednictvím
Value
vlastnosti. - PrimitiveTypeRecord<T> je odvozena z negenerické PrimitiveTypeRecord, která také zveřejňuje Value vlastnost. Ale u základní třídy se hodnota vrátí jako
object
(která zavádí boxování pro typy hodnot).
- Zpřístupňuje hodnotu prostřednictvím
- ClassRecord: popisuje všechny
class
astruct
kromě výše uvedených primitivních typů. - ArrayRecord: popisuje všechny záznamy polí, včetně jagged a multidimenzionálních polí.
- SZArrayRecord<T>: popisuje jednorozměrné záznamy pole s nulovým indexem, kde
T
mohou být buď primitivním typem, nebo .ClassRecord
SerializationRecord rootObject = NrbfDecoder.Decode(payload); // payload is a Stream
if (rootObject is PrimitiveTypeRecord primitiveRecord)
{
Console.WriteLine($"It was a primitive value: '{primitiveRecord.Value}'");
}
else if (rootObject is ClassRecord classRecord)
{
Console.WriteLine($"It was a class record of '{classRecord.TypeName.AssemblyQualifiedName}' type name.");
}
else if (rootObject is SZArrayRecord<byte> arrayOfBytes)
{
Console.WriteLine($"It was an array of `{arrayOfBytes.Length}`-many bytes.");
}
Kromě DecodeNrbfDecoder zveřejňuje metoduDecodeClassRecord, která vrací ClassRecord (nebo vyvolává).
Záznam třídy
Nejdůležitějším typem odvozený SerializationRecord je ClassRecord, který představuje všechny class
instance vedle polí a struct
nativně podporovaných primitivních typů. Umožňuje číst všechna jména a hodnoty členů. Informace o členech najdete v referenčních informacích k funkcímBinaryFormatter.
Rozhraní API, které poskytuje:
- MemberNames vlastnost, která získá názvy serializovaných členů.
- HasMember metoda, která kontroluje, zda člen daného názvu byl přítomn v datové části. Byl navržen pro zpracování scénářů správy verzí, kdy byl daný člen přejmenován.
- Sada vyhrazených metod pro načtení primitivních hodnot zadaného názvu člena: GetString, , , GetByte, GetSByteGetChar, GetInt16, GetUInt16, GetInt32, GetDecimalGetSingleGetTimeSpanGetUInt32GetInt64GetUInt64GetDoublea .GetDateTimeGetBoolean
- GetClassRecord a GetArrayRecord metody pro načtení instance daných typů záznamů.
- GetSerializationRecord načtení libovolného serialization záznamu a GetRawValue načtení jakéhokoli serialization záznamu nebo nezpracované primitivní hodnoty.
Následující fragment kódu se zobrazí ClassRecord v akci:
[Serializable]
public class Sample
{
public int Integer;
public string? Text;
public byte[]? ArrayOfBytes;
public Sample? ClassInstance;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
Sample output = new()
{
// using the dedicated methods to read primitive values
Integer = rootRecord.GetInt32(nameof(Sample.Integer)),
Text = rootRecord.GetString(nameof(Sample.Text)),
// using dedicated method to read an array of bytes
ArrayOfBytes = ((SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(Sample.ArrayOfBytes))).GetArray(),
// using GetClassRecord to read a class record
ClassInstance = new()
{
Text = rootRecord
.GetClassRecord(nameof(Sample.ClassInstance))!
.GetString(nameof(Sample.Text))
}
};
Maticovýzáznam
ArrayRecord definuje základní chování pro záznamy polí NRBF a poskytuje základ pro odvozené třídy. Poskytuje dvě vlastnosti:
- Rank který získá pořadí pole.
- Lengths které získají vyrovnávací paměť celých čísel, které představují počet prvků v každé dimenzi.
Poskytuje také jednu metodu: GetArray. Při prvním použití přidělí pole a vyplní je daty zadanými v serializovaných záznamech (v případě nativně podporovaných primitivních typů jako string
nebo) nebo int
serializovaných záznamů samotných (v případě polí komplexních typů).
GetArray vyžaduje povinný argument, který určuje typ očekávaného pole. Pokud by například záznam měl být 2D pole celých čísel, expectedArrayType
musí být zadáno jako typeof(int[,])
a vrácená matice je také int[,]
:
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(stream);
int[,] array2d = (int[,])arrayRecord.GetArray(typeof(int[,]));
Pokud dojde k neshodě typu (příklad: útočník poskytl datovou část s polem dvou miliard řetězců), metoda vyvolá InvalidOperationException.
NrbfDecoder nenačítá nebo vytvoří instanci žádné vlastní typy, takže v případě polí komplexních typů vrátí pole SerializationRecord.
[Serializable]
public class ComplexType3D
{
public int I, J, K;
}
ArrayRecord arrayRecord = (ArrayRecord)NrbfDecoder.Decode(payload);
SerializationRecord[] records = (SerializationRecord[])arrayRecord.GetArray(expectedArrayType: typeof(ComplexType3D[]));
ComplexType3D[] output = records.OfType<ClassRecord>().Select(classRecord => new ComplexType3D()
{
I = classRecord.GetInt32(nameof(ComplexType3D.I)),
J = classRecord.GetInt32(nameof(ComplexType3D.J)),
K = classRecord.GetInt32(nameof(ComplexType3D.K)),
}).ToArray();
Rozhraní .NET Framework podporovalo nenulové indexované pole v datových částech NRBF, ale tato podpora nebyla nikdy portována do .NET (Core). NrbfDecoder proto nepodporuje dekódování nenulových indexovaných polí.
SZArrayRecord
SZArrayRecord<T>
definuje základní chování pro jednorozměrné záznamy pole NRBF s nulovým indexem a poskytuje základ pro odvozené třídy. Může T
to být jeden z nativně podporovaných primitivních typů nebo SerializationRecord.
Length Poskytuje vlastnost a GetArray přetížení, které vrací T[]
.
[Serializable]
public class PrimitiveArrayFields
{
public byte[]? Bytes;
public uint[]? UnsignedIntegers;
}
ClassRecord rootRecord = NrbfDecoder.DecodeClassRecord(payload);
SZArrayRecord<byte> bytes = (SZArrayRecord<byte>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.Bytes));
SZArrayRecord<uint> uints = (SZArrayRecord<uint>)rootRecord.GetArrayRecord(nameof(PrimitiveArrayFields.UnsignedIntegers));
if (bytes.Length > 100_000 || uints.Length > 100_000)
{
throw new Exception("The array exceeded our limit");
}
PrimitiveArrayFields output = new()
{
Bytes = bytes.GetArray(),
UnsignedIntegers = uints.GetArray()
};