Initial commit

This commit is contained in:
zervo 2025-02-14 01:40:36 +01:00
parent e5d5ba8530
commit 0a4c62f9e3
13 changed files with 598 additions and 0 deletions

10
SBHPFv1/constants.go Normal file
View file

@ -0,0 +1,10 @@
package sbhpfv1
// Constants for file format.
// These are statically set in version 1 of SBHPF,
// but can be used to achieve an extended featureset in future versions,
// while maintaining backwards compatibility.
const (
FormatVersion = 0x01 // Current version
ExtendedFeatureFlag = 0x00 // Reserved for future use
)

183
SBHPFv1/deserializer.go Normal file
View file

@ -0,0 +1,183 @@
package sbhpfv1
import (
"encoding/binary"
"errors"
"io"
)
// Deserialize reads a full binary property file to a usable root node object.
// This includes verifying the start headers to ensure compatibility.
func Deserialize(r io.Reader) (*Node, error) {
// Read file header (version + feature flag).
var version, featureFlag uint8
if err := binary.Read(r, binary.LittleEndian, &version); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &featureFlag); err != nil {
return nil, err
}
// Validate version.
if version != FormatVersion {
return nil, errors.New("unsupported format version")
}
// Read root node.
return DeserializeNode(r)
}
// DeserializeNode deserializes a SBHPF node into a usable node object.
func DeserializeNode(r io.Reader) (*Node, error) {
// Read node size ("Node Size" node header).
// TODO: Node size header could possibly be removed entirely in version 2,
// since properties terminate themselves and the node header already contains
// info about amount of properties on the node. This would save 16 bytes per node.
// The size header could potentially contribute to more efficient search algos though.
var size uint32 // uint32 to align with 4 byte constraint.
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
return nil, err
}
// Read property count ("Property Count" node header).
var propCount uint16 // uint16 to align with 2 byte constraint.
if err := binary.Read(r, binary.LittleEndian, &propCount); err != nil {
return nil, err
}
// Read child count ("Child Count" node header).
var childCount uint16
if err := binary.Read(r, binary.LittleEndian, &childCount); err != nil {
return nil, err
}
// Read node name length ("Name Length" node header).
var nameLen uint8 // uint8 to align with 1 byte constraint.
if err := binary.Read(r, binary.LittleEndian, &nameLen); err != nil {
return nil, err
}
// Read node name component.
nameBytes := make([]byte, nameLen)
if _, err := io.ReadFull(r, nameBytes); err != nil {
return nil, err
}
// Read properties.
props := make([]Property, propCount)
for i := uint16(0); i < propCount; i++ { // uint16 to align with 2 byte constraint of "Property Count"
prop, err := DeserializeProperty(r)
if err != nil {
return nil, err
}
props[i] = prop
}
// Read child nodes.
children := make([]*Node, childCount)
for i := uint16(0); i < childCount; i++ {
child, err := DeserializeNode(r)
if err != nil {
return nil, err
}
children[i] = child
}
return &Node{Name: string(nameBytes), Properties: props, Children: children}, nil
}
// DeserializeProperty deserializes a SBHPF property into a usable property object.
func DeserializeProperty(r io.Reader) (Property, error) {
// Read key length
var keyLen uint8 // uint8 to align with 1 byte constraint
if err := binary.Read(r, binary.LittleEndian, &keyLen); err != nil {
return Property{}, err
}
// Read type
var propType PropertyType // PropertyType (byte, uint8) to align with 1 byte and values constraint
if err := binary.Read(r, binary.LittleEndian, &propType); err != nil {
return Property{}, err
}
// Read key string
keyBytes := make([]byte, keyLen)
if _, err := io.ReadFull(r, keyBytes); err != nil {
return Property{}, err
}
// Read value
var value interface{}
switch PropertyType(propType) {
case TypeString:
var strLen uint16 // uint16 to align with 2 byte length-prefix constraint
if err := binary.Read(r, binary.LittleEndian, &strLen); err != nil {
return Property{}, err
}
strBytes := make([]byte, strLen)
if _, err := io.ReadFull(r, strBytes); err != nil {
return Property{}, err
}
value = string(strBytes)
case TypeBool:
var val uint8 // uint8 to align with 1 byte constraint
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = (val == 1)
case TypeUint16:
var val uint16
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeUint32:
var val uint32
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeUint8:
var val uint8
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeInt16:
var val int16
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeInt32:
var val int32
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeInt8:
var val int8
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeFloat32:
var val float32
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
case TypeFloat64:
var val float64
if err := binary.Read(r, binary.LittleEndian, &val); err != nil {
return Property{}, err
}
value = val
}
return Property{
Key: string(keyBytes),
Type: propType,
Value: value,
}, nil
}

