Files
windows_exporter/internal/headers/clusapi/properties_test.go
T
2026-10-11 13:52:55 +02:00

364 lines
10 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package clusapi
import (
"encoding/binary"
"testing"
"unicode/utf16"
)
func testPropertyList() []byte {
data := binary.LittleEndian.AppendUint32(nil, 1)
name := make([]byte, 0)
for _, unit := range utf16.Encode([]rune("NodeWeight\x00")) {
name = binary.LittleEndian.AppendUint16(name, unit)
}
data = binary.LittleEndian.AppendUint32(data, propertyName)
data = binary.LittleEndian.AppendUint32(data, uint32(len(name)))
data = append(data, name...)
for len(data)%4 != 0 {
data = append(data, 0)
}
data = binary.LittleEndian.AppendUint32(data, propertyValue|formatDWORD)
data = binary.LittleEndian.AppendUint32(data, 4)
data = binary.LittleEndian.AppendUint32(data, ^uint32(0))
return binary.LittleEndian.AppendUint32(data, 0)
}
func TestParseProperties(t *testing.T) {
data := testPropertyList()
properties, err := ParseProperties(data)
if err != nil {
t.Fatal(err)
}
values := properties["NodeWeight"]
if len(values) != 1 {
t.Fatalf("values = %v", values)
}
value, err := values[0].DWORD()
if err != nil || value != ^uint32(0) {
t.Fatalf("value = %d, err = %v", value, err)
}
for i := range data {
if _, err := ParseProperties(data[:i]); err == nil {
t.Errorf("accepted truncation at %d", i)
}
}
}
func TestParsePropertiesInvalid(t *testing.T) {
tests := map[string]func([]byte){
"count": func(data []byte) { binary.LittleEndian.PutUint32(data, ^uint32(0)) },
"syntax": func(data []byte) { binary.LittleEndian.PutUint32(data[4:], propertyValue|formatString) },
"length": func(data []byte) { binary.LittleEndian.PutUint32(data[8:], ^uint32(0)) },
"dword_length": func(data []byte) { binary.LittleEndian.PutUint32(data[40:], 3) },
"endmark": func(data []byte) { binary.LittleEndian.PutUint32(data[len(data)-4:], propertyValue) },
}
for name, mutate := range tests {
t.Run(name, func(t *testing.T) {
data := testPropertyList()
mutate(data)
if _, err := ParseProperties(data); err == nil {
t.Fatal("accepted invalid property list")
}
})
}
for _, trailing := range [][]byte{{1}, {1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0x80}} {
if _, err := ParseProperties(append(testPropertyList(), trailing...)); err == nil {
t.Fatalf("accepted trailing data % x", trailing)
}
}
}
// Bounds safety does not depend on the trailer, so any zero padding is accepted.
func TestParsePropertiesZeroTrailer(t *testing.T) {
for _, trailing := range [][]byte{{0}, {0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}} {
properties, err := ParseProperties(append(testPropertyList(), trailing...))
if err != nil {
t.Fatalf("rejected zero trailer % x: %v", trailing, err)
}
if _, exists := properties["NodeWeight"]; !exists {
t.Fatalf("lost property with zero trailer % x", trailing)
}
}
}
// String contents are not validated while parsing. They decode like WMI strings:
// up to the first NUL, with invalid UTF-16 replaced by U+FFFD.
func TestParsePropertiesLenientStrings(t *testing.T) {
data := testPropertyList()
binary.LittleEndian.PutUint16(data[12:], 0xd800)
properties, err := ParseProperties(data)
if err != nil {
t.Fatal(err)
}
if _, exists := properties["�odeWeight"]; !exists {
t.Fatalf("unpaired surrogate was not replaced: %v", properties)
}
data = testPropertyList()
binary.LittleEndian.PutUint16(data[20:], 0)
properties, err = ParseProperties(data)
if err != nil {
t.Fatal(err)
}
if _, exists := properties["Node"]; !exists {
t.Fatalf("embedded NUL did not end the name: %v", properties)
}
for _, tc := range []struct {
value Property
want string
}{
{Property{Format: formatString, Data: []byte{'a', 0, 0, 0}}, "a"},
{Property{Format: formatExpandString, Data: []byte{'a', 0, 0, 0xd8, 'b', 0}}, "a�b"},
{Property{Format: formatExpandedString, Data: []byte{'a', 0, 'b'}}, "a"},
{Property{Format: formatString, Data: nil}, ""},
} {
got, err := tc.value.String()
if err != nil || got != tc.want {
t.Errorf("String(% x) = %q, %v, want %q", tc.value.Data, got, err, tc.want)
}
}
if _, err := (Property{Format: formatDWORD, Data: make([]byte, 4)}).String(); err == nil {
t.Fatal("decoded DWORD as string")
}
}
// The cluster service terminates the whole list with CLUSPROP_SYNTAX_ENDMARK.
func TestParsePropertiesListEndmark(t *testing.T) {
data := binary.LittleEndian.AppendUint32(testPropertyList(), 0)
properties, err := ParseProperties(data)
if err != nil {
t.Fatal(err)
}
if value, err := properties["NodeWeight"][0].DWORD(); err != nil || value != ^uint32(0) {
t.Fatalf("value = %d, err = %v", value, err)
}
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(nil, 0)); err != nil {
t.Fatalf("empty list: %v", err)
}
if _, err := ParseProperties(binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, 0), 0)); err != nil {
t.Fatalf("empty list with endmark: %v", err)
}
}
func testLongPropertyList() []byte {
data := testPropertyList()
binary.LittleEndian.PutUint32(data[36:], propertyValue|formatLong)
return data
}
func TestPropertyValue32(t *testing.T) {
properties, err := ParseProperties(testLongPropertyList())
if err != nil {
t.Fatal(err)
}
value := properties["NodeWeight"][0]
if _, err := value.DWORD(); err == nil {
t.Fatal("LONG accepted as DWORD")
}
bits, err := value.Value32()
if err != nil || int32(bits) != -1 {
t.Fatalf("LONG bits = %#x, err = %v", bits, err)
}
if _, err := (Property{Format: formatString, Data: []byte{0, 0, 0, 0}}).Value32(); err == nil {
t.Fatal("string accepted as 32-bit value")
}
if _, err := (Property{Format: formatLong, Data: []byte{0, 0}}).Value32(); err == nil {
t.Fatal("short LONG accepted")
}
invalid := testLongPropertyList()
binary.LittleEndian.PutUint32(invalid[40:], 3)
if _, err := ParseProperties(invalid); err == nil {
t.Fatal("accepted LONG with invalid length")
}
}
func FuzzParseProperties(f *testing.F) {
f.Add(testPropertyList())
f.Add(testLongPropertyList())
f.Add(binary.LittleEndian.AppendUint32(testPropertyList(), 0))
f.Add([]byte{0, 0, 0, 0})
f.Fuzz(func(t *testing.T, data []byte) { _, _ = ParseProperties(data) })
}
func BenchmarkParseProperties(b *testing.B) {
data := testPropertyList()
b.ReportAllocs()
for b.Loop() {
if _, err := ParseProperties(data); err != nil {
b.Fatal(err)
}
}
}
func encodeUTF16(units ...uint16) []byte {
data := make([]byte, 0, len(units)*2)
for _, unit := range units {
data = binary.LittleEndian.AppendUint16(data, unit)
}
return data
}
// referenceDecode is the utf16.Decode based decoding decodeString replaced.
func referenceDecode(data []byte) string {
units := make([]uint16, len(data)/2)
for i := range units {
units[i] = binary.LittleEndian.Uint16(data[i*2:])
}
for i, unit := range units {
if unit == 0 {
units = units[:i]
break
}
}
return string(utf16.Decode(units))
}
func TestDecodeString(t *testing.T) {
tests := map[string][]uint16{
"empty": {},
"ascii": utf16.Encode([]rune("Cluster IP Address")),
"nul": append(utf16.Encode([]rune("Type")), 0, 'x'),
"pair": utf16.Encode([]rune("disk 💾")),
"unpaired_high": {'a', 0xd83d, 'b'},
"unpaired_low": {'a', 0xdcbe, 'b'},
"high_before_nul": {'a', 0xd83d, 0, 'b'},
"high_at_end": {'a', 0xd83d},
"two_highs_then_low": {0xd83d, 0xd83d, 0xdcbe},
"replacement_char_bmp": {0xfffd},
}
for name, units := range tests {
t.Run(name, func(t *testing.T) {
data := encodeUTF16(units...)
if got, want := decodeString(data), referenceDecode(data); got != want {
t.Errorf("decodeString = %q, want %q", got, want)
}
if got, want := decodeUnits(units), referenceDecode(data); got != want {
t.Errorf("decodeUnits = %q, want %q", got, want)
}
})
}
if got := decodeString([]byte{'a', 0, 'b'}); got != "a" {
t.Errorf("odd length = %q, want %q", got, "a")
}
}
func FuzzDecodeString(f *testing.F) {
f.Add(encodeUTF16(utf16.Encode([]rune("Name 💾"))...))
f.Add(encodeUTF16('a', 0xd83d, 'b'))
f.Fuzz(func(t *testing.T, data []byte) {
if got, want := decodeString(data), referenceDecode(data); got != want {
t.Fatalf("decodeString(% x) = %q, want %q", data, got, want)
}
})
}
func appendTestProperty(data []byte, name string, values ...uint32) []byte {
encoded := encodeUTF16(append(utf16.Encode([]rune(name)), 0)...)
data = binary.LittleEndian.AppendUint32(data, propertyName)
data = binary.LittleEndian.AppendUint32(data, uint32(len(encoded)))
data = append(data, encoded...)
for len(data)%4 != 0 {
data = append(data, 0)
}
for _, value := range values {
data = binary.LittleEndian.AppendUint32(data, propertyValue|formatDWORD)
data = binary.LittleEndian.AppendUint32(data, 4)
data = binary.LittleEndian.AppendUint32(data, value)
}
return binary.LittleEndian.AppendUint32(data, 0)
}
// The value lists share one backing array; values of one property must not
// leak into another.
func TestParsePropertiesMultipleValues(t *testing.T) {
data := binary.LittleEndian.AppendUint32(nil, 3)
data = appendTestProperty(data, "First", 1, 2)
data = appendTestProperty(data, "Second", 3)
data = appendTestProperty(data, "Third", 4, 5, 6)
properties, err := ParseProperties(data)
if err != nil {
t.Fatal(err)
}
for name, want := range map[string][]uint32{"First": {1, 2}, "Second": {3}, "Third": {4, 5, 6}} {
values := properties[name]
if len(values) != len(want) {
t.Fatalf("%s: got %d values, want %d", name, len(values), len(want))
}
for i, value := range values {
if got, err := value.DWORD(); err != nil || got != want[i] {
t.Errorf("%s[%d] = %d, %v, want %d", name, i, got, err, want[i])
}
}
}
}
func BenchmarkDecodeString(b *testing.B) {
data := encodeUTF16(utf16.Encode([]rune("Microsoft Windows Server 2022 Datacenter"))...)
b.ReportAllocs()
for b.Loop() {
_ = decodeString(data)
}
}