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{}