Skip to content

Commit 4fc8bd8

Browse files
committed
Merge develop into release/v0.13.0 (add v4 write support)
2 parents ece4876 + ff0260b commit 4fc8bd8

File tree

4 files changed

+276
-8
lines changed

4 files changed

+276
-8
lines changed

CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545
### ✨ Added
4646

4747
#### HDF5 Format v4 Superblock Support (TASK-024)
48-
- **Superblock Version 4** parsing (52-byte structure)
48+
- **Superblock Version 4** read and write support (52-byte structure)
49+
- **Read Support**: Parse v4 superblocks with checksum validation
50+
- **Write Support**: Create v4 superblocks with CRC32/Fletcher32 checksums
4951
- **Checksum Validation** - CRC32, Fletcher32, none
5052
- **Mandatory Extension Validation** - Format v4 compliance
5153
- **Backward Compatibility** - Full support for v0, v2, v3 formats
5254

5355
**Implementation**:
5456
- Extended Superblock struct with v4 fields
55-
- `validateSuperblockChecksum()` with 3 algorithms
57+
- `validateSuperblockChecksum()` with 3 algorithms (read)
58+
- `writeV4()` with checksum generation (write)
5659
- `computeFletcher32()` per HDF5 specification
60+
- Round-trip validation tests (write → read → compare)
5761
- Mock-based testing (real v4 files when HDF5 2.0.0 becomes available)
5862

59-
**Files**: `superblock.go` (+103 lines), `superblock_test.go` (+285 lines)
63+
**Files**: `superblock.go` (+203 lines), `superblock_test.go` (+435 lines), `superblock_write_test.go` (+157 lines)
6064

6165
#### 64-bit Chunk Dimensions Support (TASK-025)
6266
- **BREAKING CHANGE**: `DataLayoutMessage.ChunkSize` changed from `[]uint32` to `[]uint64`