8
SBHPFv1/node.go Normal file
View file

@ -0,0 +1,8 @@
package sbhpfv1
// Node is a hierarchical entity object with properties storing data.
type Node struct {
Name string
Properties []Property
Children []*Node
}

8
SBHPFv1/property.go Normal file
View file

@ -0,0 +1,8 @@
package sbhpfv1
// Property defines a value, and belongs to a node.
type Property struct {
Key string
Type PropertyType
Value interface{}
}

19
SBHPFv1/property_type.go Normal file
View file

@ -0,0 +1,19 @@
package sbhpfv1
// PropertyType defines the type of a node property.
type PropertyType byte
const (
TypeInt8 PropertyType = 0x01 // 8-bit signed integer. (1 byte)
TypeUint8 PropertyType = 0x02 // 8-bit unsigned integer. (1 byte)
TypeInt16 PropertyType = 0x03 // 16-bit signed integer. (2 bytes)
TypeUint16 PropertyType = 0x04 // 16-bit unsigned integer. (2 bytes)
TypeInt32 PropertyType = 0x05 // 32-bit signed integer. (4 bytes)
TypeUint32 PropertyType = 0x06 // 32-bit unsigned integer. (4 bytes)
TypeInt64 PropertyType = 0x07 // 64-bit signed integer. (8 bytes)
TypeUint64 PropertyType = 0x08 // 64-bit unsigned integer. (8 bytes)
TypeFloat32 PropertyType = 0x09 // 32-bit IEEE 754 single precision floating-point number. (4 bytes)
TypeFloat64 PropertyType = 0x0a // 64-bit IEEE 754 single precision floating-point number. (8 bytes)
TypeBool PropertyType = 0x0b // 8-bit boolean. (1 byte)
TypeString PropertyType = 0x0c // Variably sized string. (2-byte length header + byte length specified by header)
)

166
SBHPFv1/serializer.go Normal file
View file

