<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SciGoLib</title>
    <description>The latest articles on DEV Community by SciGoLib (scigolib).</description>
    <link>https://dev.to/scigolib</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15906%2F67081a2c-c32f-43b7-a766-2de9e69eb1ba.png</url>
      <title>DEV Community: SciGoLib</title>
      <link>https://dev.to/scigolib</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scigolib"/>
    <language>en</language>
    <item>
      <title>Read and Write MATLAB .mat Files in Go Without CGO: Files That Read as Empty, and MATLAB Files We Couldn't Open</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sat, 07 Nov 2026 11:57:29 +0000</pubDate>
      <link>https://dev.to/scigolib/read-and-write-matlab-mat-files-in-go-without-cgo-files-that-read-as-empty-and-matlab-files-we-3dg3</link>
      <guid>https://dev.to/scigolib/read-and-write-matlab-mat-files-in-go-without-cgo-files-that-read-as-empty-and-matlab-files-we-3dg3</guid>
      <description>&lt;p&gt;The previous article was about &lt;a href="https://dev.to/scigolib/hdf5-in-pure-go-no-cgo-a-5-kb-file-that-allocated-40-gb-and-round-trip-tests-that-lied-1jao"&gt;scigolib/hdf5&lt;/a&gt;, a pure Go HDF5 library born out of the need to read MATLAB data in Go. This is the other half of that story: &lt;a href="https://github.com/scigolib/matlab" rel="noopener noreferrer"&gt;scigolib/matlab&lt;/a&gt;, the library that actually reads the &lt;code&gt;.mat&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;It reads and writes both MATLAB formats in pure Go, with no cgo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v5&lt;/strong&gt; (MATLAB v5 to v7.2): the classic binary format, including the zlib-compressed elements MATLAB writes by default;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v7.3&lt;/strong&gt;: HDF5 underneath, through scigolib/hdf5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What works today: every numeric class, complex numbers, multi-dimensional arrays, reading char arrays, both byte orders, 32-bit targets in CI, and a fuzz-tested v5 parser. Compressed elements are capped at 100 MB each and a 1000:1 ratio, so a zip bomb fails instead of eating memory.&lt;/p&gt;

&lt;p&gt;What doesn't yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structures and cell arrays are not decoded (in v5 you get the class; a v7.3 struct comes back as one variable per field);&lt;/li&gt;
&lt;li&gt;sparse matrices;&lt;/li&gt;
&lt;li&gt;complex numbers in v7.3 use a real/imag group, not MATLAB's compound layout;&lt;/li&gt;
&lt;li&gt;character arrays can be read but not written;&lt;/li&gt;
&lt;li&gt;the writer doesn't compress;&lt;/li&gt;
&lt;li&gt;an empty v5 cell &lt;code&gt;{}&lt;/code&gt; makes &lt;code&gt;Open&lt;/code&gt; fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of what I want to tell here are bugs, and nearly all of them share one cause:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our tests mostly saw files we wrote ourselves.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Files saved with MATLAB's defaults read as empty
&lt;/h2&gt;

&lt;p&gt;By default, MATLAB's &lt;code&gt;save&lt;/code&gt; writes a Version 7 MAT file with compression on: each variable is wrapped in a &lt;code&gt;miCOMPRESSED&lt;/code&gt; element. Up to v0.3.0, the reader skipped elements it didn't handle. So for a file saved with plain &lt;code&gt;save data.mat&lt;/code&gt;, &lt;code&gt;Open&lt;/code&gt; returned no error and zero variables.&lt;/p&gt;

&lt;p&gt;Reproduced on the v0.3.0 tag, with SciPy's test file &lt;code&gt;inner_outer_tbl_param.mat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v0.3.0:   0 variables, err = nil
v0.3.19: 12 variables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our tests never caught it. They read only our own writer's output, which isn't compressed. Three compressed SciPy files were already sitting in &lt;code&gt;testdata&lt;/code&gt;, but no test opened them until v0.3.15.&lt;/p&gt;

&lt;p&gt;v0.3.1 added zlib decompression with the bomb limits above. The lesson was the same one as hdf5's 116 datasets that read "fine": an empty result without an error is worse than a crash.&lt;/p&gt;




