diff --git a/spec/std/registry_spec.cr b/spec/std/registry_spec.cr new file mode 100644 index 000000000000..0245c97f1414 --- /dev/null +++ b/spec/std/registry_spec.cr @@ -0,0 +1,250 @@ +{% skip_file unless flag?(:win32) %} +require "registry" +require "spec" + +private TEST_RAND = Random.rand(0x100000000).to_s(36) + +private def test_key(prefix = "TestKey", sam = Registry::SAM::ALL_ACCESS) + name = "Crystal\\#{prefix}_#{TEST_RAND}" + Registry::CURRENT_USER.open("Software", Registry::SAM::QUERY_VALUE) do |parent| + begin + parent.create_key?(name) + parent.open(name, sam) do |key| + yield key + end + ensure + parent.delete_key?(name) + end + end +end + +private def assert_set_get(name, value, type = nil) + it name do + test_key do |key| + if valtype = type + key.set(name, value, valtype) + else + key.set(name, value) + end + key.get(name).should eq value + key.get?(name).should eq value + end + end +end + +private def assert_set_exception(name, value, type = nil, error_message = "String contains null byte", file = __FILE__, line = __LINE__) + it "#{name} fails null byte check", file, line do + test_key do |key| + expect_raises(ArgumentError, error_message) do + if valtype = type + key.set(name, value, valtype) + else + key.set(name, value) + end + end + end + end +end + +describe Registry::Key do + describe "#each_key" do + it "lists keys" do + test_key do |key| + key.create_key "EachKeySub1" + key.create_key "EachKeySub2" + subkeys = [] of String + key.each_key do |subkey| + subkeys << subkey + end + subkeys.should eq ["EachKeySub1", "EachKeySub2"] + key.subkeys.should eq ["EachKeySub1", "EachKeySub2"] + end + end + + it "finds standard key" do + Registry::CLASSES_ROOT.open("TypeLib", Registry::SAM::ENUMERATE_SUB_KEYS | Registry::SAM::QUERY_VALUE) do |key| + foundStdOle = false + key.each_key do |name| + # Every PC has "stdole 2.0 OLE Automation" library installed. + if name == "{00020430-0000-0000-C000-000000000046}" + foundStdOle = true + end + end + foundStdOle.should be_true + end + end + end + + it "create, open, delete key" do + Registry::CURRENT_USER.open("Software", Registry::SAM::QUERY_VALUE) do |software| + test_key = "TestCreateOpenDeleteKey_#{Random.rand(0x100000000).to_s(36)}" + + software.create_key(test_key, Registry::SAM::CREATE_SUB_KEY) + software.create_key?(test_key, Registry::SAM::CREATE_SUB_KEY).should be_false + + software.open(test_key, Registry::SAM::ENUMERATE_SUB_KEYS) { |test| } + + software.delete_key?(test_key) + software.delete_key?(test_key) + + software.open?(test_key, Registry::SAM::ENUMERATE_SUB_KEYS).should be_nil + end + end + + describe "values" do + it "unset value" do + test_key do |key| + expect_raises(Registry::Error, %(Value "non-existing" does not exist)) do + key.get("non-existing") + end + end + end + + describe "SZ" do + assert_set_get("String1", "") + assert_set_exception("String2", "\0", error_message: "String `value` contains null byte") + assert_set_get("String3", "Hello World") + assert_set_exception("String4", "Hello World\0", error_message: "String `value` contains null byte") + assert_set_get("StringLong", "a" * 257) + end + + describe "EXPAND_SZ" do + assert_set_get("ExpString1", "", type: Registry::ValueType::EXPAND_SZ) + assert_set_exception("ExpString2", "\0", error_message: "String `value` contains null byte", type: Registry::ValueType::EXPAND_SZ) + assert_set_get("ExpString3", "Hello World") + assert_set_exception("ExpString4", "Hello\0World", error_message: "String `value` contains null byte", type: Registry::ValueType::EXPAND_SZ) + assert_set_get("ExpString6", "%NO_SUCH_VARIABLE%", type: Registry::ValueType::EXPAND_SZ) + assert_set_get("ExpStringLong", "a" * 257, type: Registry::ValueType::EXPAND_SZ) + + it "expands single env var" do + test_key do |key| + key.set("ExpString5", "%PATH%", Registry::ValueType::EXPAND_SZ) + key.get("ExpString5").should eq(ENV["PATH"]) + key.get?("ExpString5").should eq(ENV["PATH"]) + end + end + + it "expands env var in string" do + test_key do |key| + key.set("ExpString7", "%PATH%;.", Registry::ValueType::EXPAND_SZ) + key.get("ExpString7").should eq(ENV["PATH"] + ";.") + key.get?("ExpString7").should eq(ENV["PATH"] + ";.") + end + end + end + + describe "BINARY" do + assert_set_get("Binary1", Bytes.new(0)) + assert_set_get("Binary2", StaticArray[1_u8, 2_u8, 3_u8].to_slice) + assert_set_get("Binary3", StaticArray[3_u8, 2_u8, 1_u8, 0_u8, 1_u8, 2_u8, 3_u8].to_slice) + assert_set_get("BinaryLarge", Bytes.new(257, 1_u8)) + end + + describe "DWORD" do + assert_set_get("Dword1", 0) + assert_set_get("Dword2", 1) + assert_set_get("Dword3", 0xff) + assert_set_get("Dword4", 0xffff) + end + + describe "QWORD" do + assert_set_get("Qword1", 0_i64) + assert_set_get("Qword2", 1_i64) + + assert_set_get("Qword3", 0xff_i64) + assert_set_get("Qword4", 0xffff_i64) + assert_set_get("Qword5", 0xffffff_i64) + assert_set_get("Qword6", 0xffffffff_i64) + end + + describe "MULTI_SZ" do + assert_set_get("MultiString1", ["a", "b", "c"]) + assert_set_get("MultiString2", ["abc", "", "cba"]) + assert_set_get("MultiString3", [""]) + assert_set_get("MultiString4", ["abcdef"]) + assert_set_exception("MultiString5", ["\000"]) + assert_set_exception("MultiString6", ["a\000b"]) + assert_set_exception("MultiString7", ["ab", "\000", "cd"]) + assert_set_exception("MultiString8", ["\000", "cd"]) + assert_set_exception("MultiString9", ["ab", "\000"]) + assert_set_get("MultiStringLong", ["a" * 257]) + end + end + + describe "#get_mui" do + it "handles non-existing key" do + test_key do |key| + expect_raises(Registry::Error, "Value 'NonExistingMUI' does not exist") do + key.get_mui("NonExistingMUI") + end + key.get_mui?("NonExistingMUI").should be_nil + end + end + + it "handles non-loadable value" do + test_key do |key| + key.set("InvalidMUI", "foo") + expect_raises(WinError, "RegLoadMUIStringW: [WinError 13") do + key.get_mui("InvalidMUI") + end + expect_raises(WinError, "RegLoadMUIStringW: [WinError 13") do + key.get_mui?("InvalidMUI") + end + end + end + + it "loads timezone name" do + LibC.GetDynamicTimeZoneInformation(out dtz_info).should_not eq(0) + + key_name = String.from_utf16(dtz_info.timeZoneKeyName.to_unsafe)[0] + + Registry::LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\#{key_name}", Registry::SAM::READ) do |key| + key.get_mui("MUI_Std").should eq(String.from_utf16(dtz_info.standardName.to_unsafe)[0]) + key.get_mui?("MUI_Std").should eq(String.from_utf16(dtz_info.standardName.to_unsafe)[0]) + if dtz_info.dynamicDaylightTimeDisabled == 0 + key.get_mui("MUI_Dlt").should eq(String.from_utf16(dtz_info.daylightName.to_unsafe)[0]) + key.get_mui?("MUI_Dlt").should eq(String.from_utf16(dtz_info.daylightName.to_unsafe)[0]) + end + end + end + end + + it "#get_string" do + test_key do |key| + key.set("test_string", "foo bar") + key.get_string("test_string").should eq "foo bar" + + expect_raises(Registry::Error, %(Value "non-existant" does not exist)) do + key.get_string("non-existant") + end + end + end + + it "#get_string?" do + test_key do |key| + key.set("test_string", "foo bar") + key.get_string?("test_string").should eq "foo bar" + key.get_string?("non-existant").should be_nil + end + end + + describe "#info" do + it do + test_key("TestInfo") do |test| + test.info.sub_key_count.should eq 0 + test.create_key("subkey") + test.info.sub_key_count.should eq 1 + test.info.max_sub_key_length.should eq "subkey".size + + test.set("f", "quux") + test.set("foo", "bar baz") + info = test.info + info.value_count.should eq 2 + info.max_value_name_length.should eq "foo".size + info.max_value_length.should eq "bar baz".to_utf16.bytesize + 2 + ensure + test.delete_key("subkey") + end + end + end +end diff --git a/src/crystal/system/win32/env.cr b/src/crystal/system/win32/env.cr index 1bd71d020c7f..eeab3f021727 100644 --- a/src/crystal/system/win32/env.cr +++ b/src/crystal/system/win32/env.cr @@ -74,4 +74,27 @@ module Crystal::System::Env LibC.FreeEnvironmentStringsW(orig_pointer) end end + + # Expands environment variables in a string. + # Used by Registry. + def self.expand(string : String) : String + string.check_no_null_byte + + String.from_utf16(expand(string.to_utf16)) + end + + # Expands environment variables in a string. + # Used by Registry. + def self.expand(string : Slice(UInt16) | Pointer(UInt16)) : Slice(UInt16) + Crystal::System.retry_wstr_buffer do |buffer, small_buf| + length = LibC.ExpandEnvironmentStringsW(string, buffer, buffer.size) + if 0 < length <= buffer.size + return buffer[0, length].clone + elsif small_buf && length > 0 + next length + else + raise WinError.new("ExpandEnvironmentStringsW") + end + end + end end diff --git a/src/crystal/system/win32/time.cr b/src/crystal/system/win32/time.cr index 11f4e14b2271..77ada8e047d9 100644 --- a/src/crystal/system/win32/time.cr +++ b/src/crystal/system/win32/time.cr @@ -1,5 +1,6 @@ require "c/winbase" require "winerror" +require "registry" require "./zone_names" module Crystal::System::Time @@ -21,7 +22,7 @@ module Crystal::System::Time filetime_to_seconds_and_nanoseconds(filetime) end - def self.filetime_to_seconds_and_nanoseconds(filetime) : {Int64, Int32} + def self.filetime_to_seconds_and_nanoseconds(filetime : LibC::FILETIME) : {Int64, Int32} since_epoch = (filetime.dwHighDateTime.to_u64 << 32) | filetime.dwLowDateTime.to_u64 seconds = (since_epoch / FILETIME_TICKS_PER_SECOND).to_i64 + WINDOWS_EPOCH_IN_SECONDS @@ -163,7 +164,22 @@ module Crystal::System::Time # Searches the registry for an English name of a time zone named *stdname* or *dstname* # and returns the English name. private def self.translate_zone_name(stdname, dstname) - # TODO: Needs implementation once there is access to the registry. - nil + Registry::LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", :read) do |key| + key.each_key do |sub_name| + key.open(sub_name, :read) do |sub_key| + # TODO: Only use MUI string if available (desktop apps for Windows Vista, Windows Server 2008 and upwards) + if std = sub_key.get_mui?("MUI_Std") + dlt = sub_key.get_mui?("MUI_Dlt") + else + std = sub_key.get_string?("Std").try &.strip + dlt = sub_key.get_string?("Dlt").try &.strip + end + + if std == stdname || dlt == dstname + return sub_name + end + end + end + end end end diff --git a/src/crystal/system/windows.cr b/src/crystal/system/windows.cr index c39511a2a94a..b65b2370b5ab 100644 --- a/src/crystal/system/windows.cr +++ b/src/crystal/system/windows.cr @@ -1,5 +1,16 @@ # :nodoc: module Crystal::System + def self.retry_buffer + buffer_size = 256 + buffer_arr = Bytes.new(256) + + buffer_size = yield buffer_arr.to_slice, true + buffer = Bytes.new(buffer_size) + + yield buffer, false + raise "BUG: retry_buffer returned" + end + def self.retry_wstr_buffer buffer_size = 256 buffer_arr = uninitialized LibC::WCHAR[256] diff --git a/src/docs_main.cr b/src/docs_main.cr index c44c68fcdee1..12a5e1deb2ca 100644 --- a/src/docs_main.cr +++ b/src/docs_main.cr @@ -51,6 +51,7 @@ require "./option_parser" require "./partial_comparable" require "./random/**" require "./readline" +require "./registry" require "./signal" require "./string_pool" require "./string_scanner" diff --git a/src/lib_c/x86_64-windows-msvc/c/win_def.cr b/src/lib_c/x86_64-windows-msvc/c/win_def.cr index 8117a19a9d0f..4ab7cf47c291 100644 --- a/src/lib_c/x86_64-windows-msvc/c/win_def.cr +++ b/src/lib_c/x86_64-windows-msvc/c/win_def.cr @@ -2,4 +2,9 @@ lib LibC alias WORD = UInt16 alias BOOL = Int32 alias BYTE = UChar + alias PHKEY = HKEY* + alias LPDWORD = DWORD* + alias LPBYTE = BYTE* + + type HKEY = Void* end diff --git a/src/lib_c/x86_64-windows-msvc/c/winbase.cr b/src/lib_c/x86_64-windows-msvc/c/winbase.cr index 6fb4c3620bf0..dade729138eb 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -42,11 +42,24 @@ lib LibC daylightBias : LONG end + struct DYNAMIC_TIME_ZONE_INFORMATION + bias : LONG + standardName : StaticArray(WCHAR, 32) + standardDate : SYSTEMTIME + standardBias : LONG + daylightName : StaticArray(WCHAR, 32) + daylightDate : SYSTEMTIME + daylightBias : LONG + timeZoneKeyName : StaticArray(WCHAR, 128) + dynamicDaylightTimeDisabled : BOOL + end + TIME_ZONE_ID_UNKNOWN = 0_u32 TIME_ZONE_ID_STANDARD = 1_u32 TIME_ZONE_ID_DAYLIGHT = 2_u32 fun GetTimeZoneInformation(tz_info : TIME_ZONE_INFORMATION*) : DWORD + fun GetDynamicTimeZoneInformation(dtz_info : DYNAMIC_TIME_ZONE_INFORMATION*) : DWORD fun GetSystemTimeAsFileTime(time : FILETIME*) fun GetSystemTimePreciseAsFileTime(time : FILETIME*) @@ -90,4 +103,5 @@ lib LibC fun GetEnvironmentStringsW : LPWCH fun FreeEnvironmentStringsW(lpszEnvironmentBlock : LPWCH) : BOOL fun SetEnvironmentVariableW(lpName : LPWSTR, lpValue : LPWSTR) : BOOL + fun ExpandEnvironmentStringsW(lpSrc : LPWSTR, lpDst : LPWSTR, nSize : DWORD) : DWORD end diff --git a/src/lib_c/x86_64-windows-msvc/c/winreg.cr b/src/lib_c/x86_64-windows-msvc/c/winreg.cr new file mode 100644 index 000000000000..cd98050d8cdb --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/winreg.cr @@ -0,0 +1,114 @@ +lib LibC + enum REGSAM + # Combines the `STANDARD_RIGHTS_REQUIRED`, `QUERY_VALUE`, `SET_VALUE`, `CREATE_SUB_KEY`, `ENUMERATE_SUB_KEYS`, `NOTIFY`, and `CREATE_LINK` access rights. + ALL_ACCESS = 0xf003f + + # Reserved for system use. + CREATE_LINK = 0x00020 + + # Required to create a subkey of a registry key. + CREATE_SUB_KEY = 0x00004 + + # Required to enumerate the subkeys of a registry key. + ENUMERATE_SUB_KEYS = 0x00008 + + # Equivalent to `READ`. + EXECUTE = 0x20019 + + # Required to request change notifications for a registry key or for subkeys of a registry key. + NOTIFY = 0x00010 + + # Required to query the values of a registry key. + QUERY_VALUE = 0x00001 + + # Combines the `STANDARD_RIGHTS_READ`, `QUERY_VALUE`, `ENUMERATE_SUB_KEYS`, and `NOTIFY` values. + READ = 0x20019 + + # Required to create, delete, or set a registry value. + SET_VALUE = 0x00002 + + # Indicates that an application on 64-bit Windows should operate on the 32-bit registry view. This flag is ignored by 32-bit Windows. + # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. + # Windows 2000: This flag is not supported. + WOW64_32KEY = 0x00200 + + # Indicates that an application on 64-bit Windows should operate on the 64-bit registry view. This flag is ignored by 32-bit Windows. + # This flag must be combined using the OR operator with the other flags in this table that either query or access registry values. + # Windows 2000: This flag is not supported. + WOW64_64KEY = 0x00100 + + # Combines the STANDARD_RIGHTS_WRITE, `KEY_SET_VALUE`, and `KEY_CREATE_SUB_KEY` access rights. + WRITE = 0x20006 + end + + enum ValueType + NONE = 0 + SZ = 1 + EXPAND_SZ = 2 + BINARY = 3 + DWORD = 4 + DWORD_LITTLE_ENDIAN = DWORD + DWORD_BIG_ENDIAN = 5 + LINK = 6 + MULTI_SZ = 7 + RESOURCE_LIST = 8 + FULL_RESOURCE_DESCRIPTOR = 9 + RESOURCE_REQUIREMENTS_LIST = 10 + QWORD = 11 + QWORD_LITTLE_ENDIAN = QWORD + end + + HKEY_CLASSES_ROOT = Pointer(Void).new(0x80000000).as(HKEY) + HKEY_CURRENT_USER = Pointer(Void).new(0x80000001).as(HKEY) + HKEY_LOCAL_MACHINE = Pointer(Void).new(0x80000002).as(HKEY) + HKEY_USERS = Pointer(Void).new(0x80000003).as(HKEY) + HKEY_PERFORMANCE_DATA = Pointer(Void).new(0x80000004).as(HKEY) + HKEY_CURRENT_CONFIG = Pointer(Void).new(0x80000005).as(HKEY) + HKEY_DYN_DATA = Pointer(Void).new(0x8000006).as(HKEY) + + enum RegOption + NON_VOLATILE = 0x00000000 + VOLATILE = 0x00000001 + CREATE_LINK = 0x00000002 + BACKUP_RESTORE = 0x00000004 + end + + enum RegDisposition : UInt32 + CREATED_NEW_KEY = 0x00000001 + OPENED_EXISTING_KEY = 0x00000002 + end + + alias LSTATUS = DWORD + fun RegOpenKeyExW(hKey : HKEY, lpSubKey : LPWSTR, ulOptions : DWORD, samDesired : REGSAM, phkResult : PHKEY) : LSTATUS + fun RegCloseKey(hKey : HKEY) : LSTATUS + fun RegCreateKeyExW(hKey : HKEY, lpSubKey : LPWSTR, reserved : DWORD, lpClass : LPWSTR, dwOptions : RegOption, + samDesired : REGSAM, lpSecurityAttributes : SECURITY_ATTRIBUTES*, phkResult : PHKEY, lpdwDisposition : LibC::RegDisposition*) : LSTATUS + fun RegDeleteKeyExW(hKey : HKEY, lpSubKey : LPWSTR, samDesired : DWORD, reserved : DWORD) : DWORD + + fun RegQueryValueExW(hKey : HKEY, lpValueName : LPWSTR, lpReserved : LPDWORD, lpType : ValueType*, lpData : LPBYTE, lpcbData : LPDWORD) : LSTATUS + fun RegQueryInfoKeyW(hKey : HKEY, lpClass : LPSTR, lpcchClass : LPDWORD, lpReserved : LPDWORD, + lpcSubKeys : LPDWORD, lpcbMaxSubKeyLen : LPDWORD, + lpcbMaxClassLen : LPDWORD, + lpcValues : LPDWORD, lpcbMaxValueNameLen : LPDWORD, lpcbMaxValueLen : LPDWORD, + lpcbSecurityDescriptor : LPDWORD, lpftLastWriteTime : FILETIME*) : DWORD + fun RegSetValueExW(hKey : HKEY, lpValueName : LPWSTR, reserved : DWORD, dwType : DWORD, lpData : BYTE*, cbData : DWORD) : LSTATUS + + fun RegEnumValueW(hKey : HKEY, dwIndex : DWORD, + lpValueName : LPWSTR, lpcchValueName : LPDWORD, + lpReserved : LPDWORD, lpType : ValueType*, + lpData : LPBYTE, lpcbData : LPDWORD) : LSTATUS + fun RegEnumKeyExW(hKey : HKEY, dwIndex : DWORD, + lpName : LPWSTR, lpcchName : LPDWORD, + lpReserved : LPDWORD, + lpClass : LPWSTR, lpcchClass : LPDWORD, + lpftLastWriteTime : FILETIME*) : LSTATUS + fun RegLoadMUIStringW( + hKey : HKEY, + pszValue : LPWSTR, + pszOutBuf : LPWSTR, + cbOutBuf : DWORD, + pcbData : LPDWORD, + flags : DWORD, + pszDirectory : LPWSTR + ) : LSTATUS +end diff --git a/src/registry.cr b/src/registry.cr new file mode 100644 index 000000000000..b3deb2f7f0ae --- /dev/null +++ b/src/registry.cr @@ -0,0 +1,588 @@ +{% skip_file unless flag?(:win32) %} +require "winerror" +require "c/winreg" + +# This API povides access to the Windows registry. The main type is `Key`. +# +# ``` +# Registry::LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Registry::SAM::QUERY_VALUE) do |key| +# key.get_string("SystemRoot") # => "C:\\WINDOWS" +# end +# ``` +# +# The Windows API defines some [predefined root keys](https://docs.microsoft.com/en-us/windows/desktop/sysinfo/predefined-keys) that are always open. +# They are available as constants and can be used as entry points to the registry. +# +# * `HKEY_CLASSES_ROOT` +# * `HKEY_CURRENT_USER` +# * `HKEY_LOCAL_MACHINE` +# * `HKEY_USERS` +# * `HKEY_CURRENT_CONFIG` +module Registry + # Information about file types and their properties. + CLASSES_ROOT = Key.new(LibC::HKEY_CLASSES_ROOT, "HKEY_CLASSES_ROOT") + + # Preferences of the current user. + # + # The kay maps to the current user's subkey in `HKEY_USERS`. + CURRENT_USER = Key.new(LibC::HKEY_CURRENT_USER, "HKEY_CURRENT_USER") + + # Information about the physical state of the computer, including installed hardware and software. + LOCAL_MACHINE = Key.new(LibC::HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE") + + # Preferences of all users. + # + # This key contains a subkey per user. + USERS = Key.new(LibC::HKEY_USERS, "HKEY_USERS") + + # Provides access to performance data. + PERFORMANCE_DATA = Key.new(LibC::HKEY_PERFORMANCE_DATA, "HKEY_PERFORMANCE_DATA") + + # Information about the hardware profile of the local computer system. + CURRENT_CONFIG = Key.new(LibC::HKEY_CURRENT_CONFIG, "HKEY_CURRENT_CONFIG") + + # Registry error. + class Error < Exception + end + + # Registry key security and access rights. + # See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724878.aspx + # for details. + alias SAM = LibC::REGSAM + + # Registry value types. + alias ValueType = LibC::ValueType + + # Union of Crystal types representing a registry value. + alias Value = Bytes | Int32 | String | Int64 | Array(String) + + # This type represents a handle to an open Windows registry key. + # + # Keys can be obtained by calling `#open` on an already opened key. + # The predefined root keys are defined as constants in `Registry`. + struct Key + @handle : LibC::HKEY + @name : String + + # Creates a new instance from an open Windows *handle* (`HKEY`) and *name*. + # + # NOTE: This method is only useful if a Windows handle is retrieved from + # calling external code. Usually, new keys are created by opening subkeys + # of the predefined root keys available as constants in `Registry`. + def initialize(@handle : LibC::HKEY, @name : String) + end + + # Returns the full path of this key. + getter name : String + + # Returns the Windows handle (`HKEY`) representing this key. + def to_unsafe : LibC::HKEY + @handle + end + + def get_raw?(name : String, buffer : Slice(UInt8)) : {ValueType, UInt32}? + name.check_no_null_byte + + get_raw?(name.to_utf16, buffer) + end + + private def get_raw?(name : Slice(UInt16), buffer : Bytes) : {ValueType, UInt32}? + length = buffer.size.to_u32 + status = LibC.RegQueryValueExW(self, name, nil, out valtype, buffer, pointerof(length)) + case status + when WinError::ERROR_SUCCESS + {valtype, length} + when WinError::ERROR_FILE_NOT_FOUND + nil + when WinError::ERROR_MORE_DATA + {valtype, length} + else + raise WinError.new("RegQueryValueExW", status) + end + end + + def get_raw(name : String, buffer : Slice(UInt8)) : {ValueType, UInt32} + get_raw?(name, buffer) || raise Error.new("Value #{name.inspect} does not exist") + end + + def get_raw(name : String) : {ValueType, Slice(UInt8)} + name.check_no_null_byte + + name_u16 = name.to_utf16 + + Crystal::System.retry_buffer do |buffer, small_buf| + valtype, length = get_raw?(name_u16, buffer) || raise Error.new("Value #{name.inspect} does not exist") + + if 0 <= length <= buffer.size + return {valtype, buffer[0, length]} + elsif small_buf && length > 0 + next length + else + raise Error.new("RegQueryValueExW retry buffer") + end + end + end + + def get_raw?(name : String) : {ValueType, Slice(UInt8)}? + name.check_no_null_byte + + name_u16 = name.to_utf16 + + Crystal::System.retry_buffer do |buffer, small_buf| + raw = get_raw?(name_u16, buffer) || return + valtype, length = raw + + if 0 <= length <= buffer.size + return {valtype, buffer[0, length]} + elsif small_buf && length > 0 + next length + else + raise Error.new("RegQueryValueExW retry buffer") + end + end + end + + def get_mui(name : String) : String? + get_mui?(name) || raise Error.new("Value '#{name}' does not exist") + end + + def get_mui?(name : String) : String? + name.check_no_null_byte + name_u16 = name.to_utf16 + + Crystal::System.retry_wstr_buffer do |buffer, small_buf| + length = buffer.size.to_u32 + pointer = buffer.to_unsafe + + status = LibC.RegLoadMUIStringW(self, name_u16, pointer, length, pointerof(length), 0, Pointer(UInt16).null) + + if status == WinError::ERROR_FILE_NOT_FOUND + # Try to resolve the string value using the system directory as + # a DLL search path; this assumes the string value is of the form + # @[path]\dllname,-strID but with no path given, e.g. @tzres.dll,-320. + + # This approach works with tzres.dll but may have to be revised + # in the future to allow callers to provide custom search paths. + pdir = Crystal::System::Env.expand("%SystemRoot%\\system32\\".to_utf16) + + length = buffer.size.to_u32 + status = LibC.RegLoadMUIStringW(self, name_u16, pointer, length, pointerof(length), 0, pdir) + end + + case status + when WinError::ERROR_SUCCESS + if 0 < length <= buffer.size + # returned length is in bytes, so we need to divide by 2 to get WCHAR length + return String.from_utf16(buffer[0, length / 2 - 1]) + elsif small_buf && length > 0 + next length + else + raise Error.new("RegLoadMUIStringW") + end + when WinError::ERROR_FILE_NOT_FOUND + return + else + raise WinError.new("RegLoadMUIStringW", status) + end + end + end + + def get(name : String) : Value + cast_value *get_raw(name) + end + + def get?(name : String) : Value? + if raw = get_raw?(name) + cast_value *raw + end + end + + def get_string?(name : String) : String? + if raw = get_raw?(name) + value_string *raw + end + end + + def get_string(name : String) : String + value_string *get_raw(name) + end + + def set(name : String, data : Bytes, type : ValueType = ValueType::BINARY) : Nil + name.check_no_null_byte("name") + + status = LibC.RegSetValueExW(self, name.to_utf16, 0, type, data, data.bytesize) + unless status == WinError::ERROR_SUCCESS + raise WinError.new("WinRegSetValueExW", status) + end + end + + def set(name : String, value : String, type : ValueType = ValueType::SZ) : Nil + value.check_no_null_byte("value") + + u16_slice = value.to_utf16 + u8_slice = u16_slice.to_unsafe.as(Pointer(UInt8)).to_slice(u16_slice.bytesize) + + set(name, u8_slice, type) + end + + def set(name : String, value : Int32, format : IO::ByteFormat = IO::ByteFormat::LittleEndian) : Nil + raw = uninitialized UInt8[4] + format.encode(value, raw.to_slice) + set(name, raw.to_slice, ValueType::DWORD) + end + + def set(name : String, value : Int64, format : IO::ByteFormat = IO::ByteFormat::LittleEndian) : Nil + raw = uninitialized UInt8[8] + format.encode(value, raw.to_slice) + set(name, raw.to_slice, ValueType::QWORD) + end + + def set(name : String, value : Enumerable(String)) : Nil + io = IO::Memory.new + value.each do |string| + string.check_no_null_byte + + u16_slice = string.to_utf16 + io.write u16_slice.to_unsafe.as(Pointer(UInt8)).to_slice(u16_slice.bytesize) + io.write_byte 0_u8 + io.write_byte 0_u8 + end + io.write_byte 0_u8 + io.write_byte 0_u8 + + set(name, io.to_slice, ValueType::MULTI_SZ) + end + + private def value_string(valtype, buffer) : String? + case valtype + when ValueType::SZ + value_string(buffer) + when ValueType::EXPAND_SZ + value_string(buffer, expand: true) + else + raise Error.new("Expected string value type, found #{valtype.inspect}") + end + end + + private def value_string(buffer : Bytes, expand = false) : String + wchar_buffer = buffer.to_unsafe.as(Pointer(UInt16)).to_slice(buffer.size / 2 - 1) + if expand + wchar_buffer = Crystal::System::Env.expand(wchar_buffer) + wchar_buffer = wchar_buffer[0, wchar_buffer.size - 1] + end + String.from_utf16(wchar_buffer) + end + + private def value_multi_string(buffer : Bytes) : Array(String) + wchar_buffer = buffer.to_unsafe.as(Pointer(UInt16)) + strings = [] of String + + end_pointer = (buffer.to_unsafe + buffer.bytesize).as(Pointer(UInt16)) - 1 + while wchar_buffer < end_pointer + string, wchar_buffer = String.from_utf16(wchar_buffer) + strings << string + end + + strings + end + + private def cast_value(valtype : ValueType, buffer : Bytes) : Value + case valtype + when ValueType::BINARY + buffer + when ValueType::DWORD + IO::ByteFormat::LittleEndian.decode(Int32, buffer) + when ValueType::DWORD_BIG_ENDIAN + IO::ByteFormat::BigEndian.decode(Int32, buffer) + when ValueType::EXPAND_SZ + value_string(buffer, expand: true) + when ValueType::LINK + buffer + when ValueType::MULTI_SZ + value_multi_string(buffer) + when ValueType::QWORD + IO::ByteFormat::LittleEndian.decode(Int64, buffer) + when ValueType::SZ + value_string(buffer) + when ValueType::NONE + buffer + else + raise "unreachable" + end + end + + # Opens a subkey at path *name* and returns the new key or `nil` if it does not exist. + # + # Users need to ensure the opened key will be closed after usage (see `#close`). + # *sam* specifies desired access rights to the key to be opened. + def open?(sub_name : String, sam : SAM = SAM::READ) : Key? + sub_name.check_no_null_byte + + status = LibC.RegOpenKeyExW(self, sub_name.to_utf16, 0, sam, out sub_handle) + + case status + when WinError::ERROR_SUCCESS + Key.new(sub_handle, {@name, sub_name}.join('\\')) + when WinError::ERROR_FILE_NOT_FOUND + else + raise WinError.new("RegOpenKeyExW", status) + end + end + + # Opens a subkey at path *name* and returns the new key. + # + # Users need to ensure the opened key will be closed after usage (see `#close`). + # Raises `Registry::Error` if the subkey does not exist. + # + # *sam* specifies desired access rights to the key to be opened. + def open(sub_name : String, sam : SAM = SAM::READ) : Key + open?(sub_name, sam) || raise Error.new("Key #{sub_name} does not exist.") + end + + # Opens a subkey at path *name* and yields it to the block. + # + # The key is automatically closed after the block returns. + # + # Raises `Registry::Error` if the subkey does not exist. + # + # *sam* specifies desired access rights to the key to be opened. + def open(sub_name : String, sam : SAM = SAM::READ, &block : Key ->) + sub_key = open(sub_name, sam) + begin + yield sub_key + ensure + sub_key.close + end + end + + # Closes the handle. + def close : Nil + status = LibC.RegCloseKey(self) + + unless status == WinError::ERROR_SUCCESS || status == WinError::ERROR_INVALID_HANDLE + raise WinError.new("RegCloseKey", status) + end + end + + # Retrieves information about the key. + def info : KeyInfo + info = uninitialized KeyInfo + status = LibC.RegQueryInfoKeyW(self, nil, nil, nil, + pointerof(info.@sub_key_count), pointerof(info.@max_sub_key_length), nil, + pointerof(info.@value_count), pointerof(info.@max_value_name_length), + pointerof(info.@max_value_length), nil, out last_write_time) + + unless status == WinError::ERROR_SUCCESS + raise WinError.new("RegQueryInfoKeyW", status) + end + + seconds, nanoseconds = Crystal::System::Time.filetime_to_seconds_and_nanoseconds(last_write_time) + pointerof(info.@last_write_time).value = Time.utc(seconds: seconds, nanoseconds: nanoseconds) + info + end + + # Describes the statistics of a registry key. It is returned by `Key#info`. + struct KeyInfo + private def initialize + @sub_key_count = 0 + @max_sub_key_length = 0 + @value_count = 0 + @max_value_name_length = 0 + @max_value_length = 0 + @last_write_time = Time.utc_now + end + + getter sub_key_count : UInt32 + + # size of the key's subkey with the longest name, in Unicode characters, not including the terminating null byte. + getter max_sub_key_length : UInt32 + + getter value_count : UInt32 + # size of the key's longest value name, in Unicode characters, not including the terminating null byte. + + getter max_value_name_length : UInt32 + + # longest data component among the key's values, in bytes. + getter max_value_length : UInt32 + + getter last_write_time : Time + end + + # Returns a hash of all values in this key. + def values : Hash(String, String | {ValueType, Bytes}) + values = {} of String => String | {ValueType, Bytes} + each_value do |name, value| + values[name] = value + end + values + end + + # Iterates all value names in this key and yields them to the block. + def each_name(&block : String ->) : Nil + info = self.info + buffer = Slice(UInt16).new(info.max_value_name_length + 1) + + info.value_count.times do |i| + length = buffer.size.to_u32 + status = LibC.RegEnumValueW(self, i, buffer, pointerof(length), nil, nil, nil, nil) + case status + when WinError::ERROR_SUCCESS + yield String.from_utf16(buffer[0, length]) + when WinError::ERROR_NO_MORE_ITEMS + break + else + raise WinError.new("RegEnumValueW", status) + end + end + end + + # Returns all names in this key. + def names : Array(String) + names = [] of String + each_name do |name| + names << name + end + names + end + + # Iterates all values in this key and yields the name and value to the block. + def each_value(&block : (String, Value) ->) : Nil + info = self.info + name_buffer = Slice(UInt16).new(info.max_value_name_length + 1) + data_buffer = Slice(UInt8).new(info.max_value_length + 1) + + info.value_count.times do |i| + name_length = name_buffer.size.to_u32 + data_length = data_buffer.size.to_u32 + status = LibC.RegEnumValueW(self, i, name_buffer, pointerof(name_length), nil, + out valtype, data_buffer, pointerof(data_length)) + case status + when WinError::ERROR_SUCCESS + yield String.from_utf16(name_buffer[0, name_length]), cast_value(valtype, data_buffer[0, data_length]) + when WinError::ERROR_NO_MORE_ITEMS + break + else + raise WinError.new("RegEnumValueW", status) + end + end + end + + # Iterates all subkey names in this key and yields them to the block. + def each_key(&block : String ->) : Nil + info = self.info + + name_buffer = Slice(UInt16).new(info.max_sub_key_length + 1) + info.sub_key_count.times do |i| + name_length = name_buffer.size.to_u32 + status = LibC.RegEnumKeyExW(self, i, name_buffer, pointerof(name_length), nil, nil, nil, out last_write_time) + case status + when WinError::ERROR_SUCCESS + yield String.from_utf16(name_buffer[0, name_length]) + when WinError::ERROR_NO_MORE_ITEMS + break + else + raise WinError.new("RegEnumKeyExW", status) + end + end + end + + # Returns all subkey names in this key. + def subkeys : Array(String) + subkeys = [] of String + each_key do |key| + subkeys << key + end + subkeys + end + + # Creates a subkey called *name*. + # + # *sam* specifies the access rights for the key to be created. + def create_key(name : String, sam : SAM = SAM::CREATE_SUB_KEY) : Nil + create_key?(name, sam) || raise WinError.new("RegCreateKeyExW") + end + + # Creates a subkey called *name* and returns a boolean indicating whether it was successfully created. + # Returns `false` if the key could not be created or already existed. + # + # *sam* specifies the access rights for the key to be created. + def create_key?(name : String, sam : SAM = SAM::CREATE_SUB_KEY) : Bool + name.check_no_null_byte + + status = LibC.RegCreateKeyExW(self, name.to_utf16, 0, nil, LibC::RegOption::NON_VOLATILE, sam, nil, out sub_handle, out disposition) + + unless status == WinError::ERROR_SUCCESS + return false + end + + begin + case disposition + when LibC::RegDisposition::CREATED_NEW_KEY + when LibC::RegDisposition::OPENED_EXISTING_KEY + return false + end + + true + ensure + LibC.RegCloseKey(sub_handle) + end + end + + # Deletes the subkey *name* and its values. + # + # If *recursive* is `true`, it recursively deletes subkeys. + # + # Raises `Registry::Error` if the subkey *name* does not exist. + def delete_key(name : String, recursive : Bool = true) : Nil + status = delete_key_impl(name, recursive) do |key, subname| + key.delete_key(subname) + end + + unless status == WinError::ERROR_SUCCESS + raise WinError.new("RegDeleteKeyExW", status) + end + end + + # Deletes the subkey *name* and its values. + # + # If *recursive* is `true`, it recursively deletes subkeys. + # + # Returns `false` if the subkey *name* does not exist. + def delete_key?(name : String, recursive : Bool = true) : Bool + status = delete_key_impl(name, recursive) do |key, subname| + key.delete_key?(subname) || return false + end + + case status + when WinError::ERROR_SUCCESS + true + when WinError::ERROR_FILE_NOT_FOUND + false + else + raise WinError.new("RegDeleteKeyExW", status) + end + end + + private def delete_key_impl(name, recursive) + name.check_no_null_byte + name_u16 = name.to_utf16 + + status = LibC.RegDeleteKeyExW(self, name_u16, 0, 0) + + if status == WinError::ERROR_ACCESS_DENIED && recursive + # Need to delete subkeys first, then try again + open(name, SAM::ALL_ACCESS) do |key| + # Can't use key.each_key here because the iterator would be disturbed + # by deleting keys. + key.subkeys.each do |subname| + yield key, subname + end + end + + status = LibC.RegDeleteKeyExW(self, name_u16, 0, 0) + end + + status + end + end +end