Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/dev.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ void print_devices(uint16_t vendorid, uint16_t productid)
cur->vendor_id, cur->product_id, cur->path);

if (cur->serial_number) {
std::cout << " Serial: " << headsetcontrol::wstring_to_string(cur->serial_number) << '\n';
std::cout << " Serial: " << headsetcontrol::wstring_to_utf8(cur->serial_number) << '\n';
}
if (cur->manufacturer_string) {
std::cout << " Manufacturer: " << headsetcontrol::wstring_to_string(cur->manufacturer_string) << '\n';
std::cout << " Manufacturer: " << headsetcontrol::wstring_to_utf8(cur->manufacturer_string) << '\n';
}
if (cur->product_string) {
std::cout << " Product: " << headsetcontrol::wstring_to_string(cur->product_string) << '\n';
std::cout << " Product: " << headsetcontrol::wstring_to_utf8(cur->product_string) << '\n';
}

std::cout << std::format(
Expand Down
20 changes: 20 additions & 0 deletions cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "hid_utility.hpp"
#include "output.hpp"
#include "result_types.hpp"
#include "string_utils.hpp"
#include "utility.hpp"
#include "version.h"

Expand Down Expand Up @@ -431,6 +432,7 @@ struct DiscoveredDevice {
std::vector<FeatureRequest> feature_requests;
std::wstring vendor_name;
std::wstring product_name;
bool metadata_queried = false;

[[nodiscard]] uint16_t vendorId() const
{
Expand Down Expand Up @@ -513,6 +515,23 @@ std::vector<DiscoveredDevice> discoverDevices(const Options& opts)
// Feature handling
// ============================================================================

/**
* @brief Let the device refine the product name once it is open.
*
* A device may know more than its USB strings, e.g. which headset is currently
* paired to a generic dongle. The default getMetadata() returns the HID strings,
* so devices without such knowledge are unaffected.
*/
static void refineProductName(DiscoveredDevice& dev, hid_device* handle)
{
if (dev.metadata_queried)
return;
dev.metadata_queried = true;

if (auto meta = dev.device->getMetadata(handle); meta && !meta->product.empty())
dev.product_name = headsetcontrol::string_to_wstring(meta->product);
}

hid_device* connectForCapability(HIDConnection& conn, const HIDDevice* device, uint16_t product_id, capabilities cap)
{
auto detail = device->getCapabilityDetail(cap);
Expand Down Expand Up @@ -584,6 +603,7 @@ FeatureResult handleFeature(DiscoveredDevice& dev, capabilities cap, const Featu
if (!handle) {
return make_error(-1, "Could not open device");
}
refineProductName(dev, handle);
}

// Execute via handler registry (no more giant switch!)
Expand Down
6 changes: 3 additions & 3 deletions cli/output/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ void outputYaml(const OutputData& data)
s.writeListItem("status", statusToString(dev.status));
s.pushIndent(1); // Align subsequent keys with "status" after "- "
s.write("device", dev.device_name);
s.write("vendor", dev.vendor_name.empty() ? "" : headsetcontrol::wstring_to_string(dev.vendor_name.c_str()));
s.write("product", dev.product_name.empty() ? "" : headsetcontrol::wstring_to_string(dev.product_name.c_str()));
s.write("vendor", dev.vendor_name.empty() ? "" : headsetcontrol::wstring_to_utf8(dev.vendor_name));
s.write("product", dev.product_name.empty() ? "" : headsetcontrol::wstring_to_utf8(dev.product_name));
s.write("id_vendor", dev.vendor_id);
s.write("id_product", dev.product_id);

Expand Down Expand Up @@ -463,7 +463,7 @@ void outputStandard(const OutputData& data, bool print_capabilities)

for (const auto& dev : data.devices) {
if (!dev.product_name.empty()) {
s.println(" {} ({}) [{}:{}]", dev.device_name, headsetcontrol::wstring_to_string(dev.product_name.c_str()), dev.vendor_id, dev.product_id);
s.println(" {} ({}) [{}:{}]", dev.device_name, headsetcontrol::wstring_to_utf8(dev.product_name), dev.vendor_id, dev.product_id);
} else {
s.println(" {} [{}:{}]", dev.device_name, dev.vendor_id, dev.product_id);
}
Expand Down
4 changes: 2 additions & 2 deletions cli/output/output_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ struct DeviceData {
s.beginObject("");
s.write("status", statusToString(status));
s.write("device", device_name);
s.write("vendor", vendor_name.empty() ? "" : headsetcontrol::wstring_to_string(vendor_name.c_str()));
s.write("product", product_name.empty() ? "" : headsetcontrol::wstring_to_string(product_name.c_str()));
s.write("vendor", vendor_name.empty() ? "" : headsetcontrol::wstring_to_utf8(vendor_name));
s.write("product", product_name.empty() ? "" : headsetcontrol::wstring_to_utf8(product_name));
s.write("id_vendor", vendor_id);
s.write("id_product", product_id);

Expand Down
6 changes: 3 additions & 3 deletions lib/devices/hid_device.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ class HIDDevice {
wchar_t serial[128] = {};

if (hid_get_manufacturer_string(device_handle, manufacturer, 128) == 0) {
meta.manufacturer = wstring_to_string(manufacturer);
meta.manufacturer = wstring_to_utf8(manufacturer);
}

if (hid_get_product_string(device_handle, product, 128) == 0) {
meta.product = wstring_to_string(product);
meta.product = wstring_to_utf8(product);
}

if (hid_get_serial_number_string(device_handle, serial, 128) == 0 && serial[0] != 0) {
meta.serial_number = wstring_to_string(serial);
meta.serial_number = wstring_to_utf8(serial);
}

return meta;
Expand Down
2 changes: 1 addition & 1 deletion lib/headsetcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace {

std::string hidStringOrEmpty(const wchar_t* value)
{
return value ? wstring_to_string(value) : std::string();
return value ? wstring_to_utf8(value) : std::string();
}

class LibraryState {
Expand Down
173 changes: 173 additions & 0 deletions lib/string_utils.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#pragma once

#include <cstddef>
#include <cstdlib>
#include <string>
#include <string_view>
#include <type_traits>

namespace headsetcontrol {

Expand Down Expand Up @@ -60,4 +63,174 @@ inline std::string wstring_to_string(const wchar_t* wstr)
#endif
}

/**
* @brief Convert UTF-8 string to wide string
*
* Inverse of wstring_to_string() for the UTF-8 strings carried in DeviceMetadata.
* The decoding is written out rather than delegated to mbstowcs() so that it does
* not depend on the process locale: a device-supplied name arrives as raw bytes
* off the wire and never passes through the C library's conversion on the way in.
*
* Widening byte by byte instead would produce one wide character per byte, so a
* name like "Muller" spelled with an umlaut would come back out mangled.
*
* Malformed input is replaced with U+FFFD rather than rejected, so one bad byte
* in a name read from a device does not discard the rest of it.
*
* @param str UTF-8 string
* @return Wide string: UTF-16 where wchar_t is 16 bits, UTF-32 where it is wider
*/
inline std::wstring string_to_wstring(std::string_view str)
{
constexpr char32_t REPLACEMENT = 0xFFFD;
constexpr char32_t MAX_CODEPOINT = 0x10FFFF;

std::wstring result;
result.reserve(str.size());

auto append = [&result](char32_t codepoint) {
if constexpr (sizeof(wchar_t) >= 4) {
result += static_cast<wchar_t>(codepoint);
} else if (codepoint <= 0xFFFF) {
result += static_cast<wchar_t>(codepoint);
} else {
// Split into a UTF-16 surrogate pair, as on Windows where wchar_t is 16 bits
const char32_t offset = codepoint - 0x10000;
result += static_cast<wchar_t>(0xD800 + (offset >> 10));
result += static_cast<wchar_t>(0xDC00 + (offset & 0x3FF));
}
};

for (std::size_t i = 0; i < str.size();) {
const auto lead = static_cast<unsigned char>(str[i]);

// Length of the sequence, and the smallest codepoint it may legally encode -
// a larger sequence than a codepoint needs is an overlong encoding
std::size_t continuations = 0;
char32_t codepoint = 0;
char32_t minimum = 0;
if (lead < 0x80) {
codepoint = lead;
} else if ((lead & 0xE0) == 0xC0) {
continuations = 1;
codepoint = lead & 0x1FU;
minimum = 0x80;
} else if ((lead & 0xF0) == 0xE0) {
continuations = 2;
codepoint = lead & 0x0FU;
minimum = 0x800;
} else if ((lead & 0xF8) == 0xF0) {
continuations = 3;
codepoint = lead & 0x07U;
minimum = 0x10000;
} else {
append(REPLACEMENT);
++i;
continue;
}

if (i + continuations >= str.size()) {
append(REPLACEMENT);
++i;
continue;
}

bool valid = true;
for (std::size_t k = 1; k <= continuations; ++k) {
const auto continuation = static_cast<unsigned char>(str[i + k]);
if ((continuation & 0xC0) != 0x80) {
valid = false;
break;
}
codepoint = (codepoint << 6) | (continuation & 0x3FU);
}

// Surrogates are not valid on their own, and are not encodable in UTF-8
const bool is_surrogate = codepoint >= 0xD800 && codepoint <= 0xDFFF;
if (!valid || codepoint < minimum || codepoint > MAX_CODEPOINT || is_surrogate) {
append(REPLACEMENT);
++i;
continue;
}

append(codepoint);
i += continuations + 1;
}

return result;
}

/**
* @brief Convert wide string to UTF-8 string
*
* Counterpart to string_to_wstring(), and an exact inverse of it. Unlike
* wstring_to_string() this does not consult the locale: it is for strings that are
* defined to be UTF-8, such as the device names in DeviceMetadata, where going
* through wcstombs() would replace anything outside the current locale's encoding
* with '?' - which in the default C locale means every non-ASCII character.
*
* Where wchar_t is 16 bits a surrogate pair is recombined into the codepoint it
* encodes; an unpaired surrogate is replaced with U+FFFD.
*
* @param str Wide string
* @return UTF-8 string
*/
inline std::string wstring_to_utf8(std::wstring_view str)
{
constexpr char32_t REPLACEMENT = 0xFFFD;
constexpr char32_t MAX_CODEPOINT = 0x10FFFF;

std::string result;
result.reserve(str.size());

auto append = [&result](char32_t codepoint) {
if (codepoint < 0x80) {
result += static_cast<char>(codepoint);
} else if (codepoint < 0x800) {
result += static_cast<char>(0xC0 | (codepoint >> 6));
result += static_cast<char>(0x80 | (codepoint & 0x3F));
} else if (codepoint < 0x10000) {
result += static_cast<char>(0xE0 | (codepoint >> 12));
result += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
result += static_cast<char>(0x80 | (codepoint & 0x3F));
} else {
result += static_cast<char>(0xF0 | (codepoint >> 18));
result += static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F));
result += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
result += static_cast<char>(0x80 | (codepoint & 0x3F));
}
};

for (std::size_t i = 0; i < str.size(); ++i) {
// Mask rather than cast: char32_t is unsigned, but wchar_t is signed on
// some platforms, and a plain conversion would sign-extend
auto codepoint = static_cast<char32_t>(
static_cast<std::make_unsigned_t<wchar_t>>(str[i]));

if (codepoint >= 0xD800 && codepoint <= 0xDBFF) {
// High surrogate: needs the matching low surrogate to mean anything
const bool has_low = i + 1 < str.size();
const auto low = has_low
? static_cast<char32_t>(static_cast<std::make_unsigned_t<wchar_t>>(str[i + 1]))
: char32_t { 0 };
if (has_low && low >= 0xDC00 && low <= 0xDFFF) {
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00);
++i;
} else {
codepoint = REPLACEMENT;
}
} else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) {
// Low surrogate without a high one before it
codepoint = REPLACEMENT;
}

if (codepoint > MAX_CODEPOINT) {
codepoint = REPLACEMENT;
}
append(codepoint);
}

return result;
}

} // namespace headsetcontrol
Loading