From bd24870d2d5ee3e6ad50e09bfe5e4e4359d76bc1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 18 Nov 2025 19:34:28 +0900 Subject: [PATCH] Move minimal registry access to the extension Now win32/registry depends on fiddle, and its conversion is complex and too generic for the purpose of resolv. --- ext/win32/resolv/extconf.rb | 1 + ext/win32/resolv/lib/resolv.rb | 138 +++++++-------------- ext/win32/resolv/resolv.c | 206 +++++++++++++++++++++++++++++-- test/resolv/test_win32_config.rb | 110 +++-------------- 4 files changed, 260 insertions(+), 195 deletions(-) diff --git a/ext/win32/resolv/extconf.rb b/ext/win32/resolv/extconf.rb index a5f8cc2..5ee4c0d 100644 --- a/ext/win32/resolv/extconf.rb +++ b/ext/win32/resolv/extconf.rb @@ -1,5 +1,6 @@ require 'mkmf' if RUBY_ENGINE == "ruby" and have_library('iphlpapi', 'GetNetworkParams', ['windows.h', 'iphlpapi.h']) + have_library('advapi32', 'RegGetValueW', ['windows.h']) create_makefile('win32/resolv') else File.write('Makefile', "all clean install:\n\t@echo Done: $(@)\n") diff --git a/ext/win32/resolv/lib/resolv.rb b/ext/win32/resolv/lib/resolv.rb index 43ec10c..9a74a75 100644 --- a/ext/win32/resolv/lib/resolv.rb +++ b/ext/win32/resolv/lib/resolv.rb @@ -4,8 +4,23 @@ =end +require 'win32/resolv.so' + module Win32 module Resolv + # Error at Win32 API + class Error < StandardError + # +code+ Win32 Error code + # +message+ Formatted message for +code+ + def initialize(code, message) + super(message) + @code = code + end + + # Win32 error code + attr_reader :code + end + def self.get_hosts_path path = get_hosts_dir path = File.expand_path('hosts', path) @@ -29,121 +44,62 @@ def self.get_resolv_info end [ search, nameserver ] end - end -end - -begin - require 'win32/resolv.so' -rescue LoadError -end - -module Win32 -#==================================================================== -# Windows NT -#==================================================================== - module Resolv - begin - require 'win32/registry' - module SZ - refine Registry do - # ad hoc workaround for broken registry - def read_s(key) - type, str = read(key) - unless type == Registry::REG_SZ - warn "Broken registry, #{name}\\#{key} was #{Registry.type2name(type)}, ignored" - return String.new - end - str - end - end - end - using SZ - rescue LoadError, Gem::LoadError - require "open3" - end - - TCPIP_NT = 'SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' class << self private def get_hosts_dir - get_item_property(TCPIP_NT, 'DataBasePath', expand: true) + tcpip_params do |params| + params.value('DataBasePath') + end end def get_info search = nil nameserver = get_dns_server_list - slist = get_item_property(TCPIP_NT, 'SearchList') - search = slist.split(/,\s*/) unless slist.empty? + tcpip_params do |params| + slist = params.value('SearchList') + search = slist.split(/,\s*/) if slist and !slist.empty? - if add_search = search.nil? - search = [] - nvdom = get_item_property(TCPIP_NT, 'NV Domain') + if add_search = search.nil? + search = [] + nvdom = params.value('NV Domain') - unless nvdom.empty? - search = [ nvdom ] - udmnd = get_item_property(TCPIP_NT, 'UseDomainNameDevolution', dword: true) - if udmnd != 0 - if /^\w+\./ =~ nvdom - devo = $' + if nvdom and !nvdom.empty? + search = [ nvdom ] + udmnd = params.value('UseDomainNameDevolution') + if udmnd&.nonzero? + if /^\w+\./ =~ nvdom + devo = $' + end end end end - end - ifs = if defined?(Win32::Registry) - Registry::HKEY_LOCAL_MACHINE.open(TCPIP_NT + '\Interfaces') do |reg| - reg.keys - rescue Registry::Error - [] - end - else - cmd = "Get-ChildItem 'HKLM:\\#{TCPIP_NT}\\Interfaces' | ForEach-Object { $_.PSChildName }" - output, _ = Open3.capture2('powershell', '-Command', cmd) - output.split(/\n+/) + params.open('Interfaces') do |reg| + reg.each_key do |iface| + next unless ns = %w[NameServer DhcpNameServer].find do |key| + ns = iface.value(key) + break ns.split(/[,\s]\s*/) if ns and !ns.empty? end - ifs.each do |iface| - next unless ns = %w[NameServer DhcpNameServer].find do |key| - ns = get_item_property(TCPIP_NT + '\Interfaces' + "\\#{iface}", key) - break ns.split(/[,\s]\s*/) unless ns.empty? - end - - next if (nameserver & ns).empty? + next if (nameserver & ns).empty? - if add_search - [ 'Domain', 'DhcpDomain' ].each do |key| - dom = get_item_property(TCPIP_NT + '\Interfaces' + "\\#{iface}", key) - unless dom.empty? - search.concat(dom.split(/,\s*/)) - break + if add_search + [ 'Domain', 'DhcpDomain' ].each do |key| + dom = iface.value(key) + if dom and !dom.empty? + search.concat(dom.split(/,\s*/)) + break + end + end end end end - end - search << devo if add_search and devo - [ search.uniq, nameserver.uniq ] - end - def get_item_property(path, name, expand: false, dword: false) - if defined?(Win32::Registry) - begin - Registry::HKEY_LOCAL_MACHINE.open(path) do |reg| - if dword - reg.read_i(name) - else - expand ? reg.read_s_expand(name) : reg.read_s(name) - end - end - rescue Registry::Error - dword ? 0 : "" - end - else - cmd = "Get-ItemProperty -Path 'HKLM:\\#{path}' -Name '#{name}' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty '#{name}'" - output, _ = Open3.capture2('powershell', '-Command', cmd) - dword ? output.strip.to_i : output.strip + search << devo if add_search and devo end + [ search.uniq, nameserver.uniq ] end end end diff --git a/ext/win32/resolv/resolv.c b/ext/win32/resolv/resolv.c index af678e1..066856d 100644 --- a/ext/win32/resolv/resolv.c +++ b/ext/win32/resolv/resolv.c @@ -1,24 +1,56 @@ #include #include #include +#include #ifndef NTDDI_VERSION #define NTDDI_VERSION 0x06000000 #endif #include +#ifndef numberof +#define numberof(array) ((int)(sizeof(array) / sizeof((array)[0]))) +#endif + static VALUE w32error_make_error(DWORD e) { - VALUE code = ULONG2NUM(e); - return rb_class_new_instance(1, &code, rb_path2class("Win32::Resolv::Error")); + char buffer[512], *p; + DWORD source = 0; + VALUE args[2]; + if (!FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, &source, e, + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + buffer, sizeof(buffer), NULL)) { + snprintf(buffer, sizeof(buffer), "Unknown Error %u", (unsigned long)e); + } + p = buffer; + while ((p = strpbrk(p, "\r\n")) != NULL) { + memmove(p, p + 1, strlen(p)); + if (!p[1]) { + p[0] = '\0'; + break; + } + } + args[0] = ULONG2NUM(e); + args[1] = rb_str_new_cstr(buffer); + return rb_class_new_instance(2, args, rb_path2class("Win32::Resolv::Error")); } -NORETURN(static void w32error_raise(DWORD e)); - static void -w32error_raise(DWORD e) +w32error_check(DWORD e) +{ + if (e != NO_ERROR) { + rb_exc_raise(w32error_make_error(e)); + } +} + +static VALUE +wchar_to_utf8(const WCHAR *w, int n) { - rb_exc_raise(w32error_make_error(e)); + int clen = WideCharToMultiByte(CP_UTF8, 0, w, n, NULL, 0, NULL, NULL); + VALUE str = rb_enc_str_new(NULL, clen, rb_utf8_encoding()); + WideCharToMultiByte(CP_UTF8, 0, w, n, RSTRING_PTR(str), clen, NULL, NULL); + return str; } static VALUE @@ -30,9 +62,7 @@ get_dns_server_list(VALUE self) VALUE buf, nameservers = Qnil; ret = GetNetworkParams(NULL, &buflen); - if (ret != NO_ERROR && ret != ERROR_BUFFER_OVERFLOW) { - w32error_raise(ret); - } + if (ret != ERROR_BUFFER_OVERFLOW) w32error_check(ret); fixedinfo = ALLOCV(buf, buflen); ret = GetNetworkParams(fixedinfo, &buflen); if (ret == NO_ERROR) { @@ -46,18 +76,174 @@ get_dns_server_list(VALUE self) } while ((ipaddr = ipaddr->Next) != NULL); } ALLOCV_END(buf); - if (ret != NO_ERROR) w32error_raise(ret); + w32error_check(ret); return nameservers; } + +static const WCHAR TCPIP_Params[] = L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters"; + +static void +hkey_finalize(void *p) +{ + RegCloseKey((HKEY)p); +} + +static const rb_data_type_t hkey_type = { + "RegKey", + {0, hkey_finalize}, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY, +}; + +static VALUE +hkey_close(VALUE self) +{ + RegCloseKey((HKEY)DATA_PTR(self)); + DATA_PTR(self) = 0; + return self; +} + +static VALUE reg_key_class; + +static VALUE +reg_open_key(VALUE klass, HKEY hkey, const WCHAR *wname) +{ + VALUE k = TypedData_Wrap_Struct(klass, &hkey_type, NULL); + DWORD e = RegOpenKeyExW(hkey, wname, 0, KEY_READ, (HKEY *)&DATA_PTR(k)); + if (e == ERROR_FILE_NOT_FOUND) return Qnil; + w32error_check(e); + return rb_ensure(rb_yield, k, hkey_close, k); +} + +static VALUE +tcpip_params_open(VALUE klass) +{ + return reg_open_key(reg_key_class, HKEY_LOCAL_MACHINE, TCPIP_Params); +} + +static int +to_wname(VALUE *name, WCHAR *wname, int wlen) +{ + const char *n = StringValueCStr(*name); + int nlen = RSTRING_LEN(*name); + int len = MultiByteToWideChar(CP_UTF8, 0, n, nlen, wname, wlen - 1); + if (len == 0) w32error_check(GetLastError()); + if (len >= wlen) rb_raise(rb_eArgError, "too long name"); + wname[len] = L'\0'; + return len; +} + +static VALUE +reg_open(VALUE self, VALUE name) +{ + HKEY hkey = DATA_PTR(self); + WCHAR wname[256]; + to_wname(&name, wname, numberof(wname)); + return reg_open_key(CLASS_OF(self), hkey, wname); +} + + +static VALUE +reg_each_key(VALUE self) +{ + WCHAR wname[256]; + HKEY hkey = DATA_PTR(self); + rb_encoding *utf8 = rb_utf8_encoding(); + VALUE k = TypedData_Wrap_Struct(CLASS_OF(self), &hkey_type, NULL); + DWORD i, e, n; + for (i = 0; n = numberof(wname), (e = RegEnumKeyExW(hkey, i, wname, &n, NULL, NULL, NULL, NULL)) == ERROR_SUCCESS; i++) { + e = RegOpenKeyExW(hkey, wname, 0, KEY_READ, (HKEY *)&DATA_PTR(k)); + w32error_check(e); + rb_ensure(rb_yield, k, hkey_close, k); + } + if (e != ERROR_NO_MORE_ITEMS) w32error_check(e); + return self; +} + +static inline DWORD +swap_dw(DWORD x) +{ +#if defined(_MSC_VER) + return _byteswap_ulong(x); +#else + return __builtin_bswap32(x); +#endif +} + +static VALUE +reg_value(VALUE self, VALUE name) +{ + HKEY hkey = DATA_PTR(self); + DWORD type = 0, size = 0, e; + VALUE result, value_buffer; + void *buffer; + WCHAR wname[256]; + to_wname(&name, wname, numberof(wname)); + e = RegGetValueW(hkey, NULL, wname, RRF_RT_ANY, &type, NULL, &size); + if (e == ERROR_FILE_NOT_FOUND) return Qnil; + w32error_check(e); + switch (type) { + case REG_DWORD: case REG_DWORD_BIG_ENDIAN: + { + DWORD d; + if (size != sizeof(d)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", size); + w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_REG_DWORD, &type, &d, &size)); + if (type == REG_DWORD_BIG_ENDIAN) d = swap_dw(d); + return ULONG2NUM(d); + } + case REG_QWORD: + { + QWORD q; + if (size != sizeof(q)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", size); + w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_REG_QWORD, &type, &q, &size)); + return ULL2NUM(q); + } + case REG_SZ: case REG_MULTI_SZ: case REG_EXPAND_SZ: + if (size % sizeof(WCHAR)) rb_raise(rb_eRuntimeError, "invalid size returned: %lu", size); + buffer = ALLOCV_N(char, value_buffer, size); + break; + default: + result = rb_str_new(0, size); + buffer = RSTRING_PTR(result); + } + w32error_check(RegGetValueW(hkey, NULL, wname, RRF_RT_ANY, &type, buffer, &size)); + switch (type) { + case REG_MULTI_SZ: { + const WCHAR *w = (WCHAR *)buffer; + rb_encoding *utf8 = rb_utf8_encoding(); + result = rb_ary_new(); + size /= sizeof(WCHAR); + size -= 1; + for (size_t i = 0; i < size; ++i) { + int n = lstrlenW(w+i); + rb_ary_push(result, wchar_to_utf8(w+i, n)); + i += n; + } + return result; + } + case REG_SZ: case REG_EXPAND_SZ: + return wchar_to_utf8((WCHAR *)buffer, lstrlenW((WCHAR *)buffer)); + default: + return result; + } +} + void InitVM_resolv(void) { VALUE mWin32 = rb_define_module("Win32"); VALUE resolv = rb_define_module_under(mWin32, "Resolv"); VALUE singl = rb_singleton_class(resolv); + VALUE regkey = rb_define_class_under(resolv, "registry key", rb_cObject); + + reg_key_class = regkey; + rb_undef_alloc_func(regkey); rb_define_private_method(singl, "get_dns_server_list", get_dns_server_list, 0); + rb_define_private_method(singl, "tcpip_params", tcpip_params_open, 0); + rb_define_method(regkey, "open", reg_open, 1); + rb_define_method(regkey, "each_key", reg_each_key, 0); + rb_define_method(regkey, "value", reg_value, 1); } void diff --git a/test/resolv/test_win32_config.rb b/test/resolv/test_win32_config.rb index f44d195..6167af6 100644 --- a/test/resolv/test_win32_config.rb +++ b/test/resolv/test_win32_config.rb @@ -3,102 +3,24 @@ require 'test/unit' require 'resolv' -class TestWin32Config < Test::Unit::TestCase - def setup - omit 'Win32::Resolv tests only run on Windows' unless RUBY_PLATFORM =~ /mswin|mingw|cygwin/ - end - - def test_get_item_property_string - # Test reading a string registry value - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'DataBasePath') - - # Should return a string (empty or with a path) - assert_instance_of String, result - end - - def test_get_item_property_with_expand - # Test reading an expandable string registry value - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'DataBasePath', - expand: true) - - # Should return a string with environment variables expanded - assert_instance_of String, result - end +if defined?(Win32::Resolve) + class TestWin32Config < Test::Unit::TestCase + def test_get_item_property_string + # Test reading a string registry value + result = Win32::Resolv.send(:get_hosts_dir) - def test_get_item_property_dword - # Test reading a DWORD registry value - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'UseDomainNameDevolution', - dword: true) + # Should return a string (empty or with a path) + assert_instance_of String, result + end - # Should return an integer (0 or 1 typically) - assert_kind_of Integer, result - end - - def test_get_item_property_nonexistent_key # Test reading a non-existent registry key - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'NonExistentKeyThatShouldNotExist') - - # Should return empty string for non-existent string values - assert_equal '', result - end - - def test_get_item_property_nonexistent_key_dword - # Test reading a non-existent registry key as DWORD - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'NonExistentKeyThatShouldNotExist', - dword: true) - - # Should return 0 for non-existent DWORD values - assert_equal 0, result - end - - def test_get_item_property_search_list - # Test reading SearchList which may exist in the registry - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'SearchList') - - # Should return a string (may be empty if not configured) - assert_instance_of String, result - end - - def test_get_item_property_nv_domain - # Test reading NV Domain which may exist in the registry - result = Win32::Resolv.send(:get_item_property, - Win32::Resolv::TCPIP_NT, - 'NV Domain') - - # Should return a string (may be empty if not configured) - assert_instance_of String, result - end - - def test_get_item_property_with_invalid_path - # Test with an invalid registry path - result = Win32::Resolv.send(:get_item_property, - 'SYSTEM\NonExistent\Path', - 'SomeKey') - - # Should return empty string for invalid path - assert_equal '', result - end - - def test_get_item_property_with_invalid_path_dword - # Test with an invalid registry path as DWORD - result = Win32::Resolv.send(:get_item_property, - 'SYSTEM\NonExistent\Path', - 'SomeKey', - dword: true) - - # Should return 0 for invalid path - assert_equal 0, result + def test_nonexistent_key + assert_nil(Win32::Resolv.send(:tcpip_params) {|reg| reg.open('NonExistentKeyThatShouldNotExist')}) + end + + # Test reading a non-existent registry value + def test_nonexistent_value + assert_nil(Win32::Resolv.send(:tcpip_params) {|reg| reg.value('NonExistentKeyThatShouldNotExist')}) + end end end