Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Windows Registry API #6698

Closed
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
250 changes: 250 additions & 0 deletions spec/std/registry_spec.cr
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions src/crystal/system/win32/env.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 19 additions & 3 deletions src/crystal/system/win32/time.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "c/winbase"
require "winerror"
require "registry"
require "./zone_names"

module Crystal::System::Time
Expand All @@ -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
Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions src/crystal/system/windows.cr
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
1 change: 1 addition & 0 deletions src/docs_main.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require "./option_parser"
require "./partial_comparable"
require "./random/**"
require "./readline"
require "./registry"
require "./signal"
require "./string_pool"
require "./string_scanner"
Expand Down
5 changes: 5 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/win_def.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/winbase.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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*)

Expand Down Expand Up @@ -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
Loading