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

Extract platform-specifics from ENV to Crystal::System::Env and implement for win32 #6333

Merged
merged 11 commits into from
Jul 24, 2018
Merged
24 changes: 21 additions & 3 deletions spec/std/env_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@ describe "ENV" do

describe "[]=" do
it "disallows NUL-bytes in key" do
expect_raises(ArgumentError, "Key contains null byte") do
expect_raises(ArgumentError, "String `key` contains null byte") do
ENV["FOO\0BAR"] = "something"
end
end

it "disallows NUL-bytes in key if value is nil" do
expect_raises(ArgumentError, "Key contains null byte") do
expect_raises(ArgumentError, "String `key` contains null byte") do
ENV["FOO\0BAR"] = nil
end
end

it "disallows NUL-bytes in value" do
expect_raises(ArgumentError, "Value contains null byte") do
expect_raises(ArgumentError, "String `value` contains null byte") do
ENV["FOO"] = "BAR\0BAZ"
end
end
Expand Down Expand Up @@ -95,4 +95,22 @@ describe "ENV" do
end
end
end

it "handles unicode" do
ENV["TEST_UNICODE_1"] = "bar\u{d7ff}\u{10000}"
ENV["TEST_UNICODE_2"] = "\u{1234}"
ENV["TEST_UNICODE_1"].should eq "bar\u{d7ff}\u{10000}"
ENV["TEST_UNICODE_2"].should eq "\u{1234}"

values = {} of String => String
ENV.each do |key, value|
if key.starts_with?("TEST_UNICODE_")
values[key] = value
end
end
values.should eq({
"TEST_UNICODE_1" => "bar\u{d7ff}\u{10000}",
"TEST_UNICODE_2" => "\u{1234}",
})
end
end
23 changes: 21 additions & 2 deletions spec/std/string/utf16_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,50 @@ describe "String UTF16" do
end
end

describe "from_utf16" do
describe ".from_utf16" do
it "in the range U+0000..U+D7FF" do
input = Slice[0_u16, 0x68_u16, 0x65_u16, 0x6c_u16, 0x6c_u16, 0x6f_u16, 0xd7ff_u16]
String.from_utf16(input).should eq("\u{0}hello\u{d7ff}")
String.from_utf16(input.to_unsafe).should eq({"", input.to_unsafe + 1})
end

it "in the range U+E000 to U+FFFF" do
input = Slice[0xe000_u16, 0xffff_u16]
String.from_utf16(input).should eq("\u{e000}\u{ffff}")

pointer = Slice[0xe000_u16, 0xffff_u16, 0_u16].to_unsafe
String.from_utf16(pointer).should eq({"\u{e000}\u{ffff}", pointer + 3})
end

it "in the range U+10000..U+10FFFF" do
input = Slice[0xd800_u16, 0xdc00_u16]
String.from_utf16(input).should eq("\u{10000}")

pointer = Slice[0xd800_u16, 0xdc00_u16, 0_u16].to_unsafe
String.from_utf16(pointer).should eq({"\u{10000}", pointer + 3})
end

it "in the range U+D800..U+DFFF" do
input = Slice[0xdc00_u16, 0xd800_u16]
String.from_utf16(input).should eq("\u{fffd}\u{fffd}")

pointer = Slice[0xdc00_u16, 0xd800_u16, 0_u16].to_unsafe
String.from_utf16(pointer).should eq({"\u{fffd}\u{fffd}", pointer + 3})
end

it "handles null bytes" do
slice = Slice[104_u16, 105_u16, 0_u16, 55296_u16, 56485_u16]
String.from_utf16(slice).should eq("hi\0000𐂥")
String.from_utf16(slice.to_unsafe).should eq("hi")
String.from_utf16(slice.to_unsafe).should eq({"hi", slice.to_unsafe + 3})
end

it "with pointer reads multiple strings" do
input = Slice[0_u16, 0x68_u16, 0x65_u16, 0x6c_u16, 0x6c_u16, 0x6f_u16, 0xd7ff_u16, 0_u16]
pointer = input.to_unsafe
string, pointer = String.from_utf16(pointer)
string.should eq("")
string, pointer = String.from_utf16(pointer)
string.should eq("hello\u{d7ff}")
end
end
end
19 changes: 19 additions & 0 deletions src/crystal/system/env.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Crystal::System::Env
# Sets an environment variable or unsets it if *value* is `nil`.
# def self.set(key : String, value : String?) : Nil

# Gets an environment variable.
# def self.get(key : String) : String?

# Returns `true` if environment variable is set.
# def self.has_key?(key : String) : Bool

# Iterates all environment variables.
# def self.each(&block : String, String ->)
end

{% if flag?(:win32) %}
require "./win32/env"
{% else %}
require "./unix/env"
{% end %}
55 changes: 55 additions & 0 deletions src/crystal/system/unix/env.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require "c/stdlib"

module Crystal::System::Env
# Sets an environment variable.
def self.set(key : String, value : String) : Nil
key.check_no_null_byte("key")
value.check_no_null_byte("value")