@ -0,0 +1,166 @@
package sbhpfv1
import (
"encoding/binary"
"errors"
"io"
)
// Serialize writes a full binary property file from a root node object.
// This includes the start headers necessary for proper deserialization of raw files.
func Serialize(w io.Writer, root *Node) error {
// Write file header (version + feature flag).
if err := binary.Write(w, binary.LittleEndian, uint8(FormatVersion)); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, uint8(ExtendedFeatureFlag)); err != nil {
return err
}
// Serialize root node.
return SerializeNode(w, root)
}
// SerializeNode serializes a node object into a SBHPF node.
// NOTE: Writer must support seeking. If this is not possible, use SerializeNodeStream.
func SerializeNode(w io.Writer, node *Node) error {
// Reserve space for node size ("Node Size" node header).
sizePos := getWriterPosition(w) // Save current position.
if err := binary.Write(w, binary.LittleEndian, uint32(0)); err != nil { // uint32 to align with 4 byte constraint.
return err
}
// Write property count ("Property Count" node header).
propCount := uint16(len(node.Properties)) // uint16 to align with 2 byte constraint.
if err := binary.Write(w, binary.LittleEndian, propCount); err != nil {
return err
}
// Write child node count ("Child Count" node header).
childCount := uint16(len(node.Children))
if err := binary.Write(w, binary.LittleEndian, childCount); err != nil {
return err
}
// Write node name length ("Name Length" node header).
nameLen := uint8(len(node.Name)) // uint8 to align with 1 byte constraint.
if err := binary.Write(w, binary.LittleEndian, nameLen); err != nil {
return err
}
// Write node name if name length > 0 ("Node Name" node component).
if nameLen > 0 {
if _, err := w.Write([]byte(node.Name)); err != nil {
return err
}
}
// Write properties.
for _, prop := range node.Properties {
if err := SerializeProperty(w, prop); err != nil {
return err
}
}
// Write child nodes.
for _, child := range node.Children {
if err := SerializeNode(w, child); err != nil {
return err
}
}
// Seek back and update node size.
endPos := getWriterPosition(w)
nodeSize := uint32(endPos - sizePos)
return writeUint32AtPosition(w, sizePos, nodeSize)
}
// SerializeProperty serializes a property object into a SBHPF property.
func SerializeProperty(w io.Writer, prop Property) error {
// Write key length
keyLen := uint8(len(prop.Key)) // uint8 to align with 1 byte constraint
if err := binary.Write(w, binary.LittleEndian, keyLen); err != nil {
return err
}
// Write type
if err := binary.Write(w, binary.LittleEndian, prop.Type); err != nil {
return err
}
// Write key
if _, err := w.Write([]byte(prop.Key)); err != nil {
return err
}
// Write value
switch PropertyType(prop.Type) {
case TypeString:
strVal, ok := prop.Value.(string)
if !ok {
return errors.New("invalid type for string property")
}
strLen := uint16(len(strVal))
if err := binary.Write(w, binary.LittleEndian, strLen); err != nil {
return err
}
_, err := w.Write([]byte(strVal))
return err
case TypeBool:
val, ok := prop.Value.(bool)
if !ok {
return errors.New("invalid type for boolean property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeUint16:
val, ok := prop.Value.(uint16)
if !ok {
return errors.New("invalid type for uint16 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeUint32:
val, ok := prop.Value.(uint32)
if !ok {
return errors.New("invalid type for float32 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeUint8:
val, ok := prop.Value.(uint8)
if !ok {
return errors.New("invalid type for uint8 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeInt16:
val, ok := prop.Value.(int16)
if !ok {
return errors.New("invalid type for int16 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeInt32:
val, ok := prop.Value.(int32)
if !ok {
return errors.New("invalid type for int32 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeInt8:
val, ok := prop.Value.(int8)
if !ok {
return errors.New("invalid type for int8 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeFloat32:
val, ok := prop.Value.(float32)
if !ok {
return errors.New("invalid type for float32 property")
}
return binary.Write(w, binary.LittleEndian, val)
case TypeFloat64:
val, ok := prop.Value.(float64)
if !ok {
return errors.New("invalid type for float64 property")
}
return binary.Write(w, binary.LittleEndian, val)
}
return errors.New("unsupported property type")
}

102
SBHPFv1/serializer_test.go Normal file
View file

@ -0,0 +1,102 @@
package sbhpfv1_test
import (
"bytes"
"io"
"slices"
"testing"
sbhpfv1 "git.zervo.org/FLUX/GoSBHPF/SBHPFv1"
)
var (
prop_ser_targetbytes = []byte{
0x05, // Key length = 5
0x0c, // Value type = string
0x6f, 0x77, 0x6e, 0x65, 0x72, // Key = "owner"
0x05, 0x00, // String length = 5
0x7a, 0x65, 0x72, 0x76, 0x6f, // String = "zervo"
}
node_ser_targetbytes = []byte{
// ROOT NODE
0x33, 0x00, 0x00, 0x00, // Node size = 33
0x02, 0x00, // Property count = 2
0x01, 0x00, // Child count = 1
0x06, // Name length = 6
0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, // Node name = "player"
// ROOT NODE -> PROPERTY A
0x06, // Key length = 6
0x0b, // Value type = bool
0x61, 0x63, 0x74, 0x69, 0x76, 0x65, // Key = "active"
0x01, // Value = true
// ROOT NODE -> PROPERTY B
0x05, // Key length = 5
0x03, // Value type = 16-bit signed int
0x6c, 0x65, 0x76, 0x65, 0x6c, // Key = "level"
0x1b, 0x00, // Value = 27
// ROOT NODE -> CHILD NODE
0x12, 0x00, 0x00, 0x00, // Node size = 12
0x00, 0x00, // Property count = 0
0x00, 0x00, // Child count = 0
0x09, // Name length = 9
0x69, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, // Node name = "inventory"
}
)
func TestPropertySerialization(t *testing.T) {
prop := sbhpfv1.Property{
Key: "owner",
Type: sbhpfv1.TypeString,
Value: "zervo",
}
buf := &bytes.Buffer{}
err := sbhpfv1.SerializeProperty(buf, prop)
if err != nil {
t.Fatalf("Property serialization failed: %v", err)
}
if !slices.Equal(buf.Bytes(), prop_ser_targetbytes) {
t.Fatalf("Property serialization generated bad data: %v", buf.Bytes())
}
}
func TestNodeSerialization(t *testing.T) {
prop_a := sbhpfv1.Property{
Key: "active",
Type: sbhpfv1.TypeBool,
Value: true,
}
prop_b := sbhpfv1.Property{
Key: "level",
Type: sbhpfv1.TypeInt16,
Value: int16(27),
}
child_node := sbhpfv1.Node{
Name: "inventory",
}
node := sbhpfv1.Node{
Name: "player",
Properties: []sbhpfv1.Property{
prop_a,
prop_b,
},
Children: []*sbhpfv1.Node{
&child_node,
},
}
w := io.WriteSeeker{}
err := sbhpfv1.SerializeNode(buf, &node)
if err != nil {
t.Fatalf("Node serialization failed: %v", err)
}
if !slices.Equal(buf.Bytes(), node_ser_targetbytes) {
t.Fatalf("Node serialization generated bad data: %v", buf.Bytes())
}
}

View file

@ -0,0 +1,19 @@
package sbhpfv1
import (
"bytes"
"io"
)
// SerializeNodeStream serializes a node object into a SBHPF node.
// It is a wrapper around SerializeNode that temporarily stores the serialized nodes in a memory buffer.
// This is less efficient, but also works with writers that lack seeking support.
func SerializeNodeStream(w io.Writer, node *Node) error {
buf := &bytes.Buffer{}
if err := SerializeNode(buf, node); err != nil {
return err
}
_, err := io.Copy(w, buf)
return err
}

32
SBHPFv1/util.go Normal file
View file

@ -0,0 +1,32 @@
package sbhpfv1
import (
"encoding/binary"
"errors"
"io"
)
// getWriterPosition returns the current write position.
func getWriterPosition(w io.Writer) int64 {
if seeker, ok := w.(io.Seeker); ok {
pos, _ := seeker.Seek(0, io.SeekCurrent)
return pos
}
return -1
}
// writeUint32AtPosition writes an uint32 at a given position.
func writeUint32AtPosition(w io.Writer, pos int64, value uint32) error {
if seeker, ok := w.(io.Seeker); ok {
curPos, _ := seeker.Seek(0, io.SeekCurrent) // Save current position
if _, err := seeker.Seek(pos, io.SeekStart); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, value); err != nil {
return err
}
_, _ = seeker.Seek(curPos, io.SeekStart) // Restore position
return nil
}
return errors.New("writer does not support seeking")
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.zervo.org/FLUX/GoSBHPF
go 1.23.5

0
go.sum Normal file
View file

48
main.go Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"log"
"os"
sbhpfv1 "git.zervo.org/FLUX/GoSBHPF/SBHPFv1"
)
func main() {
file, err := os.Create("test.bin")
if err != nil {
log.Fatal(err)
}
defer file.Close()
prop_a := sbhpfv1.Property{
Key: "active",
Type: sbhpfv1.TypeBool,
Value: true,
}
prop_b := sbhpfv1.Property{
Key: "level",
Type: sbhpfv1.TypeInt16,
Value: int16(27),
}
child_node := sbhpfv1.Node{
Name: "inventory",
}
node := sbhpfv1.Node{
Name: "player",
Properties: []sbhpfv1.Property{
prop_a,
prop_b,
},
Children: []*sbhpfv1.Node{
&child_node,
},
}
err = sbhpfv1.SerializeNode(file, &node)
if err != nil {
log.Fatal(err)
}
}

BIN
test.bin Normal file

Binary file not shown.