internal/core/superblock.go

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,9 @@ func computeFletcher32(data []byte) uint32 {
329329
//
330330
// Returns error if write fails or if superblock version is not supported.
331331
func (sb *Superblock) WriteTo(w io.WriterAt, eofAddress uint64) error {
332-
// Support v0 (legacy) and v2 (modern)
333-
if sb.Version != Version0 && sb.Version != Version2 {
334-
return fmt.Errorf("only superblock version 0 and 2 are supported for writing, got version %d", sb.Version)
332+
// Support v0 (legacy), v2 (modern), and v4 (HDF5 2.0.0)
333+
if sb.Version != Version0 && sb.Version != Version2 && sb.Version != Version4 {
334+
return fmt.Errorf("only superblock version 0, 2, and 4 are supported for writing, got version %d", sb.Version)
335335
}
336336

337337
// Dispatch to version-specific writer
@@ -340,6 +340,8 @@ func (sb *Superblock) WriteTo(w io.WriterAt, eofAddress uint64) error {
340340
return sb.writeV0(w, eofAddress)
341341
case Version2:
342342
return sb.writeV2(w, eofAddress)
343+
case Version4:
344+
return sb.writeV4(w, eofAddress)
343345
default:
344346
return fmt.Errorf("unsupported superblock version: %d", sb.Version)
345347
}
@@ -526,3 +528,102 @@ func (sb *Superblock) writeV0(w io.WriterAt, eofAddress uint64) error {
526528

527529
return nil
528530
}
531+
532+
// writeV4 writes superblock version 4 (HDF5 2.0.0 format with enhanced checksums).
533+
//
534+
// Superblock v4 structure (52 bytes):
535+
//
536+
// Bytes 0-7: Format Signature (\211HDF\r\n\032\n)
537+
// Byte 8: Superblock Version (4)
538+
// Byte 9: Size of Offsets (8)
539+
// Byte 10: Size of Lengths (8)
540+
// Byte 11: File Consistency Flags (0)
541+
// Bytes 12-19: Base Address
542+
// Bytes 20-27: Superblock Extension Address (REQUIRED for v4)
543+
// Bytes 28-35: End of File Address
544+
// Bytes 36-43: Root Group Object Header Address
545+
// Byte 44: Checksum Algorithm (0=None, 1=CRC32, 2=Fletcher32)
546+
// Bytes 45-47: Reserved (3 bytes, must be 0)
547+
// Bytes 48-51: Checksum (covers bytes 8-47)
548+
func (sb *Superblock) writeV4(w io.WriterAt, eofAddress uint64) error {
549+
// Validate required fields
550+
if sb.OffsetSize != 8 || sb.LengthSize != 8 {
551+
return fmt.Errorf("only 8-byte offsets and lengths are supported for writing v4, got offset=%d, length=%d",
552+
sb.OffsetSize, sb.LengthSize)
553+
}
554+
555+
// V4 requires superblock extension (cannot be UNDEFINED)
556+
if sb.SuperExtension == 0 || sb.SuperExtension == 0xFFFFFFFFFFFFFFFF {
557+
return errors.New("superblock v4 requires valid extension address")
558+
}
559+
560+
// Default to CRC32 if not specified
561+
checksumAlgo := sb.ChecksumAlgorithm
562+
if checksumAlgo == 0 {
563+
checksumAlgo = 1 // CRC32
564+
}
565+
566+
// Allocate buffer for superblock v4 (52 bytes)
567+
buf := make([]byte, 52)
568+
569+
// Bytes 0-7: Signature
570+
copy(buf[0:8], Signature)
571+
572+
// Byte 8: Version 4
573+
buf[8] = 4
574+
575+
// Byte 9: Size of offsets (8 bytes)
576+
buf[9] = 8
577+
578+
// Byte 10: Size of lengths (8 bytes)
579+
buf[10] = 8
580+
581+
// Byte 11: File consistency flags (0 for now)
582+
buf[11] = 0
583+
584+
// Bytes 12-19: Base address (typically 0)
585+
binary.LittleEndian.PutUint64(buf[12:20], sb.BaseAddress)
586+
587+
// Bytes 20-27: Superblock extension address (REQUIRED for v4)
588+
binary.LittleEndian.PutUint64(buf[20:28], sb.SuperExtension)
589+
590+
// Bytes 28-35: End-of-file address
591+
binary.LittleEndian.PutUint64(buf[28:36], eofAddress)
592+
593+
// Bytes 36-43: Root group object header address
594+
binary.LittleEndian.PutUint64(buf[36:44], sb.RootGroup)
595+
596+
// Byte 44: Checksum algorithm
597+
buf[44] = checksumAlgo
598+
599+
// Bytes 45-47: Reserved (already zero)
600+
601+
// Bytes 48-51: Checksum (covers bytes 8-47)
602+
var checksum uint32
603+
dataToChecksum := buf[8:48]
604+
605+
switch checksumAlgo {
606+
case 0: // None
607+
checksum = 0
608+
case 1: // CRC32
609+
checksum = crc32.ChecksumIEEE(dataToChecksum)
610+
case 2: // Fletcher32
611+
checksum = computeFletcher32(dataToChecksum)
612+
default:
613+
return fmt.Errorf("unsupported checksum algorithm: %d", checksumAlgo)
614+
}
615+
616+
binary.LittleEndian.PutUint32(buf[48:52], checksum)
617+
618+
// Write superblock at offset 0
619+
n, err := w.WriteAt(buf, 0)
620+
if err != nil {
621+
return fmt.Errorf("failed to write superblock v4: %w", err)
622+
}
623+
624+
if n != 52 {
625+
return fmt.Errorf("incomplete superblock v4 write: wrote %d bytes, expected 52", n)
626+
}
627+
628+
return nil
629+
}

internal/core/superblock_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,14 @@ func TestSuperblockWrite(t *testing.T) {
241241
defer tmpFile.Close()
242242

243243
sb := &Superblock{
244-
Version: 1, // v1 not supported (only v0 and v2 are supported).
244+
Version: 1, // v1 not supported (only v0, v2, and v4 are supported).
245245
OffsetSize: 8,
246246
LengthSize: 8,
247247
}
248248

249249
err = sb.WriteTo(tmpFile, 1024)
250250
assert.Error(t, err)
251-
assert.Contains(t, err.Error(), "only superblock version 0 and 2 are supported")
251+
assert.Contains(t, err.Error(), "only superblock version 0, 2, and 4 are supported")
252252
})
253253

254254
t.Run("rejects invalid sizes", func(t *testing.T) {

internal/core/superblock_write_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ func (m *memWriterAt) WriteAt(p []byte, off int64) (n int, err error) {
2525
return len(p), nil
2626
}
2727

28+
func (m *memWriterAt) ReadAt(p []byte, off int64) (n int, err error) {
29+
if off < 0 {
30+
return 0, nil
31+
}
32+
if off >= int64(len(m.data)) {
33+
return 0, nil
34+
}
35+
n = copy(p, m.data[off:])
36+
return n, nil
37+
}
38+
2839
// TestSuperblock_WriteV0 tests v0 superblock writing.
2940
func TestSuperblock_WriteV0(t *testing.T) {
3041
tests := []struct {
@@ -140,3 +151,155 @@ func TestSuperblock_WriteV0_SymbolTableEntry(t *testing.T) {
140151
cacheType := binary.LittleEndian.Uint32(symEntry[16:20])
141152
require.Equal(t, uint32(1), cacheType, "cache type should be 1 for group")
142153
}
154+
155+
// TestSuperblock_WriteV4 tests v4 superblock writing.
156+
func TestSuperblock_WriteV4(t *testing.T) {
157+
tests := []struct {
158+
name string
159+
sb *Superblock
160+
eofAddr uint64
161+
wantErr bool
162+
errContains string
163+
}{
164+
{
165+
name: "valid v4 superblock with CRC32",
166+
sb: &Superblock{
167+
Version: 4,
168+
OffsetSize: 8,
169+
LengthSize: 8,
170+
BaseAddress: 0,
171+
RootGroup: 0x1000,
172+
SuperExtension: 0x800, // Required for v4
173+
ChecksumAlgorithm: 1, // CRC32
174+
},
175+
eofAddr: 0x2000,
176+
wantErr: false,
177+
},
178+
{
179+
name: "valid v4 with Fletcher32",
180+
sb: &Superblock{
181+
Version: 4,
182+
OffsetSize: 8,
183+
LengthSize: 8,
184+
BaseAddress: 0,
185+
RootGroup: 0x1000,
186+
SuperExtension: 0x800,
187+
ChecksumAlgorithm: 2, // Fletcher32
188+
},
189+
eofAddr: 0x2000,
190+
wantErr: false,
191+
},
192+
{
193+
name: "v4 missing superblock extension",
194+
sb: &Superblock{
195+
Version: 4,
196+
OffsetSize: 8,
197+
LengthSize: 8,
198+
BaseAddress: 0,
199+
RootGroup: 0x1000,
200+
SuperExtension: 0, // Invalid for v4
201+
},
202+
eofAddr: 0x2000,
203+
wantErr: true,
204+
errContains: "requires valid extension",
205+
},
206+
{
207+
name: "v4 with UNDEF extension",
208+
sb: &Superblock{
209+
Version: 4,
210+
OffsetSize: 8,
211+
LengthSize: 8,
212+
BaseAddress: 0,
213+
RootGroup: 0x1000,
214+
SuperExtension: 0xFFFFFFFFFFFFFFFF, // UNDEF
215+
},
216+
eofAddr: 0x2000,
217+
wantErr: true,
218+
errContains: "requires valid extension",
219+
},
220+
}
221+
222+
for _, tt := range tests {
223+
t.Run(tt.name, func(t *testing.T) {
224+
buf := &memWriterAt{data: make([]byte, 0)}
225+
err := tt.sb.writeV4(buf, tt.eofAddr)
226+
227+
if tt.wantErr {
228+
require.Error(t, err)
229+
if tt.errContains != "" {
230+
require.Contains(t, err.Error(), tt.errContains)
231+
}
232+
return
233+
}
234+
235+
require.NoError(t, err)
236+
237+
// Verify written data
238+
data := buf.data
239+
require.Equal(t, 52, len(data), "v4 superblock should be exactly 52 bytes")
240+
241+
// Check signature
242+
for i := 0; i < 8; i++ {
243+
require.Equal(t, Signature[i], data[i], "signature byte %d mismatch", i)
244+
}
245+
246+
// Check version
247+
require.Equal(t, uint8(4), data[8])
248+
249+
// Check sizes
250+
require.Equal(t, uint8(8), data[9], "offset size")
251+
require.Equal(t, uint8(8), data[10], "length size")
252+
253+
// Check addresses
254+
gotBase := binary.LittleEndian.Uint64(data[12:20])
255+
require.Equal(t, tt.sb.BaseAddress, gotBase)
256+
257+
gotSuperExt := binary.LittleEndian.Uint64(data[20:28])
258+
require.Equal(t, tt.sb.SuperExtension, gotSuperExt)
259+
260+
gotEOF := binary.LittleEndian.Uint64(data[28:36])
261+
require.Equal(t, tt.eofAddr, gotEOF)
262+
263+
gotRootGroup := binary.LittleEndian.Uint64(data[36:44])
264+
require.Equal(t, tt.sb.RootGroup, gotRootGroup)
265+
266+
// Check checksum algorithm
267+
require.Equal(t, tt.sb.ChecksumAlgorithm, data[44])
268+
269+
// Verify checksum is correct
270+
checksum := binary.LittleEndian.Uint32(data[48:52])
271+
require.NotZero(t, checksum, "checksum should be non-zero")
272+
})
273+
}
274+
}
275+
276+
// TestSuperblock_WriteV4_RoundTrip tests write + read round-trip for v4.
277+
func TestSuperblock_WriteV4_RoundTrip(t *testing.T) {
278+
original := &Superblock{
279+
Version: 4,
280+
OffsetSize: 8,
281+
LengthSize: 8,
282+
BaseAddress: 0,
283+
RootGroup: 0xABCDEF,
284+
SuperExtension: 0x12345,
285+
ChecksumAlgorithm: 1, // CRC32
286+
}
287+
288+
// Write
289+
buf := &memWriterAt{data: make([]byte, 0)}
290+
err := original.writeV4(buf, 0x20000)
291+
require.NoError(t, err)
292+
293+
// Read back
294+
readBack, err := ReadSuperblock(buf)
295+
require.NoError(t, err)
296+
297+
// Compare
298+
require.Equal(t, uint8(4), readBack.Version)
299+
require.Equal(t, original.OffsetSize, readBack.OffsetSize)
300+
require.Equal(t, original.LengthSize, readBack.LengthSize)
301+
require.Equal(t, original.BaseAddress, readBack.BaseAddress)
302+
require.Equal(t, original.RootGroup, readBack.RootGroup)
303+
require.Equal(t, original.SuperExtension, readBack.SuperExtension)
304+
require.Equal(t, original.ChecksumAlgorithm, readBack.ChecksumAlgorithm)
305+
}

0 commit comments

Comments
 (0)