if LibC.setenv(key, value, 1) != 0
raise Errno.new("setenv")
end
end

# Unsets an environment variable.
def self.set(key : String, value : Nil) : Nil
key.check_no_null_byte("key")

if LibC.unsetenv(key) != 0
raise Errno.new("unsetenv")
end
end

# Gets an environment variable.
def self.get(key : String) : String?
key.check_no_null_byte("key")

if value = LibC.getenv(key)
String.new(value)
end
end

# Returns `true` if environment variable is set.
def self.has_key?(key : String) : Bool
key.check_no_null_byte("key")

!!LibC.getenv(key)
end

# Iterates all environment variables.
def self.each(&block : String, String ->)
environ_ptr = LibC.environ
while environ_ptr
environ_value = environ_ptr.value
if environ_value
key_value = String.new(environ_value).split('=', 2)
key = key_value[0]
value = key_value[1]? || ""
yield key, value
environ_ptr += 1
else
break
end
end
end
end
67 changes: 67 additions & 0 deletions src/crystal/system/win32/env.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require "crystal/system/windows"
require "c/winbase"

module Crystal::System::Env
# Sets an environment variable or unsets it if *value* is `nil`.
def self.set(key : String, value : String) : Nil
key.check_no_null_byte("key")
value.check_no_null_byte("value")

if LibC.SetEnvironmentVariableW(key.to_utf16, value.to_utf16) == 0
raise WinError.new("SetEnvironmentVariableW")
end
end

# Unsets an environment variable.
def self.set(key : String, value : Nil) : Nil
key.check_no_null_byte("key")

if LibC.SetEnvironmentVariableW(key.to_utf16, nil) == 0
raise WinError.new("SetEnvironmentVariableW")
end
end

# Gets an environment variable.
def self.get(key : String) : String?
key.check_no_null_byte("key")

System.retry_wstr_buffer do |buffer, small_buf|
length = LibC.GetEnvironmentVariableW(key.to_utf16, buffer, buffer.size)
if 0 < length < buffer.size
return String.from_utf16(buffer[0, length])
elsif small_buf && length > 0
next length
elsif length == 0 && LibC.GetLastError == WinError::ERROR_ENVVAR_NOT_FOUND
return
else
raise WinError.new("GetEnvironmentVariableW")
end
end
end

# Returns `true` if environment variable is set.
def self.has_key?(key : String) : Bool
key.check_no_null_byte("key")

buffer = uninitialized UInt16[1]
LibC.GetEnvironmentVariableW(key.to_utf16, buffer, buffer.size) != 0
end

# Iterates all environment variables.
def self.each(&block : String, String ->)
orig_pointer = pointer = LibC.GetEnvironmentStringsW
raise WinError.new("GetEnvironmentStringsW") if pointer.null?

begin
while !pointer.value.zero?
string, pointer = String.from_utf16(pointer)
key_value = string.split('=', 2)
key = key_value[0]
value = key_value[1]? || ""
yield key, value
end
ensure
LibC.FreeEnvironmentStringsW(orig_pointer)
end
end
end
4 changes: 2 additions & 2 deletions src/crystal/system/win32/time.cr
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,13 @@ module Crystal::System::Time

# Normalizes the names of the standard and dst zones.
private def self.normalize_zone_names(info : LibC::TIME_ZONE_INFORMATION) : Tuple(String, String)
stdname = String.from_utf16(info.standardName.to_unsafe)
stdname = String.from_utf16(info.standardName.to_slice)

if normalized_names = WINDOWS_ZONE_NAMES[stdname]?
return normalized_names
end

dstname = String.from_utf16(info.daylightName.to_unsafe)
dstname = String.from_utf16(info.daylightName.to_slice)

if english_name = translate_zone_name(stdname, dstname)
if normalized_names = WINDOWS_ZONE_NAMES[english_name]?
Expand Down
39 changes: 11 additions & 28 deletions src/env.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "c/stdlib"
require "crystal/system/env"

# `ENV` is a hash-like accessor for environment variables.
#
Expand Down Expand Up @@ -35,24 +35,15 @@ module ENV
#
# If *key* or *value* contains a null-byte an `ArgumentError` is raised.
def self.[]=(key : String, value : String?)
raise ArgumentError.new("Key contains null byte") if key.byte_index(0)
Crystal::System::Env.set(key, value)

if value
raise ArgumentError.new("Value contains null byte") if value.byte_index(0)

if LibC.setenv(key, value, 1) != 0
raise Errno.new("Error setting environment variable #{key.inspect}")
end
else
LibC.unsetenv(key)
end
value
end

# Returns `true` if the environment variable named *key* exists and `false`
# if it doesn't.
def self.has_key?(key : String) : Bool
!!LibC.getenv(key)
Crystal::System::Env.has_key?(key)
end

