gohdf / hdf4 / hdf4.go

// Copyright 2011 GoHDF Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package hdf4 provides access to Hierarchical Data Format (HDF)
// version 4 files.
//
// The specification can be found here:
// http://www.hdfgroup.org/ftp/HDF/Documentation/HDF42r3_SpecDG.pdf
package hdf4

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
)

type File struct {
	fd      *os.File
	r       *bufio.Reader
	Records []Record
	Version *Version
}

type Record struct {
	Tag, Ref    uint16
	offset, len uint32
	file        *File
	parsing     bool
	Info        interface{}
}

func get8(b []byte) (uint8, []byte) {
	return uint8(b[0]), b[1:]
}

func get16(b []byte) (uint16, []byte) {
	return uint16(b[0])<<8 | uint16(b[1]), b[2:]
}

func get32(b []byte) (uint32, []byte) {
	return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), b[4:]
}

func getstring(b []byte) ([]byte, []byte) {
	n, b := get16(b)
	return b[:n], b[n:]
}

func Open(filename string) (*File, error) {
	fd, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	file := new(File)
	file.fd = fd
	file.r = bufio.NewReader(fd)

	var hdfMagic = []byte{0x0E, 0x03, 0x13, 0x01}
	magic := make([]byte, len(hdfMagic))
	if _, err = io.ReadFull(file.r, magic); err != nil {
		return nil, err
	}
	if bytes.Compare(magic, hdfMagic) != 0 {
		return nil, errors.New("not a HDF file")
	}
	if err = file.readDescriptors(); err != nil {
		return nil, err
	}

	rec, err := file.LookupRecord(DFTAG_VERSION, -1)
	if err != nil {
		return nil, err
	}
	file.Version = rec.Info.(*Version)

	return file, nil
}

func (file *File) Close() {
	file.fd.Close()
}

func (file *File) readDescriptors() error {
	file.Records = nil
	for {
		// Read block header
		var bheader [2 + 4]byte
		if _, err := io.ReadFull(file.r, bheader[:]); err != nil {
			return err
		}
		bsize, b := get16(bheader[:])
		bnext, b := get32(b)

		// Read descriptors in this block
		const descriptorSize = 12
		b = make([]byte, bsize*descriptorSize)
		if _, err := io.ReadFull(file.r, b); err != nil {
			return err
		}
		for i := 0; i < int(bsize); i++ {
			rec := new(Record)
			rec.file = file
			rec.Tag, b = get16(b)
			rec.Ref, b = get16(b)
			rec.offset, b = get32(b)
			rec.len, b = get32(b)
			if rec.Tag != DFTAG_NULL {
				file.Records = append(file.Records, *rec)
			}
		}

		// Seek to the next block if any
		if bnext == 0 {
			break
		}
		if _, err := file.fd.Seek(int64(bnext), os.SEEK_SET); err != nil {
			return err
		}
		file.r = bufio.NewReader(file.fd)
	}
	return nil
}

// LookupRecord returns the first record matching tag
// and ref, which uniquely identifies a record. If ref < 0,
// first record matching tag is returns.
func (file *File) LookupRecord(tag, ref int) (*Record, error) {
	var rec *Record
	for i := 0; i < len(file.Records); i++ {
		rec = &file.Records[i]
		if ref < 0 && int(rec.Tag) == tag {
			goto found
		}
		if int(rec.Ref) == ref && int(rec.Tag) == tag {
			goto found
		}
	}
	return nil, errors.New("tag/ref not found")

found:
	if rec.Info == nil && !rec.parsing {
		b, err := rec.readPayload()
		if err != nil {
			return nil, err
		}
		rec.parsing = true
		defer func() { rec.parsing = false }()

		switch rec.Tag {
		case DFTAG_VERSION:
			rec.Info = parseVersion(b)
		case DFTAG_NT:
			rec.Info = parseNumType(b)
		case DFTAG_SDD:
			rec.Info, err = file.parseSDD(b)
			if err != nil {
				return nil, err
			}
		case DFTAG_NDG:
			rec.Info, err = file.parseNDG(b)
			if err != nil {
				return nil, err
			}
		case DFTAG_VG:
			rec.Info, err = file.parseVGroup(b)
			if err != nil {
				return nil, err
			}
		default:
			return nil, fmt.Errorf("unknown tag %d", int(rec.Tag))
		}
	}
	return rec, nil
}