&lt;h2&gt;
  
  
  A bug report that waited three months
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/scigolib/matlab/issues/21" rel="noopener noreferrer"&gt;Issue #21&lt;/a&gt; from &lt;a href="https://github.com/EtherHelm" rel="noopener noreferrer"&gt;@EtherHelm&lt;/a&gt; was the first and so far only external issue the project has had. MATLAB couldn't read files the library wrote. They had already traced the v5 cause themselves.&lt;/p&gt;

&lt;p&gt;The array flags element has two 32-bit words. The spec puts the class in bits 0–7 of the first word and the flags in bits 8–11; the second word is &lt;code&gt;nzmax&lt;/code&gt;. Our &lt;code&gt;encodeArrayFlags&lt;/code&gt; put them in the wrong words. And the reader had the mirror bug: it read the class from the second word.&lt;/p&gt;

&lt;p&gt;So the writer and the reader agreed with each other, and every round-trip test passed. Same story as the compound datatypes in hdf5: a round trip through your own code proves only that your code is consistent with itself.&lt;/p&gt;

&lt;p&gt;What that looked like from the outside, reproduced on the v0.3.14 tag: real files saved by MATLAB and SciPy read with the correct values, but every variable's &lt;code&gt;DataType&lt;/code&gt; came back as &lt;code&gt;unknown&lt;/code&gt;, because the class was taken from &lt;code&gt;nzmax&lt;/code&gt;, which is 0. A second bug sat next to it: the byte-order mapping was inverted, so the default output was big-endian.&lt;/p&gt;

&lt;p&gt;The issue was opened on June 7 and answered and fixed on September 2. The plain reason: most of my time goes to &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt;, &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born&lt;/a&gt; and &lt;a href="https://github.com/coregx/coregex" rel="noopener noreferrer"&gt;coregex&lt;/a&gt;, and a notification about a new issue here can get lost. Once those reach their releases, there will be more time to go through every library regularly. Until then, a report like this one, with the cause already traced, is the fastest way to a fix.&lt;/p&gt;

