Files
windows_exporter/internal/headers/clusapi/properties_fixture_test.go
2026-10-10 17:52:05 +02:00

230 lines
7.5 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.
//go:build windows
package clusapi
import (
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
// CLUSPROP_FORMAT values that the collectors do not decode.
const (
formatBinary = 1
formatMultiString = 5
formatULargeInteger = 6
formatFileTime = 12
)
// Property lists in testdata were captured with TestDumpPropertyLists from the
// CI cluster (Windows Server 2022). Unlike lists built from the documentation,
// they end with the extra CLUSPROP_SYNTAX_ENDMARK of the cluster service.
func readPropertyFixture(tb testing.TB, name string) []byte {
tb.Helper()
text, err := os.ReadFile(filepath.Join("testdata", name))
if err != nil {
tb.Fatal(err)
}
data, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), ""))
if err != nil {
tb.Fatal(err)
}
return data
}
type fixtureValue struct {
format uint16
length int
dword *uint32
text *string
}
func dwordValue(value uint32) fixtureValue {
return fixtureValue{format: formatDWORD, length: 4, dword: &value}
}
func stringValue(value string, length int) fixtureValue {
return fixtureValue{format: formatString, length: length, text: &value}
}
func checkFixtureProperties(t *testing.T, properties map[string][]Property, count int, want map[string]fixtureValue) {
t.Helper()
if len(properties) != count {
t.Errorf("properties = %d, want %d", len(properties), count)
}
for name, value := range want {
values := properties[name]
if len(values) != 1 || values[0].Format != value.format || len(values[0].Data) != value.length {
t.Errorf("%s = %+v, want format %d with %d bytes", name, values, value.format, value.length)
continue
}
if value.dword != nil {
if got, err := values[0].DWORD(); err != nil || got != *value.dword {
t.Errorf("%s = %d, %v, want %d", name, got, err, *value.dword)
}
}
if value.text != nil {
if got, err := values[0].String(); err != nil || got != *value.text {
t.Errorf("%s = %q, %v, want %q", name, got, err, *value.text)
}
}
}
}
func TestParsePropertiesCapturedResourceLists(t *testing.T) {
// Read-only common properties: an SZ whose 38 bytes need 2 bytes of
// padding, ULARGE_INTEGER values and a DWORD.
properties, err := ParseProperties(readPropertyFixture(t, "resource_ro_common_generic_service.hex"))
if err != nil {
t.Fatal(err)
}
checkFixtureProperties(t, properties, 6, map[string]fixtureValue{
"Name": stringValue("CI Generic Service", 38),
"MonitorProcessId": dwordValue(9092),
"StatusInformation": {format: formatULargeInteger, length: 8},
"LastOperationStatusCode": {format: formatULargeInteger, length: 8},
"ResourceSpecificData1": {format: formatULargeInteger, length: 8},
"ResourceSpecificData2": {format: formatULargeInteger, length: 8},
})
// Common properties: the resource type name the collector publishes, empty
// strings (2 bytes, padded) and the DWORD values.
properties, err = ParseProperties(readPropertyFixture(t, "resource_common_generic_service.hex"))
if err != nil {
t.Fatal(err)
}
checkFixtureProperties(t, properties, 15, map[string]fixtureValue{
"Type": stringValue("Generic Service", 32),
"Description": stringValue("", 2),
"ResourceSpecificStatus": stringValue("", 2),
"DeadlockTimeout": dwordValue(300000),
"EmbeddedFailureAction": dwordValue(2),
"IsAlivePollInterval": dwordValue(^uint32(0)),
"LooksAlivePollInterval": dwordValue(^uint32(0)),
"PendingTimeout": dwordValue(180000),
"PersistentState": dwordValue(1),
"RestartAction": dwordValue(2),
"RestartDelay": dwordValue(500),
"RestartPeriod": dwordValue(600000),
"RestartThreshold": dwordValue(1),
"RetryPeriodOnFailure": dwordValue(600000),
"SeparateMonitor": dwordValue(0),
})
// Read-only private properties of an IP Address: FILETIME values.
properties, err = ParseProperties(readPropertyFixture(t, "resource_ro_private_ip_address.hex"))
if err != nil {
t.Fatal(err)
}
checkFixtureProperties(t, properties, 5, map[string]fixtureValue{
"LeaseObtainedTime": {format: formatFileTime, length: 8},
"LeaseExpiresTime": {format: formatFileTime, length: 8},
"DhcpServer": stringValue("255.255.255.255", 32),
"DhcpAddress": stringValue("0.0.0.0", 16),
"DhcpSubnetMask": stringValue("255.0.0.0", 20),
})
}
func TestParsePropertiesCapturedClusterList(t *testing.T) {
// The cluster common list is larger than the 4 KiB first buffer and holds
// BINARY security descriptors, MULTI_SZ lists, ULARGE_INTEGER and FILETIME
// values next to 64 DWORDs.
data := readPropertyFixture(t, "cluster_common.hex")
if len(data) <= propertyListBufferSize {
t.Fatalf("fixture has %d bytes, want more than the first buffer", len(data))
}
properties, err := ParseProperties(data)
if err != nil {
t.Fatal(err)
}
checkFixtureProperties(t, properties, 74, map[string]fixtureValue{
"Description": stringValue("", 2),
"PreferredSite": stringValue("", 2),
"Security Descriptor": {format: formatBinary, length: 396},
"SharedVolumeSecurityDescriptor": {format: formatBinary, length: 280},
"SharedVolumeCompatibleFilters": {format: formatMultiString, length: 2},
"SharedVolumeIncompatibleFilters": {format: formatMultiString, length: 2},
"EnabledEventLogs": {format: formatMultiString, length: 354},
"DumpPolicy": {format: formatULargeInteger, length: 8},
"S2DCacheBehavior": {format: formatULargeInteger, length: 8},
"RecentEventsResetTime": {format: formatFileTime, length: 8},
"ClusterEnforcedAntiaffinity": {format: formatDWORD, length: 4},
})
dwords := 0
for _, values := range properties {
if _, err := values[0].Value32(); err == nil {
dwords++
}
}
if dwords != 64 {
t.Errorf("32-bit values = %d, want 64", dwords)
}
// Every truncation of a real list must be rejected without a panic.
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex"} {
data := readPropertyFixture(t, name)
for index := range len(data) - 4 {
if _, err := ParseProperties(data[:index]); err == nil {
t.Errorf("%s: accepted truncation at %d", name, index)
}
}
}
}
func FuzzParsePropertiesCaptured(f *testing.F) {
for _, name := range []string{"resource_ro_common_generic_service.hex", "resource_common_generic_service.hex", "resource_ro_private_ip_address.hex", "cluster_common.hex"} {
f.Add(readPropertyFixture(f, name))
}
f.Fuzz(func(t *testing.T, data []byte) {
properties, err := ParseProperties(data)
if err != nil {
return
}
for _, values := range properties {
for _, value := range values {
_, _ = value.String()
_, _ = value.Value32()
if (value.Format == formatDWORD || value.Format == formatLong) && len(value.Data) != 4 {
t.Fatalf("32-bit value with %d bytes", len(value.Data))
}
}
}
})
}