summaryrefslogtreecommitdiff
path: root/rrdformat/sniff.go
blob: de1e67b492c21bd7d70106298b701c727b21803b (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
package rrdformat

import (
	"encoding/binary"
	"fmt"
	"math"

	"git.lukeshu.com/go/librrd/rrdformat/rrdbinary"
)

func SniffArchitecture(data []byte) (rrdbinary.Architecture, error) {
	var arch rrdbinary.Architecture

	var header struct {
		Cookie  rrdbinary.String `rrdbinary:"size=4" xml:"-"`
		Version rrdbinary.String `rrdbinary:"size=5" xml:"version"`
	}
	if err := rrdbinary.NewDecoder(arch, data).Decode(&header, ""); err != nil {
		return rrdbinary.Architecture{}, err
	}

	// 1. File header magic number
	if header.Cookie != "RRD" {
		return rrdbinary.Architecture{}, rrdbinary.NewBinError("not an RRD file: wrong magic number", data, 0, 4)
	}

	// 1. File format version string
	switch header.Version {
	case "0001", "0002", "0003", "0004", "0005":
		// do nothing
	default:
		return rrdbinary.Architecture{}, rrdbinary.NewBinError(fmt.Sprintf("can't handle RRD file version %q", header.Version), data, 4, 5)
	}

	// 3. 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.
	arch.DoubleWidth = 8
	magicFloat := float64(8.642135e130)
	floatAddrPacked := 9
	floatAddr32 := ((floatAddrPacked + 3) / 4) * 4
	floatAddr64 := ((floatAddrPacked + 7) / 8) * 8
	var restOffset int
	switch {
	case len(data) < floatAddr32+arch.DoubleWidth:
		return rrdbinary.Architecture{}, rrdbinary.NewBinError("unexpected end-of-file", data, floatAddrPacked, floatAddr64+arch.DoubleWidth-floatAddrPacked)
	case math.Float64frombits(binary.LittleEndian.Uint64(data[floatAddr32:])) == magicFloat:
		arch.ByteOrder = binary.LittleEndian
		arch.DoubleAlign = 4
		restOffset = floatAddr32 + arch.DoubleWidth
	case math.Float64frombits(binary.BigEndian.Uint64(data[floatAddr32:])) == magicFloat:
		arch.ByteOrder = binary.BigEndian
		arch.DoubleAlign = 4
		restOffset = floatAddr32 + arch.DoubleWidth
	case len(data) < floatAddr64+arch.DoubleWidth:
		return rrdbinary.Architecture{}, rrdbinary.NewBinError("unexpected end-of-file", data, floatAddrPacked, floatAddr64+arch.DoubleWidth-floatAddrPacked)
	case math.Float64frombits(binary.LittleEndian.Uint64(data[floatAddr64:])) == magicFloat:
		arch.ByteOrder = binary.LittleEndian
		arch.DoubleAlign = 8
		restOffset = floatAddr64 + arch.DoubleWidth
	case math.Float64frombits(binary.BigEndian.Uint64(data[floatAddr64:])) == magicFloat:
		arch.ByteOrder = binary.BigEndian
		arch.DoubleAlign = 8
		restOffset = floatAddr64 + arch.DoubleWidth
	default:
		return rrdbinary.Architecture{}, rrdbinary.NewBinError("failed to sniff byte-order and float-alignment",
			data, floatAddrPacked, floatAddr64+arch.DoubleWidth-floatAddrPacked)
	}

	// 5, 6. ds_cnt, rra_cnt
	switch arch.DoubleAlign {
	case 4:
		// Assume that if floats are only 32-bit aligned, then everything is 32-bit
		arch.LongWidth = 4
		arch.LongAlign = 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 rrdbinary.Architecture{}, rrdbinary.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
		}[arch.ByteOrder]
		if arch.ByteOrder.Uint32(data[offset:]) == 0 {
			arch.LongWidth = 8
			arch.LongAlign = 8
		} else {
			arch.LongWidth = 4
			arch.LongAlign = 4
		}
	}

	// The above just os happens that DoubleXXX >= LongXXX, so we
	// can just set the Unival stuff to the Double Stuff.
	arch.UnivalWidth = arch.DoubleWidth // max(DoubleWidth, LongWidth)
	arch.UnivalAlign = arch.DoubleAlign // max(DoubleAlign, LongAlign)

	// FIXME: Figure out how to sniff the sizeof(time_t).
	//
	// javascriptRRD assumes that it's the same as sizeof(long),
	// which his historically been true, but
	//  - isn't true of proprietary 32-bit Unixen that are 2038-safe
	//  - isn't true of the Linux kernel with the x32 ABI
	arch.TimeWidth = arch.LongWidth
	arch.TimeAlign = arch.LongAlign

	return arch, nil
}