diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c218ca01..f435131fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2473,6 +2473,13 @@ if (BUILD_TESTS) PRIVATE libfastfetch ) + add_executable(fastfetch-test-gpu-pci + tests/gpu_pci.c + ) + target_link_libraries(fastfetch-test-gpu-pci + PRIVATE libfastfetch + ) + enable_testing() add_test(NAME test-cache COMMAND fastfetch-test-cache) add_test(NAME test-logo COMMAND fastfetch-test-logo) @@ -2492,6 +2499,7 @@ if (BUILD_TESTS) add_test(NAME test-temps COMMAND fastfetch-test-temps) add_test(NAME test-endian COMMAND fastfetch-test-endian) add_test(NAME test-endian-bigendian COMMAND fastfetch-test-endian-bigendian) + add_test(NAME test-gpu-pci COMMAND fastfetch-test-gpu-pci) endif() ################## diff --git a/src/detection/gpu/gpu.h b/src/detection/gpu/gpu.h index 546de0749..584a18b70 100644 --- a/src/detection/gpu/gpu.h +++ b/src/detection/gpu/gpu.h @@ -80,6 +80,8 @@ typedef struct FFGpuDriverPciBusId { } FFGpuDriverPciBusId; #if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__) +#define FF_GPU_HAS_PCI_IDS 1 +void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu); diff --git a/src/detection/gpu/gpu_pci.c b/src/detection/gpu/gpu_pci.c index eb1b874f6..2aa98745c 100644 --- a/src/detection/gpu/gpu_pci.c +++ b/src/detection/gpu/gpu_pci.c @@ -60,62 +60,78 @@ static const FFstrbuf* loadPciIds() { return &pciids; } -static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { +void ffGPUParsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { FF_DEBUG("Searching pci.ids for vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass); if (content->length) { + const char* const fileEnd = content->chars + content->length; char buffer[32]; // Search for vendor uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor); - char* start = (char*) memmem(content->chars, content->length, buffer, len); - char* end = content->chars + content->length; + const char* start = memmem(content->chars, content->length, buffer, len); if (start) { FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor); start += len; - end = memchr(start, '\n', (uint32_t) (end - start)); - if (!end) { - end = content->chars + content->length; + const char* vendorEnd = memchr(start, '\n', (size_t) (fileEnd - start)); + if (!vendorEnd) { + vendorEnd = fileEnd; } if (!gpu->vendor.length) { - ffStrbufSetNS(&gpu->vendor, (uint32_t) (end - start), start); + ffStrbufSetNS(&gpu->vendor, (uint32_t) (vendorEnd - start), start); } - start = end; // point to '\n' of vendor - end = start + 1; // point to start of devices - // find the start of next vendor - while (end[0] == '\t' || end[0] == '#') { - end = strchr(end, '\n'); - if (!end) { - end = content->chars + content->length; - break; - } else { - end++; - } - } + // Search for device. Device lines are `\tXXXX name`; their ids only + // grow inside a vendor block, so one linear walk over the block is + // enough: compare in place and stop either at the line starting the + // next vendor, or -- because the ids are sorted -- right after the + // first id greater than the one we are looking for. Doing the compare + // here also removes the need to know where the block ends, so no + // length is ever derived from a pointer that may have run past it. + // pci.ids only asks contributors to keep the entries sorted, and it + // is strictly lowercase hex, so the byte comparison below matches the + // numeric order. A block that is not sorted (only reachable via + // FF_CUSTOM_PCI_IDS_PATH) makes us give up early and fall back to the + // synthesized name below. + char deviceId[8]; + snprintf(deviceId, ARRAY_SIZE(deviceId), "%04x", device); + [[maybe_unused]] bool found = false; + for (const char* p = vendorEnd < fileEnd ? vendorEnd + 1 : fileEnd; + p < fileEnd && (*p == '\t' || *p == '#');) { + if (*p == '\t' && fileEnd - p >= 7) { + int order = memcmp(p + 1, deviceId, 4); + if (order > 0) { + break; + } + if (order == 0 && p[5] == ' ' && p[6] == ' ') { + FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device); + start = p + 7; + const char* end = memchr(start, '\n', (size_t) (fileEnd - start)); + if (!end) { + end = fileEnd; + } - // Search for device - len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x ", device); - start = memmem(start, (size_t) (end - start), buffer, len); - if (start) { - FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device); - start += len; - end = memchr(start, '\n', (uint32_t) (end - start)); - if (!end) { - end = content->chars + content->length; - } - - char* closingBracket = end - 1; - if (*closingBracket == ']') { - char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start)); - if (openingBracket) { - openingBracket++; - ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + const char* closingBracket = end - 1; + if (*closingBracket == ']') { + const char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start)); + if (openingBracket) { + openingBracket++; + ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + } + } + if (!gpu->name.length) { + ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start); + } + found = true; + break; } } - if (!gpu->name.length) { - ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start); + p = memchr(p, '\n', (size_t) (fileEnd - p)); + if (!p) { + break; } - } else { + p++; + } + if (!found) { FF_DEBUG("PCI device entry 0x%04x:0x%04x was not found in pci.ids", vendor, device); } } else { @@ -231,7 +247,7 @@ void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, return; } #endif - parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); + ffGPUParsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); FF_DEBUG("Resolved PCI GPU name from pci.ids: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars); } diff --git a/tests/gpu_pci.c b/tests/gpu_pci.c new file mode 100644 index 000000000..a7c430a9e --- /dev/null +++ b/tests/gpu_pci.c @@ -0,0 +1,201 @@ +// Tests for ffGPUParsePciIdsFile(), the pci.ids lookup that resolves a PCI GPU name on Linux and +// the BSDs. +// +// The parser takes the file content as a buffer instead of opening the file itself, which is +// exactly what makes it testable: a small hand written pci.ids excerpt is enough, and the +// expectations stay readable. +// +// The function is only compiled where libfastfetch ships the pci.ids based detection, so on other +// platforms this test has nothing to link against and only reports success. + +#include "common/textModifier.h" +#include "detection/gpu/gpu.h" + +#include +#include + +#if FF_GPU_HAS_PCI_IDS + +static void verifyPciEx( + const char* content, uint8_t subclass, uint16_t vendor, uint16_t device, + const char* presetVendor, const char* presetName, + const char* expectedVendor, const char* expectedName, + int lineNo +) { + FFGPUResult gpu = {}; + ffStrbufInit(&gpu.vendor); + ffStrbufInit(&gpu.name); + + // A caller may already know the vendor (for example from the PCI subsystem), in which case the + // parser must not clobber it. + if (presetVendor) { + ffStrbufSetS(&gpu.vendor, presetVendor); + } + if (presetName) { + ffStrbufSetS(&gpu.name, presetName); + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS(content); + ffGPUParsePciIdsFile(&buffer, subclass, vendor, device, &gpu); + + if (!ffStrbufEqualS(&gpu.vendor, expectedVendor) || !ffStrbufEqualS(&gpu.name, expectedName)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] ffGPUParsePciIdsFile(vendor=0x%04x, device=0x%04x, subclass=%u): expected vendor=\"%s\" name=\"%s\", got vendor=\"%s\" name=\"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, + lineNo, vendor, device, (unsigned) subclass, expectedVendor, expectedName, gpu.vendor.chars, gpu.name.chars); + exit(1); + } + + ffStrbufDestroy(&gpu.vendor); + ffStrbufDestroy(&gpu.name); +} + +#define VERIFY_PCI(content, subclass, vendor, device, expectedVendor, expectedName) \ + verifyPciEx((content), (subclass), (vendor), (device), nullptr, nullptr, (expectedVendor), (expectedName), __LINE__) + +#define VERIFY_PCI_EX(content, subclass, vendor, device, presetVendor, presetName, expectedVendor, expectedName) \ + verifyPciEx((content), (subclass), (vendor), (device), (presetVendor), (presetName), (expectedVendor), (expectedName), __LINE__) + +// A miniature pci.ids. Vendors are ordered, device ids grow within a block, and the block is closed +// by a blank line -- the same shape the real file has. +static const char PCI_IDS[] = + "# pci.ids excerpt used by the tests\n" + "#\n" + "1a2b Vendor Alpha\n" + "\t0001 Alpha One\n" + "\t0005 Alpha Five\n" + "\t\t0005 0abc Subsystem that must be ignored\n" + "\t00ff Alpha FF [Alias]\n" + "\t1000 Alpha Thousand\n" + "\n" + "1a2c Vendor Beta\n" + "\t0001 Beta One\n" + "\t0002 Beta Two\n" + "\t1234 Beta TwelveThirtyFour\n" + "\n" + "abcd Vendor Delta\n" + "\t0e1f Delta Device\n" + "\n" + "ffff Vendor NoDevices\n" + "\n" + "C 00 Unclassified device\n"; + +// The last line has no terminating newline, so the parser has to fall back to the end of the +// buffer. +static const char PCI_IDS_NO_TRAILING_NL[] = + "#\n" + "1a2b Vendor Alpha\n" + "\t0001 Alpha One"; + +// A vendor with no device lines at all, again without a terminating newline. +static const char PCI_IDS_VENDOR_ONLY[] = + "#\n" + "1a2b Vendor Alpha"; + +int main(void) { + // An empty buffer is not an error: the name is synthesized from the vendor and device id. + { + VERIFY_PCI("", 0, 0x1a2b, 0x0001, "", "Unknown Device 0001 (VGA compatible)"); + VERIFY_PCI("", 2, 0x1a2b, 0x0001, "", "Unknown Device 0001 (3D)"); + } + + // The happy path: vendor and device are both present. + { + VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0005, "Vendor Alpha", "Alpha Five"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1000, "Vendor Alpha", "Alpha Thousand"); + } + + // Hex letters in the ids work on both sides. + { + VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0x0e1f, "Vendor Delta", "Delta Device"); + } + + // A name of the form `... [Alias]` is reduced to the alias. Only the trailing bracket pair is + // considered, and a `[` without a matching `]` at the end is kept verbatim. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x00ff, "Vendor Alpha", "Alias"); + } + + // The lookup must stay inside the vendor block. `Beta` also has a 0x1234, but a lookup under + // `Alpha` must not find it -- `Alpha` has no 0x1234 of its own. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2c, 0x1234, "Vendor Beta", "Beta TwelveThirtyFour"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x1234, "Vendor Alpha", "Vendor Alpha Device 1234 (VGA compatible)"); + } + + // An unknown device under a known vendor synthesizes a name from the vendor. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 1, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (XGA compatible)"); + VERIFY_PCI(PCI_IDS, 2, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (3D)"); + VERIFY_PCI(PCI_IDS, 5, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002"); + } + + // The synthesized name uses the upper case form of the device id, while the lookup needle is + // lower case. + { + VERIFY_PCI(PCI_IDS, 0, 0xabcd, 0xbeef, "Vendor Delta", "Vendor Delta Device BEEF (VGA compatible)"); + } + + // A device id below the first entry, between two entries and above the last one all miss. The + // first case is also what the "stop once the ids grow past the target" shortcut relies on. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0000, "Vendor Alpha", "Vendor Alpha Device 0000 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0020, "Vendor Alpha", "Vendor Alpha Device 0020 (VGA compatible)"); + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x9999, "Vendor Alpha", "Vendor Alpha Device 9999 (VGA compatible)"); + } + + // A subsystem id lives at twice the indentation and must never be mistaken for a device id, + // even when it is the value being searched for. + { + VERIFY_PCI(PCI_IDS, 0, 0x1a2b, 0x0abc, "Vendor Alpha", "Vendor Alpha Device 0ABC (VGA compatible)"); + } + + // A vendor with no device lines falls back to the synthesized name, and the vendor is still + // reported. + { + VERIFY_PCI(PCI_IDS, 0, 0xffff, 0x0001, "Vendor NoDevices", "Vendor NoDevices Device 0001 (VGA compatible)"); + } + + // An unknown vendor leaves the vendor buffer empty, so the synthesized name says "Unknown". + { + VERIFY_PCI(PCI_IDS, 0, 0x1234, 0x5678, "", "Unknown Device 5678 (VGA compatible)"); + } + + // Lines beyond the vendor section (the class list) are not reachable from a vendor block. + { + VERIFY_PCI(PCI_IDS, 0, 0x000c, 0x0000, "", "Unknown Device 0000 (VGA compatible)"); + } + + // A device line as the very last line of the buffer, with no trailing newline. + { + VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Alpha One"); + VERIFY_PCI(PCI_IDS_NO_TRAILING_NL, 0, 0x1a2b, 0x0002, "Vendor Alpha", "Vendor Alpha Device 0002 (VGA compatible)"); + } + + // A vendor line as the very last line of the buffer, with no device lines after it. + { + VERIFY_PCI(PCI_IDS_VENDOR_ONLY, 0, 0x1a2b, 0x0001, "Vendor Alpha", "Vendor Alpha Device 0001 (VGA compatible)"); + } + + // A known vendor is not overwritten, but resolving the device name still happens. + { + VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, "Custom Vendor", nullptr, "Custom Vendor", "Alpha One"); + } + + // Likewise a name the caller already resolved is kept when the entry is a plain one. + { + VERIFY_PCI_EX(PCI_IDS, 0, 0x1a2b, 0x0001, nullptr, "Custom Name", "Vendor Alpha", "Custom Name"); + } + + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} + +#else // FF_TEST_HAS_PCI_IDS + +int main(void) { + puts("\033[32mTests skipped. (pci.ids parser not available on this platform)" FASTFETCH_TEXT_MODIFIER_RESET); + return 0; +} + +#endif