Skip to content

Commit f26f218

Browse files
feat: Support read-only spans
1 parent d4207da commit f26f218

2 files changed

Lines changed: 78 additions & 35 deletions

File tree

src/D2SSharp/Model/D2SaveOverlay.cs

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,21 @@ public static ref D2SaveLayout From(Span<byte> data)
595595
return ref layout;
596596
}
597597

598+
/// <summary>
599+
/// Gets a read-only reference to the layout from a read-only byte span.
600+
/// Throws InvalidDataException if magic values are invalid or version is >= 104.
601+
/// For v104+ saves, use <see cref="D2SaveLayoutV104.FromReadOnly(ReadOnlySpan{byte})"/> instead.
602+
/// </summary>
603+
public static ref readonly D2SaveLayout FromReadOnly(ReadOnlySpan<byte> data)
604+
{
605+
ref readonly var layout = ref MemoryMarshal.AsRef<D2SaveLayout>(data);
606+
if (!layout.IsValid)
607+
throw new InvalidDataException("Invalid D2 save file: magic values do not match.");
608+
if (layout.Header.Version >= 104)
609+
throw new InvalidDataException("Save version >= 104 uses a different header layout. Use D2SaveLayoutV104.FromReadOnly() instead.");
610+
return ref layout;
611+
}
612+
598613
/// <summary>
599614
/// Updates the checksum in the provided data buffer.
600615
/// </summary>
@@ -652,44 +667,20 @@ public static ref D2SaveLayoutV104 From(Span<byte> data)
652667
}
653668

654669
/// <summary>
655-
/// Updates the checksum in the provided data buffer.
656-
/// </summary>
657-
public static void UpdateChecksum(Span<byte> data)
658-
=> ChecksumCalculator.Update(data);
659-
}
660-
661-
/// <summary>
662-
/// Blittable layout for the fixed 64-byte header of a shared stash tab.
663-
/// Provides zero-copy access to tab header fields without parsing items.
664-
/// </summary>
665-
[StructLayout(LayoutKind.Explicit, Size = Size)]
666-
public unsafe struct D2StashTabLayout
667-
{
668-
/// <summary>Size of the stash tab header in bytes.</summary>
669-
public const int Size = 64;
670-
671-
[FieldOffset(0)] public uint Magic;
672-
[FieldOffset(4)] public uint StashFormat;
673-
[FieldOffset(8)] public uint ItemFormat;
674-
[FieldOffset(12)] public uint Gold;
675-
[FieldOffset(16)] public ushort TabSize;
676-
[FieldOffset(18)] public ushort Season;
677-
[FieldOffset(20)] public StashTabType TabType;
678-
[FieldOffset(21)] private fixed byte _reserved[43];
679-
680-
private readonly bool IsValid => Magic == D2StashTab.Magic;
681-
682-
/// <summary>
683-
/// Gets a reference to the layout from a byte span at the given offset.
670+
/// Gets a read-only reference to the layout from a read-only byte span.
671+
/// Throws InvalidDataException if magic values are invalid or version is &lt; 104.
684672
/// </summary>
685-
public static ref D2StashTabLayout From(Span<byte> data, int offset = 0)
673+
public static ref readonly D2SaveLayoutV104 FromReadOnly(ReadOnlySpan<byte> data)
686674
{
687-
if (data.Length - offset < Size)
688-
throw new InvalidDataException($"Data too small for stash tab header: {data.Length - offset} bytes, need {Size}.");
689-
ref var layout = ref MemoryMarshal.AsRef<D2StashTabLayout>(data[offset..]);
675+
ref readonly var layout = ref MemoryMarshal.AsRef<D2SaveLayoutV104>(data);
690676
if (!layout.IsValid)
691-
throw new InvalidDataException($"Invalid stash tab magic: 0x{layout.Magic:X8}, expected 0x{D2StashTab.Magic:X8}");
677+
throw new InvalidDataException("Invalid D2 save file: magic values do not match or version < 104.");
692678
return ref layout;
693679
}
694680

695-
}
681+
/// <summary>
682+
/// Updates the checksum in the provided data buffer.
683+
/// </summary>
684+
public static void UpdateChecksum(Span<byte> data)
685+
=> ChecksumCalculator.Update(data);
686+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Runtime.InteropServices;
2+
using D2SSharp.Enums;
3+
4+
namespace D2SSharp.Model;
5+
6+
/// <summary>
7+
/// Blittable layout for the fixed 64-byte header of a shared stash tab.
8+
/// Provides zero-copy access to tab header fields without parsing items.
9+
/// </summary>
10+
[StructLayout(LayoutKind.Explicit, Size = Size)]
11+
public unsafe struct D2StashTabLayout
12+
{
13+
/// <summary>Size of the stash tab header in bytes.</summary>
14+
public const int Size = 64;
15+
16+
[FieldOffset(0)] public uint Magic;
17+
[FieldOffset(4)] public uint StashFormat;
18+
[FieldOffset(8)] public uint ItemFormat;
19+
[FieldOffset(12)] public uint Gold;
20+
[FieldOffset(16)] public ushort TabSize;
21+
[FieldOffset(18)] public ushort Season;
22+
[FieldOffset(20)] public StashTabType TabType;
23+
[FieldOffset(21)] private fixed byte _reserved[43];
24+
25+
private readonly bool IsValid => Magic == D2StashTab.Magic;
26+
27+
/// <summary>
28+
/// Gets a mutable reference to the layout from a byte span.
29+
/// </summary>
30+
public static ref D2StashTabLayout From(Span<byte> data)
31+
{
32+
if (data.Length < Size)
33+
throw new InvalidDataException($"Data too small for stash tab header: {data.Length} bytes, need {Size}.");
34+
ref var layout = ref MemoryMarshal.AsRef<D2StashTabLayout>(data);
35+
if (!layout.IsValid)
36+
throw new InvalidDataException($"Invalid stash tab magic: 0x{layout.Magic:X8}, expected 0x{D2StashTab.Magic:X8}");
37+
return ref layout;
38+
}
39+
40+
/// <summary>
41+
/// Gets a read-only reference to the layout from a read-only byte span.
42+
/// </summary>
43+
public static ref readonly D2StashTabLayout FromReadOnly(ReadOnlySpan<byte> data)
44+
{
45+
if (data.Length < Size)
46+
throw new InvalidDataException($"Data too small for stash tab header: {data.Length} bytes, need {Size}.");
47+
ref readonly var layout = ref MemoryMarshal.AsRef<D2StashTabLayout>(data);
48+
if (!layout.IsValid)
49+
throw new InvalidDataException($"Invalid stash tab magic: 0x{layout.Magic:X8}, expected 0x{D2StashTab.Magic:X8}");
50+
return ref layout;
51+
}
52+
}

0 commit comments

Comments
 (0)