func (file *File) lookupNumType(tag, ref int) (*NumType, error) {
	rec, err := file.LookupRecord(int(tag), int(ref))
	if err != nil {
		return nil, err
	}
	nt, ok := rec.Info.(*NumType)
	if !ok {
		return nil, fmt.Errorf("parse error: expected NumType; got tag %d", int(tag))
	}
	return nt, nil
}

func (rec *Record) readPayload() ([]byte, error) {
	if _, err := rec.file.fd.Seek(int64(rec.offset), os.SEEK_SET); err != nil {
		return nil, err
	}
	r := bufio.NewReader(rec.file.fd)

	b := make([]byte, rec.len)
	if _, err := io.ReadFull(r, b); err != nil {
		return nil, err
	}
	return b, nil
}

type Version struct {
	Major, Minor, Release uint32
	Description           string
}

func parseVersion(b []byte) *Version {
	v := new(Version)
	v.Major, b = get32(b)
	v.Minor, b = get32(b)
	v.Release, b = get32(b)
	v.Description = string(b[:])
	return v
}

type NumType struct {
	version, Type, width, Class uint8
}

func parseNumType(b []byte) *NumType {
	nt := new(NumType)
	nt.version, b = get8(b)
	nt.Type, b = get8(b)
	nt.width, b = get8(b)
	nt.Class, b = get8(b)
	return nt
}

// Scientific Data Dimension
type SDD struct {
	Dim     []uint32
	DataNT  *NumType
	ScaleNT []*NumType
}

func (file *File) parseSDD(b []byte) (*SDD, error) {
	sdd := new(SDD)
	rank, b := get16(b)

	sdd.Dim = make([]uint32, rank)
	for i := 0; i < int(rank); i++ {
		sdd.Dim[i], b = get32(b)
	}

	tag, b := get16(b)
	ref, b := get16(b)
	nt, err := file.lookupNumType(int(tag), int(ref))
	if err != nil {
		return nil, err
	}
	sdd.DataNT = nt

	sdd.ScaleNT = make([]*NumType, rank)
	for i := 0; i < int(rank); i++ {
		tag, b = get16(b)
		ref, b = get16(b)
		nt, err := file.lookupNumType(int(tag), int(ref))
		if err != nil {
			return nil, err
		}
		sdd.ScaleNT[i] = nt
	}
	return sdd, nil
}

// Numeric Data Group
type NDG struct {
	Records []*Record
}

func (file *File) parseNDG(b []byte) (*NDG, error) {
	var tag, ref uint16

	ndg := new(NDG)
	for len(b) > 0 {
		tag, b = get16(b)
		ref, b = get16(b)
		rec, err := file.LookupRecord(int(tag), int(ref))
		if err != nil {
			continue
		}
		ndg.Records = append(ndg.Records, rec)
	}
	return ndg, nil
}

type VGroup struct {
	Records      []*Record
	Name         []byte
	Class        []byte
	extag, exref uint16
	version      uint16
}

func (file *File) parseVGroup(b []byte) (*VGroup, error) {
	vg := new(VGroup)
	nelt, b := get16(b)

	tags := make([]uint16, nelt)
	for i := 0; i < int(nelt); i++ {
		tags[i], b = get16(b)
	}
	for i := 0; i < int(nelt); i++ {
		var ref uint16
		ref, b = get16(b)
		rec, err := file.LookupRecord(int(tags[i]), int(ref))
		if err != nil {
			continue
		}
		vg.Records = append(vg.Records, rec)
	}
	vg.Name, b = getstring(b)
	vg.Class, b = getstring(b)
	vg.extag, b = get16(b)
	vg.exref, b = get16(b)
	vg.version, b = get16(b)
	return vg, nil
}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.