summaryrefslogtreecommitdiff
path: root/rrdformat/format.go
blob: 0233a5c0bf9a34ca43bd96c0dae83e17cd7287c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package rrdformat

import (
	"bytes"
	"encoding"
	"encoding/binary"
	"encoding/xml"
	"math"
)

type Unival uint64

func (u Unival) AsUint64() uint64   { return uint64(u) }
func (u Unival) AsFloat64() float64 { return math.Float64frombits(uint64(u)) }

const XMLNS = "https://oss.oetiker.ch/rrdtool/rrdtool-dump.xml"

// rrdtool:
//   rrd_format.h: stat_head_t -- static header of the database
//
// javascriptRRD:
//   rrdFile.js: RRDHeader
type Header struct {
	// identification section
	Cookie  []byte // 4 bytes
	Version []byte // 4 bytes (eh, we let the \0-terminator decided how long)
	// padding
	FloatCookie []byte // 8 bytes

	Bytes

	// structure definition
	DSCnt      uint64 // how many different DS provide input to the RRD
	RRACnt     uint64 // how many RRAs will be maintained in the RRD
	PDPStep    uint64 // PDP interval in seconds
	Parameters [10]Unival

	// sniffed metadata
	ByteOrder   binary.ByteOrder
	FloatWidth  int
	FloatAlign  int
	IntWidth    int
	IntAlign    int
	UnivalWidth int
	UnivalAlign int
}

func (h *Header) UnmarshalBinary(data []byte) error {
	// magic number cookie
	if !bytes.HasPrefix(data, []byte("RRD\x00")) {
		return newBinError("not an RRD file: wrong magic number", data, 0, 4)
	}
	h.Cookie = data[0:4]

	// version string
	null := bytes.IndexByte(data[4:], 0)
	if null < 0 {
		return newBinError("no null-terminator on version string", data, 4, 5)
	}
	null += 4
	h.Version = data[4:null]
	switch string(h.Version) {
	case "0003":
	case "0004":
	case "0005":
	default:
	}

	// float cookie
	//
	// Assume IEEE 754 doubles.  C doesn't assume 754 doubles, but anything that doesn't use 754 doubles is exotic
	// enough that I'm OK saying "you're going to need to use `rrdtool dump`".  This lets us assume that:
	//  - a 'double' is 8 bytes wide

	//  - the value will be exactly equal, and we don't need to worry about weird rounding.
	h.FloatWidth = 8
	magicFloat := float64(8.642135e130)
	floatAddrPacked := null + 1
	floatAddr32 := ((floatAddrPacked + 3) / 4) * 4
	floatAddr64 := ((floatAddrPacked + 7) / 8) * 8
	var restOffset int
	switch {
	case len(data) < floatAddr32+h.FloatWidth:
		return newBinError("unexpected end of file", data, floatAddrPacked, floatAddr64+h.FloatWidth-floatAddrPacked)
	case math.Float64frombits(binary.LittleEndian.Uint64(data[floatAddr32:])) == magicFloat:
		h.FloatCookie = data[floatAddr32 : floatAddr32+h.FloatWidth]
		h.ByteOrder = binary.LittleEndian
		h.FloatAlign = 4
		restOffset = floatAddr32 + h.FloatWidth
	case math.Float64frombits(binary.BigEndian.Uint64(data[floatAddr32:])) == magicFloat:
		h.FloatCookie = data[floatAddr32 : floatAddr32+h.FloatWidth]
		h.ByteOrder = binary.BigEndian
		h.FloatAlign = 4
		restOffset = floatAddr32 + h.FloatWidth
	case len(data) < floatAddr64+h.FloatWidth:
		return newBinError("unexpected end of file", data, floatAddrPacked, floatAddr64+h.FloatWidth-floatAddrPacked)
	case math.Float64frombits(binary.LittleEndian.Uint64(data[floatAddr64:])) == magicFloat:
		h.FloatCookie = data[floatAddr64 : floatAddr64+h.FloatWidth]
		h.ByteOrder = binary.LittleEndian
		h.FloatAlign = 8
		restOffset = floatAddr64 + h.FloatWidth
	case math.Float64frombits(binary.BigEndian.Uint64(data[floatAddr64:])) == magicFloat:
		h.FloatCookie = data[floatAddr64 : floatAddr64+h.FloatWidth]
		h.ByteOrder = binary.BigEndian
		h.FloatAlign = 8
		restOffset = floatAddr64 + h.FloatWidth
	default:
		return newBinError("failed to sniff byte-order and float-alignment",
			data, floatAddrPacked, floatAddr64+h.FloatWidth-floatAddrPacked)
	}

	switch h.FloatAlign {
	case 4:
		// Assume that if floats are only 32-bit aligned, then everything is 32-bit
		h.IntWidth = 4
		h.IntAlign = 4
	case 8:
		// If floats are 64-bit aligned, then this might be all-in on 64-bit, or it might 32-bit ints.

		// (The following heuristic is borrowed from javascriptRRD, and adjusted to also work with big-endian.)
		//
		// The next 2 things after the float_cookie are ds_cnt and rra_cnt (both 'unsigned long'--which may be
		// either 32 or 64 bit).  We'll inspect the bytes a bit to guess how long a long is.
		//
		// By assuming
		//  1. ds_cnt <= math.MaxUint32
		//  2l. rra_cnt > 0 (relevant if little-endian)
		//  2b. ds_cnt > 0 (relevant if big-endian)
		// we can inspect the 4 bytes (marked "big" or "little" below) that are either
		//  - the most significant bits of 64-bit ds_cnt, or
		//  - the entirety of 32-bit rra_cnt (if littlen-endian) or 32-bit ds_cnt (if big-endian)
		// If we see that those 4 bytes are all 0, then we assume that it's part of a 64-bit ds_cnt.
		//
		//       |           |           |           |           |           |           |    big    |  little   |
		//       |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
		//  32   | R| R| D|\0| 0| 0| 0| 3|\0|                    |<----doublecookie----->|<--ds_cnt->|<-rra_cnt->|
		//  64le | R| R| D|\0| 0| 0| 0| 3|\0|                    |<----doublecookie----->|<1111----ds_cnt---0000>|
		//  64be | R| R| D|\0| 0| 0| 0| 3|\0|                    |<----doublecookie----->|<0000----ds_cnt---1111>|
		if len(data) < restOffset+8 {
			return newBinError("unexpected end of file", data, restOffset, 8)
		}
		offset := map[binary.ByteOrder]int{
			binary.BigEndian:    restOffset,     // 24 in the above diagram
			binary.LittleEndian: restOffset + 4, // 28 in the above diagram
		}[h.ByteOrder]
		if h.ByteOrder.Uint32(data[offset:]) == 0 {
			h.IntWidth = 8
			h.IntAlign = 8
		} else {
			h.IntWidth = 4
			h.IntAlign = 4
		}
	}

	return nil
}

func (h *Header) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	if err := e.EncodeElement(h.Version, xml.StartElement{Name: xml.Name{Local: "version", Space: XMLNS}}); err != nil {
		return err
	}
	if err := e.EncodeElement(h.PDPStep, xml.StartElement{Name: xml.Name{Local: "step", Space: XMLNS}}); err != nil {
		return err
	}
	return nil
}

//var _ encoding.BinaryMarshaler = &Header{}
var _ encoding.BinaryUnmarshaler = &Header{}

var _ xml.Marshaler = &Header{}

//var _ xml.Unmarshaler = &Header{}