Skip to content

Commit 3b7a8a3

Browse files
committed
Expose zero-copy NPY layout parsing
1 parent 39e59a8 commit 3b7a8a3

1 file changed

Lines changed: 33 additions & 17 deletions

File tree

  • Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Numpy

‎Hawkynt.FileFormats.Archives/FileFormats/FileFormat.Numpy/NpyReader.cs‎

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,32 @@ public sealed record NpyArray(
3838
byte[] ArrayBytes // raw payload after the header
3939
);
4040

41+
internal readonly record struct NpyLayout(
42+
byte MajorVersion,
43+
byte MinorVersion,
44+
int HeaderLength,
45+
string HeaderText,
46+
string? Dtype,
47+
string? Shape,
48+
bool FortranOrder,
49+
int BodyOffset);
50+
4151
/// <summary>Parses an NPY file from an in-memory span.</summary>
4252
public static NpyArray Read(ReadOnlySpan<byte> data) {
53+
var layout = ReadLayout(data);
54+
return new NpyArray(
55+
MajorVersion: layout.MajorVersion,
56+
MinorVersion: layout.MinorVersion,
57+
HeaderLength: layout.HeaderLength,
58+
HeaderText: layout.HeaderText,
59+
Dtype: layout.Dtype,
60+
Shape: layout.Shape,
61+
FortranOrder: layout.FortranOrder,
62+
HeaderBytes: data[..layout.BodyOffset].ToArray(),
63+
ArrayBytes: data[layout.BodyOffset..].ToArray());
64+
}
65+
66+
internal static NpyLayout ReadLayout(ReadOnlySpan<byte> data) {
4367
if (data.Length < 10) throw new InvalidDataException("npy: file shorter than 10-byte preamble.");
4468
if (!data[..6].SequenceEqual(Magic))
4569
throw new InvalidDataException("npy: bad magic — expected '\\x93NUMPY'.");
@@ -56,38 +80,30 @@ public static NpyArray Read(ReadOnlySpan<byte> data) {
5680
case 2:
5781
case 3:
5882
if (data.Length < 12) throw new InvalidDataException("npy: v2/v3 preamble truncated.");
59-
headerLen = (int)BinaryPrimitives.ReadUInt32LittleEndian(data[8..]);
83+
headerLen = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(data[8..]));
6084
headerStart = 12;
6185
break;
6286
default:
6387
throw new InvalidDataException($"npy: unsupported version {major}.{minor}");
6488
}
6589

66-
if (headerStart + headerLen > data.Length)
90+
if (headerLen < 0 || headerStart > data.Length - headerLen)
6791
throw new InvalidDataException("npy: header length exceeds file size.");
6892

6993
var dictBytes = data.Slice(headerStart, headerLen);
70-
// v1/v2 headers are latin-1; v3 is UTF-8. Latin-1 round-trips all bytes so it's safe here.
94+
// Keep the existing descriptor behaviour: decode the Python-dict header as
95+
// Latin-1 so every byte round-trips through the metadata parser.
7196
var headerText = Encoding.Latin1.GetString(dictBytes);
7297

73-
var dtype = ExtractDictString(headerText, "descr");
74-
var shape = ExtractTuple(headerText, "shape");
75-
var fortran = ExtractDictBool(headerText, "fortran_order");
76-
77-
var bodyStart = headerStart + headerLen;
78-
var header = data[..bodyStart].ToArray();
79-
var body = data[bodyStart..].ToArray();
80-
81-
return new NpyArray(
98+
return new NpyLayout(
8299
MajorVersion: major,
83100
MinorVersion: minor,
84101
HeaderLength: headerLen,
85102
HeaderText: headerText,
86-
Dtype: dtype,
87-
Shape: shape,
88-
FortranOrder: fortran,
89-
HeaderBytes: header,
90-
ArrayBytes: body);
103+
Dtype: ExtractDictString(headerText, "descr"),
104+
Shape: ExtractTuple(headerText, "shape"),
105+
FortranOrder: ExtractDictBool(headerText, "fortran_order"),
106+
BodyOffset: headerStart + headerLen);
91107
}
92108

93109
// Pull a single-quoted or double-quoted value out of a Python-ish dict string.

0 commit comments

Comments
 (0)