# Retrieves a value corresponding to the given *key*. Raises a `KeyError` exception if the
Expand All @@ -72,9 +63,11 @@ module ENV
# Retrieves a value corresponding to a given *key*. Return the value of the block if
# the *key* does not exist.
def self.fetch(key : String, &block : String -> String?)
value = LibC.getenv key
return String.new(value) if value
yield(key)
if value = Crystal::System::Env.get(key)
return value
else
yield key
end
end

# Returns an array of all the environment variable names.
Expand All @@ -95,7 +88,7 @@ module ENV
# the environment variable existed, otherwise returns `nil`.
def self.delete(key : String) : String?
if value = self[key]?
LibC.unsetenv(key)
Crystal::System::Env.set(key, nil)
value
else
nil
Expand All @@ -111,18 +104,8 @@ module ENV
# end
# ```
def self.each
environ_ptr = LibC.environ
while environ_ptr
environ_value = environ_ptr.value
if environ_value
key_value = String.new(environ_value).split('=', 2)
key = key_value[0]
value = key_value[1]? || ""
yield({key, value})
environ_ptr += 1
else
break
end
Crystal::System::Env.each do |key, value|
yield key, value
end
end

Expand Down
1 change: 0 additions & 1 deletion src/lib_c/x86_64-windows-msvc/c/stdlib.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ lib LibC
fun div(numer : Int, denom : Int) : DivT
fun exit(status : Int) : NoReturn
fun free(ptr : Void*) : Void
fun getenv(name : Char*) : Char*
fun malloc(size : SizeT) : Void*
fun putenv(string : Char*) : Int
fun realloc(ptr : Void*, size : SizeT) : Void*
Expand Down
5 changes: 5 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 @@ -85,4 +85,9 @@ lib LibC
INVALID_HANDLE_VALUE = HANDLE.new(-1)

fun CloseHandle(hObject : HANDLE) : BOOL

fun GetEnvironmentVariableW(lpName : LPWSTR, lpBuffer : LPWSTR, nSize : DWORD) : DWORD
fun GetEnvironmentStringsW : LPWCH
fun FreeEnvironmentStringsW(lpszEnvironmentBlock : LPWCH) : BOOL
fun SetEnvironmentVariableW(lpName : LPWSTR, lpValue : LPWSTR) : BOOL
end
1 change: 1 addition & 0 deletions src/lib_c/x86_64-windows-msvc/c/winnt.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ lib LibC
alias WCHAR = UInt16
alias LPSTR = CHAR*
alias LPWSTR = WCHAR*
alias LPWCH = WCHAR*

alias HANDLE = Void*

Expand Down
7 changes: 5 additions & 2 deletions src/string.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4244,8 +4244,11 @@ class String
# Raises an `ArgumentError` if `self` has null bytes. Returns `self` otherwise.
#
# This method should sometimes be called before passing a `String` to a C function.
def check_no_null_byte
raise ArgumentError.new("String contains null byte") if byte_index(0)
def check_no_null_byte(name = nil)
if byte_index(0)
name = "`#{name}` " if name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the backticks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To show that this refers to a name. This formatting is used in many other compiler and stdlib error messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, I never noticed that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not that common

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO Key contains null byte reads way better than String 'key' contains null byte.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asterite Do you mean name should be required?

Copy link
Contributor

@RX14 RX14 Jul 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps check_no_null_byte(what = "String") then just #{what} contains null byte?

Or just go with making it fully configurable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at the code, it always seems to be a check against an argument. To avoid repeating the argument name, maybe we can have:

class String
  macro check_no_null_byte(name)
    raise ArgumentError.new("Argument '{{name}}' contains null byte") if {{name}}.byte_index(0)
    {{name}}
  end
end

Then it can be used like this:

def some_method(arg)
  String.check_no_null_byte(arg)
  # do something
end

That way there's no string interpolation, and there's no need to repeat the argument name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bikeshedding and overengineering. The diff is fine as-is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RX14 I was writing the same thing. All are valid options and it doesn't make a difference regarding ENV.
Anyone can send a PR later to refactor check_no_null_byte if you care enough.

raise ArgumentError.new("String #{name}contains null byte")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this on two lines??

Also, I'd rather the entire exception message was passed in, i.e. def check_no_null_byte(message = "String contains null byte").

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the first comment, disagree with the second one.

Copy link
Contributor

@RX14 RX14 Jul 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing you save by doing it the other way is a bit of typing, and the benefit of having fully customized error messages. For "Key contains null byte" vs "String `key` contains null byte" - the former is way better. Please, let's keep it simple, understandable and flexible.

Copy link
Member Author

@straight-shoota straight-shoota Jul 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two lines because I forgot a if name in the first line. To avoid empty argument to be printed. That's why the specs fail.

Maybe full message is better. But actually, I don't think there is much benefit from it. Yes, it's more flexible, but I don't see any use for that. check_no_null_byte is only used to validate method arguments and for such it makes sense to declare the name of the argument if there are more than one. But apart from that, I don't see any need for customization.

end
self
end

Expand Down
Loading