@@ -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