diff --git a/lib/puppet/provider/package/windows/package.rb b/lib/puppet/provider/package/windows/package.rb index a1526a886..c4299e127 100644 --- a/lib/puppet/provider/package/windows/package.rb +++ b/lib/puppet/provider/package/windows/package.rb @@ -1,92 +1,92 @@ require 'puppet/provider/package' require 'puppet/util/windows' class Puppet::Provider::Package::Windows class Package extend Enumerable extend Puppet::Util::Errors include Puppet::Util::Windows::Registry extend Puppet::Util::Windows::Registry attr_reader :name, :version # Enumerate each package. The appropriate package subclass # will be yielded. def self.each(&block) with_key do |key, values| name = key.name.match(/^.+\\([^\\]+)$/).captures[0] [MsiPackage, ExePackage].find do |klass| if pkg = klass.from_registry(name, values) yield pkg end end end end # Yield each registry key and its values associated with an # installed package. This searches both per-machine and current # user contexts, as well as packages associated with 64 and # 32-bit installers. def self.with_key(&block) %w[HKEY_LOCAL_MACHINE HKEY_CURRENT_USER].each do |hive| [KEY64, KEY32].each do |mode| mode |= KEY_READ begin open(hive, 'Software\Microsoft\Windows\CurrentVersion\Uninstall', mode) do |uninstall| - uninstall.each_key do |name, wtime| + each_key(uninstall) do |name, wtime| open(hive, "#{uninstall.keyname}\\#{name}", mode) do |key| yield key, values(key) end end end rescue Puppet::Util::Windows::Error => e raise e unless e.code == Puppet::Util::Windows::Error::ERROR_FILE_NOT_FOUND end end end end # Get the class that knows how to install this resource def self.installer_class(resource) fail("The source parameter is required when using the Windows provider.") unless resource[:source] case resource[:source] when /\.msi"?\Z/i # REMIND: can we install from URL? # REMIND: what about msp, etc MsiPackage when /\.exe"?\Z/i fail("The source does not exist: '#{resource[:source]}'") unless Puppet::FileSystem.exist?(resource[:source]) ExePackage else fail("Don't know how to install '#{resource[:source]}'") end end def self.munge(value) quote(replace_forward_slashes(value)) end def self.replace_forward_slashes(value) if value.include?('/') value.gsub!('/', "\\") Puppet.debug('Package source parameter contained /s - replaced with \\s') end value end def self.quote(value) value.include?(' ') ? %Q["#{value.gsub(/"/, '\"')}"] : value end def initialize(name, version) @name = name @version = version end end end require 'puppet/provider/package/windows/msi_package' require 'puppet/provider/package/windows/exe_package' diff --git a/lib/puppet/util/windows/api_types.rb b/lib/puppet/util/windows/api_types.rb index 52645d879..bb28da201 100644 --- a/lib/puppet/util/windows/api_types.rb +++ b/lib/puppet/util/windows/api_types.rb @@ -1,255 +1,269 @@ require 'ffi' require 'puppet/util/windows/string' module Puppet::Util::Windows::APITypes module ::FFI WIN32_FALSE = 0 # standard Win32 error codes ERROR_SUCCESS = 0 end module ::FFI::Library # Wrapper method for attach_function + private def attach_function_private(*args) attach_function(*args) private args[0] end end class ::FFI::Pointer NULL_HANDLE = 0 NULL_TERMINATOR_WCHAR = 0 def self.from_string_to_wide_string(str, &block) str = Puppet::Util::Windows::String.wide_string(str) FFI::MemoryPointer.new(:byte, str.bytesize) do |ptr| # uchar here is synonymous with byte ptr.put_array_of_uchar(0, str.bytes.to_a) yield ptr end # ptr has already had free called, so nothing to return nil end def read_win32_bool # BOOL is always a 32-bit integer in Win32 # some Win32 APIs return 1 for true, while others are non-0 read_int32 != FFI::WIN32_FALSE end alias_method :read_dword, :read_uint32 alias_method :read_win32_ulong, :read_uint32 + alias_method :read_qword, :read_uint64 alias_method :read_hresult, :read_int32 def read_handle type_size == 4 ? read_uint32 : read_uint64 end alias_method :read_wchar, :read_uint16 alias_method :read_word, :read_uint16 - def read_wide_string(char_length) + def read_wide_string(char_length, dst_encoding = Encoding.default_external) # char_length is number of wide chars (typically excluding NULLs), *not* bytes str = get_bytes(0, char_length * 2).force_encoding('UTF-16LE') - str.encode(Encoding.default_external) + str.encode(dst_encoding) end def read_arbitrary_wide_string_up_to(max_char_length = 512) # max_char_length is number of wide chars (typically excluding NULLs), *not* bytes # use a pointer to read one UTF-16LE char (2 bytes) at a time wchar_ptr = FFI::Pointer.new(:wchar, address) # now iterate 2 bytes at a time until an offset lower than max_char_length is found 0.upto(max_char_length - 1) do |i| if wchar_ptr[i].read_wchar == NULL_TERMINATOR_WCHAR return read_wide_string(i) end end read_wide_string(max_char_length) end def read_win32_local_pointer(&block) ptr = nil begin ptr = read_pointer yield ptr ensure if ptr && ! ptr.null? if FFI::WIN32::LocalFree(ptr.address) != FFI::Pointer::NULL_HANDLE Puppet.debug "LocalFree memory leak" end end end # ptr has already had LocalFree called, so nothing to return nil end def read_com_memory_pointer(&block) ptr = nil begin ptr = read_pointer yield ptr ensure FFI::WIN32::CoTaskMemFree(ptr) if ptr && ! ptr.null? end # ptr has already had CoTaskMemFree called, so nothing to return nil end alias_method :write_dword, :write_uint32 alias_method :write_word, :write_uint16 end # FFI Types # https://github.com/ffi/ffi/wiki/Types # Windows - Common Data Types # http://msdn.microsoft.com/en-us/library/cc230309.aspx # Windows Data Types # http://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx FFI.typedef :uint16, :word FFI.typedef :uint32, :dword # uintptr_t is defined in an FFI conf as platform specific, either # ulong_long on x64 or just ulong on x86 FFI.typedef :uintptr_t, :handle FFI.typedef :uintptr_t, :hwnd # buffer_inout is similar to pointer (platform specific), but optimized for buffers FFI.typedef :buffer_inout, :lpwstr # buffer_in is similar to pointer (platform specific), but optimized for CONST read only buffers FFI.typedef :buffer_in, :lpcwstr FFI.typedef :buffer_in, :lpcolestr # string is also similar to pointer, but should be used for const char * # NOTE that this is not wide, useful only for A suffixed functions FFI.typedef :string, :lpcstr # pointer in FFI is platform specific # NOTE: for API calls with reserved lpvoid parameters, pass a FFI::Pointer::NULL FFI.typedef :pointer, :lpcvoid FFI.typedef :pointer, :lpvoid FFI.typedef :pointer, :lpword + FFI.typedef :pointer, :lpbyte FFI.typedef :pointer, :lpdword FFI.typedef :pointer, :pdword FFI.typedef :pointer, :phandle FFI.typedef :pointer, :ulong_ptr FFI.typedef :pointer, :pbool FFI.typedef :pointer, :lpunknown # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width # which is what FFI uses by default # instead create new aliases for these very special cases # NOTE: not a good idea to redefine FFI :ulong since other typedefs may rely on it FFI.typedef :uint32, :win32_ulong FFI.typedef :int32, :win32_long # FFI bool can be only 1 byte at times, # Win32 BOOL is a signed int, and is always 4 bytes, even on x64 # http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx FFI.typedef :int32, :win32_bool # Same as a LONG, a 32-bit signed integer FFI.typedef :int32, :hresult # NOTE: FFI already defines (u)short as a 16-bit (un)signed like this: # FFI.typedef :uint16, :ushort # FFI.typedef :int16, :short # 8 bits per byte FFI.typedef :uchar, :byte FFI.typedef :uint16, :wchar module ::FFI::WIN32 extend ::FFI::Library # http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931(v=vs.85).aspx # typedef struct _GUID { # DWORD Data1; # WORD Data2; # WORD Data3; # BYTE Data4[8]; # } GUID; class GUID < FFI::Struct layout :Data1, :dword, :Data2, :word, :Data3, :word, :Data4, [:byte, 8] def self.[](s) raise 'Bad GUID format.' unless s =~ /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i new.tap do |guid| guid[:Data1] = s[0, 8].to_i(16) guid[:Data2] = s[9, 4].to_i(16) guid[:Data3] = s[14, 4].to_i(16) guid[:Data4][0] = s[19, 2].to_i(16) guid[:Data4][1] = s[21, 2].to_i(16) s[24, 12].split('').each_slice(2).with_index do |a, i| guid[:Data4][i + 2] = a.join('').to_i(16) end end end def ==(other) Windows.memcmp(other, self, size) == 0 end end # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724950(v=vs.85).aspx # typedef struct _SYSTEMTIME { # WORD wYear; # WORD wMonth; # WORD wDayOfWeek; # WORD wDay; # WORD wHour; # WORD wMinute; # WORD wSecond; # WORD wMilliseconds; # } SYSTEMTIME, *PSYSTEMTIME; class SYSTEMTIME < FFI::Struct layout :wYear, :word, :wMonth, :word, :wDayOfWeek, :word, :wDay, :word, :wHour, :word, :wMinute, :word, :wSecond, :word, :wMilliseconds, :word def to_local_time Time.local(self[:wYear], self[:wMonth], self[:wDay], self[:wHour], self[:wMinute], self[:wSecond], self[:wMilliseconds] * 1000) end end + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx + # Contains a 64-bit value representing the number of 100-nanosecond + # intervals since January 1, 1601 (UTC). + # typedef struct _FILETIME { + # DWORD dwLowDateTime; + # DWORD dwHighDateTime; + # } FILETIME, *PFILETIME; + class FILETIME < FFI::Struct + layout :dwLowDateTime, :dword, + :dwHighDateTime, :dword + end + ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366730(v=vs.85).aspx # HLOCAL WINAPI LocalFree( # _In_ HLOCAL hMem # ); ffi_lib :kernel32 attach_function :LocalFree, [:handle], :handle # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx # BOOL WINAPI CloseHandle( # _In_ HANDLE hObject # ); ffi_lib :kernel32 attach_function_private :CloseHandle, [:handle], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680722(v=vs.85).aspx # void CoTaskMemFree( # _In_opt_ LPVOID pv # ); ffi_lib :ole32 attach_function :CoTaskMemFree, [:lpvoid], :void end end diff --git a/lib/puppet/util/windows/registry.rb b/lib/puppet/util/windows/registry.rb index 84cdde793..68adbb662 100644 --- a/lib/puppet/util/windows/registry.rb +++ b/lib/puppet/util/windows/registry.rb @@ -1,80 +1,368 @@ require 'puppet/util/windows' module Puppet::Util::Windows module Registry require 'ffi' extend FFI::Library # http://msdn.microsoft.com/en-us/library/windows/desktop/aa384129(v=vs.85).aspx KEY64 = 0x100 KEY32 = 0x200 KEY_READ = 0x20019 KEY_WRITE = 0x20006 KEY_ALL_ACCESS = 0x2003f + ERROR_NO_MORE_ITEMS = 259 + def root(name) Win32::Registry.const_get(name) rescue NameError raise Puppet::Error, "Invalid registry key '#{name}'", $!.backtrace end def open(name, path, mode = KEY_READ | KEY64, &block) hkey = root(name) begin hkey.open(path, mode) do |subkey| return yield subkey end rescue Win32::Registry::Error => error raise Puppet::Util::Windows::Error.new("Failed to open registry key '#{hkey.keyname}\\#{path}'", error.code, error) end end - def values(subkey) - values = {} - subkey.each_value do |name, type, data| + def keys(key) + keys = {} + each_key(key) { |subkey, filetime| keys[subkey] = filetime } + keys + end + + # subkey is String which contains name of subkey. + # wtime is last write time as FILETIME (64-bit integer). (see Registry.wtime2time) + def each_key(key, &block) + index = 0 + subkey = nil + + subkey_max_len, value_max_len = reg_query_info_key_max_lengths(key) + + begin + subkey, filetime = reg_enum_key(key, index, subkey_max_len) + yield subkey, filetime if !subkey.nil? + index += 1 + end while !subkey.nil? + + index + end + + def delete_key(key, subkey_name, mode = KEY64) + reg_delete_key_ex(key, subkey_name, mode) + end + + def values(key) + vals = {} + each_value(key) { |subkey, type, data| vals[subkey] = data } + vals + end + + def each_value(key, &block) + index = 0 + subkey = nil + + subkey_max_len, value_max_len = reg_query_info_key_max_lengths(key) + + begin + subkey, type, data = reg_enum_value(key, index, value_max_len) + yield subkey, type, data if !subkey.nil? + index += 1 + end while !subkey.nil? + + index + end + + def delete_value(key, subkey_name) + reg_delete_value(key, subkey_name) + end + + private + + def reg_enum_key(key, index, max_key_length = Win32::Registry::Constants::MAX_KEY_LENGTH) + subkey, filetime = nil, nil + + FFI::MemoryPointer.new(:dword) do |subkey_length_ptr| + FFI::MemoryPointer.new(FFI::WIN32::FILETIME.size) do |filetime_ptr| + FFI::MemoryPointer.new(:wchar, max_key_length) do |subkey_ptr| + subkey_length_ptr.write_dword(max_key_length) + + # RegEnumKeyEx cannot be called twice to properly size the buffer + result = RegEnumKeyExW(key.hkey, index, + subkey_ptr, subkey_length_ptr, + FFI::Pointer::NULL, FFI::Pointer::NULL, + FFI::Pointer::NULL, filetime_ptr) + + break if result == ERROR_NO_MORE_ITEMS + + if result != FFI::ERROR_SUCCESS + msg = "Failed to enumerate #{key.keyname} registry keys at index #{index}" + raise Puppet::Util::Windows::Error.new(msg) + end + + filetime = FFI::WIN32::FILETIME.new(filetime_ptr) + subkey_length = subkey_length_ptr.read_dword + subkey = subkey_ptr.read_wide_string(subkey_length, Encoding::UTF_8) + end + end + end + + [subkey, filetime] + end + + def reg_enum_value(key, index, max_value_length = Win32::Registry::Constants::MAX_VALUE_LENGTH) + subkey, type, data = nil, nil, nil + + FFI::MemoryPointer.new(:dword) do |subkey_length_ptr| + FFI::MemoryPointer.new(:wchar, max_value_length) do |subkey_ptr| + # RegEnumValueW cannot be called twice to properly size the buffer + subkey_length_ptr.write_dword(max_value_length) + + result = RegEnumValueW(key.hkey, index, + subkey_ptr, subkey_length_ptr, + FFI::Pointer::NULL, FFI::Pointer::NULL, + FFI::Pointer::NULL, FFI::Pointer::NULL + ) + + break if result == ERROR_NO_MORE_ITEMS + + if result != FFI::ERROR_SUCCESS + msg = "Failed to enumerate #{key.keyname} registry values at index #{index}" + raise Puppet::Util::Windows::Error.new(msg) + end + + subkey_length = subkey_length_ptr.read_dword + subkey = subkey_ptr.read_wide_string(subkey_length, Encoding::UTF_8) + + type, data = read(key, subkey_ptr) + end + end + + [subkey, type, data] + end + + def reg_query_info_key_max_lengths(key) + result = nil + + FFI::MemoryPointer.new(:dword) do |max_subkey_name_length_ptr| + FFI::MemoryPointer.new(:dword) do |max_value_name_length_ptr| + + status = RegQueryInfoKeyW(key.hkey, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL, + max_subkey_name_length_ptr, FFI::MemoryPointer::NULL, + FFI::MemoryPointer::NULL, max_value_name_length_ptr, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL, + FFI::MemoryPointer::NULL + ) + + if status != FFI::ERROR_SUCCESS + msg = "Failed to query registry #{key.keyname} for sizes" + raise Puppet::Util::Windows::Error.new(msg) + end + + result = [ + # Unicode characters *not* including trailing NULL + max_subkey_name_length_ptr.read_dword + 1, + max_value_name_length_ptr.read_dword + 1, + ] + end + end + + result + end + + # Read a registry value named name and return array of + # [ type, data ]. + # When name is nil, the `default' value is read. + # type is value type. (see Win32::Registry::Constants module) + # data is value data, its class is: + # :REG_SZ, REG_EXPAND_SZ + # String + # :REG_MULTI_SZ + # Array of String + # :REG_DWORD, REG_DWORD_BIG_ENDIAN, REG_QWORD + # Integer + # :REG_BINARY + # String (contains binary data) + # + # When rtype is specified, the value type must be included by + # rtype array, or TypeError is raised. + def read(key, name_ptr, *rtype) + result = nil + + query_value_ex(key, name_ptr) do |type, data_ptr, byte_length| + unless rtype.empty? or rtype.include?(type) + raise TypeError, "Type mismatch (expect #{rtype.inspect} but #{type} present)" + end + + string_length = 0 + # buffer is raw bytes, *not* chars - less a NULL terminator + string_length = (byte_length / FFI.type_size(:wchar)) - 1 if byte_length > 0 + case type - when Win32::Registry::REG_MULTI_SZ - data.each { |str| force_encoding(str) } when Win32::Registry::REG_SZ, Win32::Registry::REG_EXPAND_SZ - force_encoding(data) + result = [ type, data_ptr.read_wide_string(string_length, Encoding::UTF_8) ] + when Win32::Registry::REG_MULTI_SZ + result = [ type, data_ptr.read_wide_string(string_length, Encoding::UTF_8).split(/\0/) ] + when Win32::Registry::REG_BINARY + result = [ type, data.read_bytes(0, byte_length) ] + when Win32::Registry::REG_DWORD + result = [ type, data_ptr.read_dword ] + when Win32::Registry::REG_DWORD_BIG_ENDIAN + result = [ type, data_ptr.order(:big).read_dword ] + when Win32::Registry::REG_QWORD + result = [ type, data_ptr.read_qword ] + else + raise TypeError, "Type #{type} is not supported." end - values[name] = data end - values + + result end - if defined?(Encoding) - def force_encoding(str) - if @encoding.nil? - # See https://bugs.ruby-lang.org/issues/8943 - # Ruby uses ANSI versions of Win32 APIs to read values from the - # registry. The encoding of these strings depends on the active - # code page. However, ruby incorrectly sets the string - # encoding to US-ASCII. So we must force the encoding to the - # correct value. - begin - cp = GetACP() - @encoding = Encoding.const_get("CP#{cp}") - rescue - @encoding = Encoding.default_external + def query_value_ex(key, name_ptr, &block) + FFI::MemoryPointer.new(:dword) do |type_ptr| + FFI::MemoryPointer.new(:dword) do |length_ptr| + result = RegQueryValueExW(key.hkey, name_ptr, + FFI::Pointer::NULL, type_ptr, + FFI::Pointer::NULL, length_ptr) + + FFI::MemoryPointer.new(:byte, length_ptr.read_dword) do |buffer_ptr| + result = RegQueryValueExW(key.hkey, name_ptr, + FFI::Pointer::NULL, type_ptr, + buffer_ptr, length_ptr) + + if result != FFI::ERROR_SUCCESS + msg = "Failed to read registry value #{name_ptr.read_wide_string} at #{key.keyname}" + raise Puppet::Util::Windows::Error.new(msg) + end + + # allows caller to use FFI MemoryPointer helpers to read / shape + yield [type_ptr.read_dword, buffer_ptr, length_ptr.read_dword] end end - - str.force_encoding(@encoding) end - else - def force_encoding(str, enc) + end + + def reg_delete_value(key, name) + result = 0 + + FFI::Pointer.from_string_to_wide_string(name) do |name_ptr| + result = RegDeleteValueW(key.hkey, name_ptr) + + if result != FFI::ERROR_SUCCESS + msg = "Failed to delete registry value #{name} at #{key.keyname}" + raise Puppet::Util::Windows::Error.new(msg) + end end + + result end - private :force_encoding + def reg_delete_key_ex(key, name, regsam = KEY64) + result = 0 + + FFI::Pointer.from_string_to_wide_string(name) do |name_ptr| + result = RegDeleteKeyExW(key.hkey, name_ptr, regsam, 0) + + if result != FFI::ERROR_SUCCESS + msg = "Failed to delete registry value #{name} at #{key.keyname}" + raise Puppet::Util::Windows::Error.new(msg) + end + end + + result + end ffi_convention :stdcall - # http://msdn.microsoft.com/en-us/library/windows/desktop/dd318070(v=vs.85).aspx - # UINT GetACP(void); - ffi_lib :kernel32 - attach_function_private :GetACP, [], :uint32 + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724862(v=vs.85).aspx + # LONG WINAPI RegEnumKeyEx( + # _In_ HKEY hKey, + # _In_ DWORD dwIndex, + # _Out_ LPTSTR lpName, + # _Inout_ LPDWORD lpcName, + # _Reserved_ LPDWORD lpReserved, + # _Inout_ LPTSTR lpClass, + # _Inout_opt_ LPDWORD lpcClass, + # _Out_opt_ PFILETIME lpftLastWriteTime + # ); + ffi_lib :advapi32 + attach_function_private :RegEnumKeyExW, + [:handle, :dword, :lpwstr, :lpdword, :lpdword, :lpwstr, :lpdword, :pointer], :win32_long + + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724865(v=vs.85).aspx + # LONG WINAPI RegEnumValue( + # _In_ HKEY hKey, + # _In_ DWORD dwIndex, + # _Out_ LPTSTR lpValueName, + # _Inout_ LPDWORD lpcchValueName, + # _Reserved_ LPDWORD lpReserved, + # _Out_opt_ LPDWORD lpType, + # _Out_opt_ LPBYTE lpData, + # _Inout_opt_ LPDWORD lpcbData + # ); + ffi_lib :advapi32 + attach_function_private :RegEnumValueW, + [:handle, :dword, :lpwstr, :lpdword, :lpdword, :lpdword, :lpbyte, :lpdword], :win32_long + + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724911(v=vs.85).aspx + # LONG WINAPI RegQueryValueExW( + # _In_ HKEY hKey, + # _In_opt_ LPCTSTR lpValueName, + # _Reserved_ LPDWORD lpReserved, + # _Out_opt_ LPDWORD lpType, + # _Out_opt_ LPBYTE lpData, + # _Inout_opt_ LPDWORD lpcbData + # ); + ffi_lib :advapi32 + attach_function_private :RegQueryValueExW, + [:handle, :lpcwstr, :lpdword, :lpdword, :lpbyte, :lpdword], :win32_long + + # LONG WINAPI RegDeleteValue( + # _In_ HKEY hKey, + # _In_opt_ LPCTSTR lpValueName + # ); + ffi_lib :advapi32 + attach_function_private :RegDeleteValueW, + [:handle, :lpcwstr], :win32_long + + # LONG WINAPI RegDeleteKeyEx( + # _In_ HKEY hKey, + # _In_ LPCTSTR lpSubKey, + # _In_ REGSAM samDesired, + # _Reserved_ DWORD Reserved + # ); + ffi_lib :advapi32 + attach_function_private :RegDeleteKeyExW, + [:handle, :lpcwstr, :win32_ulong, :dword], :win32_long + + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724902(v=vs.85).aspx + # LONG WINAPI RegQueryInfoKey( + # _In_ HKEY hKey, + # _Out_opt_ LPTSTR lpClass, + # _Inout_opt_ LPDWORD lpcClass, + # _Reserved_ LPDWORD lpReserved, + # _Out_opt_ LPDWORD lpcSubKeys, + # _Out_opt_ LPDWORD lpcMaxSubKeyLen, + # _Out_opt_ LPDWORD lpcMaxClassLen, + # _Out_opt_ LPDWORD lpcValues, + # _Out_opt_ LPDWORD lpcMaxValueNameLen, + # _Out_opt_ LPDWORD lpcMaxValueLen, + # _Out_opt_ LPDWORD lpcbSecurityDescriptor, + # _Out_opt_ PFILETIME lpftLastWriteTime + # ); + ffi_lib :advapi32 + attach_function_private :RegQueryInfoKeyW, + [:handle, :lpwstr, :lpdword, :lpdword, :lpdword, :lpdword, :lpdword, + :lpdword, :lpdword, :lpdword, :lpdword, :pointer], :win32_long end end diff --git a/spec/unit/util/windows/registry_spec.rb b/spec/unit/util/windows/registry_spec.rb index 76fd5da68..25fe6c736 100755 --- a/spec/unit/util/windows/registry_spec.rb +++ b/spec/unit/util/windows/registry_spec.rb @@ -1,141 +1,166 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/windows' describe Puppet::Util::Windows::Registry, :if => Puppet::Util::Platform.windows? do subject do class TestRegistry include Puppet::Util::Windows::Registry end TestRegistry.new end let(:name) { 'HKEY_LOCAL_MACHINE' } let(:path) { 'Software\Microsoft' } context "#root" do it "should lookup the root hkey" do expect(subject.root(name)).to be_instance_of(Win32::Registry::PredefinedKey) end it "should raise for unknown root keys" do expect { subject.root('HKEY_BOGUS') }.to raise_error(Puppet::Error, /Invalid registry key/) end end context "#open" do let(:hkey) { stub 'hklm' } let(:subkey) { stub 'subkey' } before :each do subject.stubs(:root).returns(hkey) end it "should yield the opened the subkey" do hkey.expects(:open).with do |p, _| expect(p).to eq(path) end.yields(subkey) yielded = nil subject.open(name, path) {|reg| yielded = reg} expect(yielded).to eq(subkey) end if Puppet::Util::Platform.windows? [described_class::KEY64, described_class::KEY32].each do |access| it "should open the key for read access 0x#{access.to_s(16)}" do mode = described_class::KEY_READ | access hkey.expects(:open).with(path, mode) subject.open(name, path, mode) {|reg| } end end end it "should default to KEY64" do hkey.expects(:open).with(path, described_class::KEY_READ | described_class::KEY64) subject.open(hkey, path) {|hkey| } end it "should raise for a path that doesn't exist" do hkey.expects(:keyname).returns('HKEY_LOCAL_MACHINE') hkey.expects(:open).raises(Win32::Registry::Error.new(2)) # file not found expect do subject.open(hkey, 'doesnotexist') {|hkey| } end.to raise_error(Puppet::Error, /Failed to open registry key 'HKEY_LOCAL_MACHINE\\doesnotexist'/) end end context "#values" do let(:key) { stub('uninstall') } + def expects_registry_value(array) + key.expects(:each_value).never + subject.expects(:each_value).with(key).multiple_yields(array) + + subject.values(key).first[1] + end + it "should return each value's name and data" do - key.expects(:each_value).multiple_yields( + key.expects(:each_value).never + subject.expects(:each_value).with(key).multiple_yields( ['string', 1, 'foo'], ['dword', 4, 0] ) expect(subject.values(key)).to eq({ 'string' => 'foo', 'dword' => 0 }) end it "should return an empty hash if there are no values" do - key.expects(:each_value) + key.expects(:each_value).never + subject.expects(:each_value).with(key) expect(subject.values(key)).to eq({}) end - context "when reading non-ASCII values" do - # registered trademark symbol - let(:data) do - str = [0xAE].pack("C") - str.force_encoding('US-ASCII') - str - end - - def expects_registry_value(array) - key.expects(:each_value).multiple_yields(array) - - subject.values(key).first[1] - end - - # The win32console gem applies this regex to strip out ANSI escape - # sequences. If the registered trademark had encoding US-ASCII, - # the regex would fail with 'invalid byte sequence in US-ASCII' - def strip_ansi_escapes(value) - value.sub(/([^\e]*)?\e([\[\(])([0-9\;\=]*)([a-zA-Z@])(.*)/, '\5') - end + it "passes REG_DWORD through" do + reg_value = ['dword', Win32::Registry::REG_DWORD, '1'] - it "encodes REG_SZ according to the current code page" do - reg_value = ['string', Win32::Registry::REG_SZ, data] + value = expects_registry_value(reg_value) - value = expects_registry_value(reg_value) + expect(Integer(value)).to eq(1) + end - strip_ansi_escapes(value) + context "when reading non-ASCII values" do + ENDASH_UTF_8 = [0xE2, 0x80, 0x93] + ENDASH_UTF_16 = [0x2013] + TM_UTF_8 = [0xE2, 0x84, 0xA2] + TM_UTF_16 = [0x2122] + + let (:hklm) { Win32::Registry::HKEY_LOCAL_MACHINE } + let (:puppet_key) { "SOFTWARE\\Puppet Labs"} + let (:subkey_name) { "PuppetRegistryTest" } + let (:guid) { SecureRandom.uuid } + + after(:each) do + # Ruby 2.1.5 has bugs with deleting registry keys due to using ANSI + # character APIs, but passing wide strings to them (facepalm) + # https://github.com/ruby/ruby/blob/v2_1_5/ext/win32/lib/win32/registry.rb#L323-L329 + # therefore, use our own built-in registry helper code + + hklm.open(puppet_key, Win32::Registry::KEY_ALL_ACCESS) do |reg| + subject.delete_key(reg, subkey_name) + end end - it "encodes REG_EXPAND_SZ based on the current code page" do - reg_value = ['expand', Win32::Registry::REG_EXPAND_SZ, "%SYSTEMROOT%\\#{data}"] + # proof that local encodings (such as IBM437 are no longer relevant) + it "will return a UTF-8 string from a REG_SZ registry value (written as UTF-16LE)", + :if => Puppet::Util::Platform.windows? && RUBY_VERSION >= '2.1' do - value = expects_registry_value(reg_value) + # create a UTF-16LE byte array representing "–™" + utf_16_bytes = ENDASH_UTF_16 + TM_UTF_16 + utf_16_str = utf_16_bytes.pack('s*').force_encoding(Encoding::UTF_16LE) - strip_ansi_escapes(value) - end + # and it's UTF-8 equivalent bytes + utf_8_bytes = ENDASH_UTF_8 + TM_UTF_8 + utf_8_str = utf_8_bytes.pack('c*').force_encoding(Encoding::UTF_8) - it "encodes REG_MULTI_SZ based on the current code page" do - reg_value = ['multi', Win32::Registry::REG_MULTI_SZ, ["one#{data}", "two#{data}"]] + # this problematic Ruby codepath triggers a conversion of UTF-16LE to + # a local codepage which can totally break when that codepage has no + # conversion from the given UTF-16LE characters to local codepage + # a prime example is that IBM437 has no conversion from a Unicode en-dash + Win32::Registry.expects(:export_string).never - value = expects_registry_value(reg_value) + # also, expect that we're using our variants of keys / values, not Rubys + Win32::Registry.expects(:each_key).never + Win32::Registry.expects(:each_value).never - value.each { |str| strip_ansi_escapes(str) } - end + hklm.create("#{puppet_key}\\#{subkey_name}", Win32::Registry::KEY_ALL_ACCESS) do |reg| + reg.write("#{guid}", Win32::Registry::REG_SZ, utf_16_str) - it "passes REG_DWORD through" do - reg_value = ['dword', Win32::Registry::REG_DWORD, '1'] + # trigger Puppet::Util::Windows::Registry FFI calls + keys = subject.keys(reg) + vals = subject.values(reg) - value = expects_registry_value(reg_value) + expect(keys).to be_empty + expect(vals).to have_key(guid) - expect(Integer(value)).to eq(1) + # The UTF-16LE string written should come back as the equivalent UTF-8 + written = vals[guid] + expect(written).to eq(utf_8_str) + expect(written.encoding).to eq(Encoding::UTF_8) + end end end end end