&lt;p&gt;The fix (&lt;a href="https://github.com/scigolib/matlab/pull/23" rel="noopener noreferrer"&gt;PR #23&lt;/a&gt;) put the spec layout on both sides and added two kinds of tests nobody can satisfy by agreeing with themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading SciPy's reference &lt;code&gt;.mat&lt;/code&gt; files;&lt;/li&gt;
&lt;li&gt;golden-byte tests that compare our output with the spec, byte by byte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more honest note. The v0.1.1-beta CHANGELOG said "Files now fully compatible with MATLAB/Octave". No test in the repository has ever run MATLAB or Octave. @EtherHelm's report was the first real check, and it failed.&lt;/p&gt;




&lt;h2&gt;
  
  
  A 136-byte file that could ask for 2 GiB
&lt;/h2&gt;

&lt;p&gt;The same class of bug as hdf5's &lt;a href="https://github.com/scigolib/hdf5/security/advisories/GHSA-8p88-h28q-w6m9" rel="noopener noreferrer"&gt;GHSA-8p88-h28q-w6m9&lt;/a&gt;: the v5 parser allocated each element at the size its tag claimed, before reading a byte of it, up to a 2 GiB cap. A 136-byte input that claimed 64 MiB allocated 64 MiB, then failed.&lt;/p&gt;

&lt;p&gt;The rest turned up while bumping the hdf5 dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an untyped &lt;code&gt;2^31&lt;/code&gt; constant used as an &lt;code&gt;int&lt;/code&gt; broke the build on linux/386, linux/arm and windows/386;&lt;/li&gt;
&lt;li&gt;a claimed size of exactly 2 GiB slipped past the cap and panicked inside &lt;code&gt;make&lt;/code&gt; on 386.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v0.3.16 validates every size against the bytes actually present, in one place, the tag reader. When the input length is unknown, it accumulates instead of allocating up front. A CI job now builds for linux/386, linux/arm and windows/386 and runs the tests on linux/386.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix that broke &lt;code&gt;bytes.Buffer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The release review of v0.3.17 found that the v0.3.16 fix had broken something else.&lt;/p&gt;

&lt;p&gt;The bound measured how much input was left after reading the 128-byte header. A &lt;code&gt;bytes.Buffer&lt;/code&gt; reports only its unread part, so the bound came out 128 bytes short, and every valid v5 file opened from a &lt;code&gt;bytes.Buffer&lt;/code&gt; was rejected: &lt;code&gt;tag size beyond end of file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No test had passed a &lt;code&gt;bytes.Buffer&lt;/code&gt; to &lt;code&gt;Open&lt;/code&gt;. Now one test opens every test file through &lt;code&gt;os.File&lt;/code&gt;, &lt;code&gt;bytes.Reader&lt;/code&gt;, &lt;code&gt;bytes.Buffer&lt;/code&gt; and a reader that hides its size.&lt;/p&gt;

&lt;p&gt;The same release also caught a stream cut off inside a skipped element being accepted, because &lt;code&gt;io.CopyN&lt;/code&gt; errors were dropped. The review added that a file cut short gave a different error depending on whether its length was known. Every path now returns &lt;code&gt;io.ErrUnexpectedEOF&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  MATLAB's own v7.3 files didn't open
&lt;/h2&gt;

&lt;p&gt;This bug came to light during the fact-checking for this article, and it affected every v7.3 file MATLAB writes.&lt;/p&gt;

&lt;p&gt;MATLAB writes a v7.3 file as HDF5 behind a 512-byte user block. The first 128 bytes of that block are the usual MAT header, with version &lt;code&gt;0x0200&lt;/code&gt;; the HDF5 signature starts at byte 512. Our &lt;code&gt;Open&lt;/code&gt; looked for HDF5 only at offset 0, didn't find it, and handed the file to the v5 parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tag size beyond end of file: 169478669 bytes at offset 512
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All our v7.3 test files came from our own writer, which writes no user block. Same blind spot, third time.&lt;/p&gt;

&lt;p&gt;There was no MATLAB-written v7.3 file at hand, so the layout was built from the references: &lt;a href="https://github.com/tbeu/matio" rel="noopener noreferrer"&gt;matio&lt;/a&gt; (&lt;code&gt;Mat_Open&lt;/code&gt; reads the version at bytes 124–125 and opens &lt;code&gt;0x0200&lt;/code&gt; through HDF5; &lt;code&gt;Mat_Create73&lt;/code&gt; sets a 512-byte user block), SciPy's &lt;code&gt;_get_matfile_version&lt;/code&gt;, and the HDF5 C library's signature search, which looks at offset 0 and then at every power of two from 512. &lt;code&gt;h5jam -u&lt;/code&gt; produced a test file byte-identical to "block + file", and &lt;code&gt;h5dump&lt;/code&gt; opens it. v0.3.18 detects the format by the header version, as matio and SciPy do.&lt;/p&gt;

&lt;p&gt;The other direction took one more release. Our own v7.3 output had no MAT header either, and #21 had mentioned v7.3 too, so this may well have been that half of it. Writing the header needed user-block support in hdf5, the equivalent of the C library's &lt;code&gt;H5Pset_userblock&lt;/code&gt;. hdf5 v0.15.0 added it, and v0.3.19 writes the header the way matio's &lt;code&gt;Mat_Create73&lt;/code&gt; does. &lt;code&gt;h5dump&lt;/code&gt; opens the files. Whether MATLAB loads them, nobody has confirmed yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test data and docs that claimed too much
&lt;/h2&gt;

&lt;p&gt;Two smaller ones, in the same spirit.&lt;/p&gt;

&lt;p&gt;The 8 v7.3 test files were written in November 2025 by an early hdf5 version, and &lt;code&gt;h5dump&lt;/code&gt; can't open them; the hdf5 article tells that story from its side. They are regenerated now, and a test fails on any hdf5 warning. One old file stays in &lt;code&gt;testdata/legacy&lt;/code&gt; to prove such files still read.&lt;/p&gt;

&lt;p&gt;The README listed structures, cell arrays and sparse matrices as supported, and complex numbers as "proper MATLAB format for both v5 and v7.3". The reader decoded none of the three, and the v7.3 half of the complex claim wasn't true. It now says what the code does, with a Known Limitations list.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Your own files are not compatibility tests.&lt;/strong&gt; Compressed files, the flags layout, the user block: each time, the tests saw little beyond what our writer produced. Test against files written by the other implementations from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A silent empty result is worse than an error.&lt;/strong&gt; Zero variables and &lt;code&gt;nil&lt;/code&gt; looked like success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Measure the input before you consume it&lt;/strong&gt;, and test every kind of reader your API accepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't rely on email alone for issues.&lt;/strong&gt; #21 waited three months because the notification went unseen. With many projects, look at each one's open issues directly from time to time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MATLAB's compound layout for complex numbers, read and write;&lt;/li&gt;
&lt;li&gt;structures, cell arrays, sparse matrices, empty cells;&lt;/li&gt;
&lt;li&gt;writing character arrays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you have MATLAB or Octave, I'd like your help.&lt;/strong&gt; Write a v5 and a v7.3 file with v0.3.19, load them there, and tell me what happens in an issue. Nobody has verified our v7.3 output in MATLAB itself, and a short report from a real MATLAB session is worth more than any test written without one.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/EtherHelm" rel="noopener noreferrer"&gt;@EtherHelm&lt;/a&gt; for #21, and for doing half the work in it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;matlab&lt;/strong&gt;: &lt;a href="https://github.com/scigolib/matlab" rel="noopener noreferrer"&gt;github.com/scigolib/matlab&lt;/a&gt; · &lt;a href="https://pkg.go.dev/github.com/scigolib/matlab" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/scigolib/matlab@v0.3.19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.mat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;matlab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// v5 or v7.3, detected from the header&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;mat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Variables&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>go</category>
      <category>matlab</category>
      <category>datascience</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HDF5 in Pure Go, No CGO: a 5 KB File That Allocated 40 GB, and Round-Trip Tests That Lied</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sat, 07 Nov 2026 02:01:30 +0000</pubDate>
      <link>https://dev.to/scigolib/hdf5-in-pure-go-no-cgo-a-5-kb-file-that-allocated-40-gb-and-round-trip-tests-that-lied-1jao</link>
      <guid>https://dev.to/scigolib/hdf5-in-pure-go-no-cgo-a-5-kb-file-that-allocated-40-gb-and-round-trip-tests-that-lied-1jao</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, 2026-11-07:&lt;/strong&gt; &lt;a href="https://github.com/scigolib/hdf5/releases/tag/v0.15.0" rel="noopener noreferrer"&gt;v0.15.0&lt;/a&gt; is out, and it is the version to use: &lt;code&gt;go get github.com/scigolib/hdf5@v0.15.0&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade from v0.14.3.&lt;/strong&gt; A group that switched to dense link storage while being written (at its ninth link, for example in a group that also holds an external link) could leave a file the C library rejects as truncated: &lt;code&gt;end-of-file address N is beyond the file size M&lt;/code&gt;. v0.15.0 fixes it. An affected file is repaired by zero-extending it to N (on Linux and macOS: &lt;code&gt;truncate -s N file.h5&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New:&lt;/strong&gt; user blocks (&lt;code&gt;WithUserBlock&lt;/code&gt;, &lt;code&gt;FileWriter.WriteUserBlock&lt;/code&gt;, the equivalent of &lt;code&gt;H5Pset_userblock&lt;/code&gt;). That is where a MATLAB v7.3 file keeps its MAT header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behaviour change:&lt;/strong&gt; &lt;code&gt;FileWriter.Delete&lt;/code&gt; on a non-empty group now deletes the group and everything only it links to, as &lt;code&gt;H5Ldelete&lt;/code&gt; does. Code that relied on the old refusal should check that the group is empty first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The follow-up article, about the &lt;code&gt;.mat&lt;/code&gt; reader and writer built on this library: &lt;a href="https://dev.to/scigolib/read-and-write-matlab-mat-files-in-go-without-cgo-files-that-read-as-empty-and-matlab-files-we-3dg3"&gt;Read and Write MATLAB .mat Files in Go Without CGO&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In June 2025 I was porting &lt;a href="https://github.com/ALD-Lab/SURD" rel="noopener noreferrer"&gt;SURD&lt;/a&gt;, a causal-discovery method from a Nature Communications paper, from its Python reference to Go (&lt;a href="https://github.com/causalgo/causalgo" rel="noopener noreferrer"&gt;causalgo&lt;/a&gt;). The reference data came as MATLAB &lt;code&gt;.mat&lt;/code&gt; files. In Python that's one &lt;code&gt;scipy.io.loadmat&lt;/code&gt; call. In Go I found nothing I could rely on.&lt;/p&gt;

&lt;p&gt;Those files happened to be the older v5 format. But a v7.3 &lt;code&gt;.mat&lt;/code&gt; file, the format MATLAB needs for any variable of 2 GB or more, is an HDF5 file, so a Go reader that only handled v5 would fail on the first large dataset. And HDF5 itself is how a large part of science stores its data: climate models, telescopes, particle physics. In Go there were cgo bindings to the C library and a few pure-Go attempts, abandoned years ago and far from complete.&lt;/p&gt;

&lt;p&gt;If I was going to write these libraries at all, it made sense to make them universal from the start. So four days later I started two repositories instead of one narrow reader: &lt;a href="https://github.com/scigolib/matlab" rel="noopener noreferrer"&gt;scigolib/matlab&lt;/a&gt; and &lt;a href="https://github.com/scigolib/hdf5" rel="noopener noreferrer"&gt;scigolib/hdf5&lt;/a&gt;, a full HDF5 library, read and write, in pure Go with no cgo. It has been public for a year now, with 34 releases, and this is the first time I write about it.&lt;/p&gt;

&lt;p&gt;Only later did I understand why the earlier attempts stopped. HDF5 is a scientific binary format with decades of history and a nuance in every structure: several versions of each header and message, variable-width fields set per file, half a dozen chunk index types, a filter pipeline, shared messages, and edge cases that only the C library's own test files show you.&lt;/p&gt;

&lt;p&gt;What's in it today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;superblock v0, v2 and v3; object headers v1 and v2; compact and dense attributes and links;&lt;/li&gt;
&lt;li&gt;contiguous, chunked and compact layouts; GZIP, shuffle and Fletcher32 (LZF and BZIP2 on read);&lt;/li&gt;
&lt;li&gt;compound, array, enum, variable-length, reference and opaque types;&lt;/li&gt;
&lt;li&gt;chunk indexes of every format version, including the ones HDF5 1.10+ writes (fixed and extensible array, B-tree v2, single chunk, implicit);&lt;/li&gt;
&lt;li&gt;reading tested against the HDF5 C library's own test files: 379 of the 383 valid ones read;&lt;/li&gt;
&lt;li&gt;files written by the library are checked with &lt;code&gt;h5dump&lt;/code&gt; in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing goes beyond the C library: when the version 2 B-trees behind dense attributes rebalance after deletions. C merges and redistributes their nodes on every delete (its older version 1 B-trees are never rebalanced). Here you choose: immediately, in batches, in a background goroutine, a strategy picked from the workload, or not at all for append-only files.&lt;/p&gt;

&lt;p&gt;What isn't there yet. Each of these fails with an explicit error, not wrong data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;virtual datasets;&lt;/li&gt;
&lt;li&gt;the N-bit, scale-offset and SZIP filters;&lt;/li&gt;
&lt;li&gt;the shared-message heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not everything is done, but it's close.&lt;/p&gt;

&lt;p&gt;scigolib/matlab reads and writes &lt;code&gt;.mat&lt;/code&gt; files from v5 to v7.3, the v7.3 ones on top of it.&lt;/p&gt;

&lt;p&gt;Then v0.14.3 happened. It started with one report:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One changed byte in a 5,656-byte file made the library try to allocate 40.9 GB.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A 5 KB file, a 40 GB allocation
&lt;/h2&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/scigolib/hdf5/issues/75" rel="noopener noreferrer"&gt;#75&lt;/a&gt; from &lt;a href="https://github.com/TuSKan" rel="noopener noreferrer"&gt;@TuSKan&lt;/a&gt;. Take a valid file, change the datatype size in the dataset's metadata, and ask for 12 values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadSlice&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c"&gt;// v0.14.2: tries to allocate 40,873,492,480 bytes, the process dies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v0.14.2 had just shipped a fix for this class of bug, with a security advisory (&lt;a href="https://github.com/scigolib/hdf5/security/advisories/GHSA-8p88-h28q-w6m9" rel="noopener noreferrer"&gt;GHSA-8p88-h28q-w6m9&lt;/a&gt;). Whole-dataset reads were bounded. The hyperslab readers weren't: they parsed the dataset metadata again on their own and skipped the check.&lt;/p&gt;

&lt;p&gt;That is the real bug. The fix for one path didn't reach the other paths, because each path had its own copy of the parsing.&lt;/p&gt;

&lt;p&gt;The C library doesn't work like that. When it opens a dataset, it validates the storage once (&lt;code&gt;H5D__contig_check&lt;/code&gt;, &lt;code&gt;H5D__compact_init&lt;/code&gt;, &lt;code&gt;H5D__chunk_set_sizes&lt;/code&gt;), and everything after works from that. v0.14.3 does the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one validated dataset descriptor, built once per dataset;&lt;/li&gt;
&lt;li&gt;every reader takes its sizes from it;&lt;/li&gt;
&lt;li&gt;every result buffer is sized through one helper;&lt;/li&gt;
&lt;li&gt;a guard test fails if any allocation sized from file metadata appears outside that helper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that in place, the review found four more paths of the same class: the B-tree walk, filter output, variable-length amplification and the compound result size. All four are closed.&lt;/p&gt;

&lt;p&gt;One case can't be bounded by the file size. A chunked dataset may legally be larger than the file, because unwritten chunks read as the fill value. Those reads get a per-file limit: &lt;code&gt;File.SetReadLimit&lt;/code&gt;, 4 GiB by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Round-trip tests that lied
&lt;/h2&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/scigolib/hdf5/issues/65" rel="noopener noreferrer"&gt;#65&lt;/a&gt; from &lt;a href="https://github.com/kf-domeng" rel="noopener noreferrer"&gt;@kf-domeng&lt;/a&gt;. A compound dataset written by the library opened fine in the library. h5py and H5Web refused it: &lt;code&gt;invalid number of members: 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My round-trip tests were green the whole time. The writer and the reader had the same misunderstanding of the compound datatype message, so they agreed with each other. A round trip through your own code proves only that your code is consistent with itself.&lt;/p&gt;

&lt;p&gt;The fix was to stop having two opinions about the format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one datatype codec for every class, mirroring the C library's encoder and decoder;&lt;/li&gt;
&lt;li&gt;the test: decode and re-encode all 1554 datatype messages in the official test files and compare with C's bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They match. The one deliberate difference is 8 old v1 compounds with array members, which get upgraded exactly the way C upgrades them.&lt;/p&gt;

&lt;p&gt;Compounds were not the worst case. Dense attribute storage, which HDF5 uses once an object has many attributes, had never been C-compatible at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the attribute info message was written and read as message type 15, which in C is the shared-message table (the attribute info message is 21);&lt;/li&gt;
&lt;li&gt;the attribute name index was built with link records instead of attribute records, and C checks the record type;&lt;/li&gt;
&lt;li&gt;fractal heap objects were addressed from the end of the block header instead of the block start, so C answered "invalid fractal heap offset" for every object we wrote.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it passed our tests, because our reader made the same mistakes. Run against the C suite, the reader found 0 of the 9 attributes in &lt;code&gt;tattr4_be.h5&lt;/code&gt;, and returned no error. Old files written that way still read, and their storage is moved to the C layout the first time their attributes change.&lt;/p&gt;

&lt;p&gt;Compound types also got a public API, validated the way &lt;code&gt;H5Tinsert&lt;/code&gt; validates members:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCompoundType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompoundMember&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float64&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateCompound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"energy"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13.6&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"energy"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;27.2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  116 datasets that read "fine"
&lt;/h2&gt;

&lt;p&gt;This one I found myself, and it bothered me more than the crash.&lt;/p&gt;

&lt;p&gt;HDF5 has optional filters the library doesn't implement: N-bit, scale-offset, SZIP, blosc and others. The old code skipped an unknown filter and returned the stored bytes as if they were the values. In the C test suite, 116 datasets read without an error and returned garbage.&lt;/p&gt;

&lt;p&gt;A crash you notice. Wrong numbers in a science pipeline you may never notice.&lt;/p&gt;

&lt;p&gt;Now an unsupported filter returns &lt;code&gt;ErrNotSupported&lt;/code&gt;, as the C library's filter pipeline does. Three smaller ones of the same kind were fixed along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unwritten chunks read as the fill value, not as zero;&lt;/li&gt;
&lt;li&gt;unsigned compound members above 2^31 no longer come back negative;&lt;/li&gt;
&lt;li&gt;Fletcher32 checksums are verified on read; they used to be stripped unchecked.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Errors you can act on
&lt;/h2&gt;

&lt;p&gt;All the failures above now come back as errors you can tell apart with &lt;code&gt;errors.Is&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.h5"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetReadLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// reads of data the file itself doesn't bound: at most 1 GiB&lt;/span&gt;

&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrCorrupt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="c"&gt;// the file contradicts itself&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="c"&gt;// a read above the limit&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotSupported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="c"&gt;// e.g. an SZIP-compressed dataset&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hdf5&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNoData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="c"&gt;// nothing stored, no fill value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/skyqvn" rel="noopener noreferrer"&gt;@skyqvn&lt;/a&gt; asked in &lt;a href="https://github.com/scigolib/hdf5/issues/67" rel="noopener noreferrer"&gt;#67&lt;/a&gt; for structured metadata instead of parsing &lt;code&gt;Info()&lt;/code&gt; strings. Datasets now have &lt;code&gt;Shape&lt;/code&gt;, &lt;code&gt;MaxShape&lt;/code&gt;, &lt;code&gt;Datatype&lt;/code&gt; and &lt;code&gt;Layout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The writer moved closer to C as well. In more places it now writes exactly what the C library writes: the fill value message, filter flags, pipeline v2, dataspace v2 and message order. Tests compare its output with C-written files.&lt;/p&gt;

&lt;p&gt;It also got faster. Against v0.14.2, opening and walking a file takes half the time with a tenth of the memory, reading many small slices of a chunked dataset takes 58–72% less time, and across the benchmarks the geometric mean is 44% less time and 64% less memory. (&lt;code&gt;benchstat&lt;/code&gt;, six runs of each benchmark in &lt;a href="https://github.com/scigolib/hdf5/blob/v0.14.3/release_bench_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;release_bench_test.go&lt;/code&gt;&lt;/a&gt;, run against both versions on the same laptop.) Most of it came from one fix: object headers are now read with one read per chunk instead of two per message.&lt;/p&gt;




&lt;h2&gt;
  
  
  Our own old files
&lt;/h2&gt;

&lt;p&gt;The hardest question in this release came from our own sibling project. scigolib/matlab ran its tests against the new, stricter superblock check, and 8 of its &lt;code&gt;.mat&lt;/code&gt; test files were rejected. They had been written by this library in November 2025.&lt;/p&gt;

&lt;p&gt;The library was right to reject them. Versions up to v0.13.4 wrote a CRC32 checksum where HDF5 requires Jenkins lookup3, and &lt;code&gt;h5dump&lt;/code&gt; can't open those files either. Later versions had their own gaps: v2 object headers with no checksum at all, an end-of-file address not updated on close. Files from those versions are not valid HDF5, even though our own reader always opened them.&lt;/p&gt;

&lt;p&gt;So what do you do with files your own library wrote wrong?&lt;/p&gt;

&lt;p&gt;"Be liberal in what you accept" has a bad record: &lt;a href="https://www.rfc-editor.org/info/rfc9413/" rel="noopener noreferrer"&gt;RFC 9413&lt;/a&gt; describes how silent tolerance entrenches bugs. But a targeted workaround for one known buggy writer is standard practice: Apache Parquet ignores the min/max statistics written by old parquet-mr versions, which it identifies by &lt;code&gt;created_by&lt;/code&gt;, instead of rejecting the files. A checksum on metadata guards integrity, not authenticity, so accepting one exact legacy value costs nothing in security.&lt;/p&gt;

&lt;p&gt;The policy in v0.14.3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;old files still read, and every deviation is reported through &lt;code&gt;File.Warnings()&lt;/code&gt; with a typed code. A file written by the C library or by the current version produces no warnings;&lt;/li&gt;
&lt;li&gt;everything the library writes must be valid HDF5. What can be repaired in place is repaired; what can't is refused with a clear error;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go.mod&lt;/code&gt; retracts v0.13.9 and older, the versions that wrote files no HDF5 tool can open, so &lt;code&gt;go get&lt;/code&gt; no longer picks them and &lt;code&gt;go list -m -u&lt;/code&gt; tells users why;&lt;/li&gt;
&lt;li&gt;planned: a repack function to convert old files, and from v1.0 a strict default with one explicit legacy option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read old files, but never write a new broken one, and tell people so nobody keeps producing them.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I check it now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The C library is the oracle.&lt;/strong&gt; Every format check cites the C source it follows, file and line. Where the format spec defines a value, the spec decides, not the C code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every fix starts with a test that fails on the old code.&lt;/strong&gt; Corruption tests claim megabytes against kilobyte inputs. Crafted-input runs work under a 4 GB memory cap, so a missed bound fails the test instead of the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two reviews.&lt;/strong&gt; Every change gets an adversarial review. Once per release, a second, independent model reviews the final security diff. I started that after the v0.14.2 advisory turned out to be incomplete. I use AI tools for this work openly. They are fast at finding the fifth path of a bug class once you've found the first, and they never replace the test that proves it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Validate once, at the boundary.&lt;/strong&gt; Every reader that re-parses the metadata is a reader that can forget a check. One validated descriptor turned "fix it in N places" into "fix it once".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never trust a round trip alone.&lt;/strong&gt; Reading your own output proves only consistency. Test against the other implementation: h5dump, h5py, the C library's bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unknown means error, not "skip".&lt;/strong&gt; A filter you don't understand is a reason to stop, not to hand back bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Your own old files are part of the format.&lt;/strong&gt; Users keep what you wrote. Decide what you will read, what you will warn about and what you will write before the strict check ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A security fix covers a class, not a path.&lt;/strong&gt; After the first path, look for every other path that sizes memory from file metadata. In #75 there were five.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Users drive much of this work: the crash, the h5py failure and the metadata API all started as issues from people I'd never met. Thank you, &lt;a href="https://github.com/TuSKan" rel="noopener noreferrer"&gt;@TuSKan&lt;/a&gt;, &lt;a href="https://github.com/kf-domeng" rel="noopener noreferrer"&gt;@kf-domeng&lt;/a&gt; and &lt;a href="https://github.com/skyqvn" rel="noopener noreferrer"&gt;@skyqvn&lt;/a&gt;, and &lt;a href="https://github.com/rhaist" rel="noopener noreferrer"&gt;@rhaist&lt;/a&gt; and &lt;a href="https://github.com/chiliec" rel="noopener noreferrer"&gt;@chiliec&lt;/a&gt; for merged PRs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/scigolib" rel="noopener noreferrer"&gt;scigolib&lt;/a&gt; is meant to be a home for scientific libraries in Go. The longer plan is Go counterparts of the Python libraries people reach for, visualization included. My own main focus is GPU graphics (&lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt;) and ML (&lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born&lt;/a&gt;). So if scientific Go is your thing, I'd be glad of help: issues, PRs, or bringing your own library into scigolib. A co-maintainer for hdf5 would be welcome.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;hdf5&lt;/strong&gt;: &lt;a href="https://github.com/scigolib/hdf5" rel="noopener noreferrer"&gt;github.com/scigolib/hdf5&lt;/a&gt; · &lt;a href="https://pkg.go.dev/github.com/scigolib/hdf5" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/scigolib/hdf5@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>go</category>
      <category>datascience</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
