Extract platform-specifics from ENV to Crystal::System::Env and implement for win32#6333
Conversation
src/crystal/system/unix/env.cr
Outdated
| module Crystal::System::Env | ||
| # Sets an environment variable. | ||
| def self.set(key : String, value : String) : Nil | ||
| raise ArgumentError.new("Key contains null byte") if key.byte_index(0) |
There was a problem hiding this comment.
key.check_no_bull_byte and down the diff
There was a problem hiding this comment.
That's what I initially thought, too. But check_no_null_byte loses the information which of the arguments was invalid (it's String contains null byte instead of Key contains null byte). Doesn't make much difference, so I'm not sure about it...
There was a problem hiding this comment.
Then refactor it to take an argument
src/crystal/system/win32/env.cr
Outdated
| System.retry_wstr_buffer do |buffer, small_buf| | ||
| length = LibC.GetEnvironmentVariableW(key.to_utf16, buffer, buffer.size) | ||
| if 0 < length < buffer.size | ||
| break String.from_utf16(buffer[0, length]) |
There was a problem hiding this comment.
This is probably slightly clearer using return instead of break.
src/env.cr
Outdated
| # if it doesn't. | ||
| def self.has_key?(key : String) : Bool | ||
| !!LibC.getenv(key) | ||
| !!Crystal::System::Env.set?(key) |
There was a problem hiding this comment.
rename this to has_key? and remove the !!
| alias WCHAR = UInt16 | ||
| alias LPSTR = CHAR* | ||
| alias LPWSTR = WCHAR* | ||
| alias LPCWSTR = WCHAR* |
There was a problem hiding this comment.
LPCWSTR shouldn't be an alias since it's just constant which means nothing ABI-wise.
There was a problem hiding this comment.
So... what should it be?
| lib LibC | ||
| fun GetLastError : DWORD | ||
|
|
||
| ERROR_ENVVAR_NOT_FOUND = 203_u32 |
There was a problem hiding this comment.
ca2905f to
7efc63e
Compare
52da7cc to
a68fd42
Compare
|
Rebased after #5623 was merged and ready for review 👍 |
a68fd42 to
9bf5cb8
Compare
| # | ||
| # Invalid values are encoded using the unicode replacement char with | ||
| # codepoint `0xfffd`. | ||
| def self.from_utf16(pointer : Pointer(UInt16)) : {String, Pointer(UInt16)} |
There was a problem hiding this comment.
This is a breaking change and as such needs some more discussion.
There was a problem hiding this comment.
Sure. When reading from a pointer it needs to advance the amount of UInt16 read. We can't determine that afterwards from String#bytesize because UTF-8 and UTF-16 have different bytesizes. We could rename the method with this changed behaviour (to maybe String.from_utf16_advancing_pointer). Or change the signature in some way. But I don't think it makes much sense to have both a version of this method (with pointer) that returns the advanced pointer and one that doesn't. It's easy to just ignore the returned pointer value if you don't need it.
|
This is why I recommended keeping the UTF-16 implementation internal until we got it working well in Windows, otherwise we are bound to make breaking changes. I'd say for now don't worry about breaking changes here, I doubt anyone is using these methods, and if they are, well, it's a simple fix. |
|
I'm fine with this breaking change actually |
Remove LPCWSTR
| def check_no_null_byte(name = nil) | ||
| if byte_index(0) | ||
| name = "`#{name}` " | ||
| raise ArgumentError.new("String #{name}contains null byte") |
There was a problem hiding this comment.
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").
There was a problem hiding this comment.
I agree with the first comment, disagree with the second one.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Refactor String#check_no_null_byte
ec4b780 to
5de7a7a
Compare
| 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 |
There was a problem hiding this comment.
To show that this refers to a name. This formatting is used in many other compiler and stdlib error messages.
There was a problem hiding this comment.
Weird, I never noticed that.
There was a problem hiding this comment.
IMHO Key contains null byte reads way better than String 'key' contains null byte.
There was a problem hiding this comment.
@asterite Do you mean name should be required?
There was a problem hiding this comment.
Perhaps check_no_null_byte(what = "String") then just #{what} contains null byte?
Or just go with making it fully configurable.
There was a problem hiding this comment.
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
endThen it can be used like this:
def some_method(arg)
String.check_no_null_byte(arg)
# do something
endThat way there's no string interpolation, and there's no need to repeat the argument name.
There was a problem hiding this comment.
This is bikeshedding and overengineering. The diff is fine as-is.
There was a problem hiding this comment.
@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.
5de7a7a to
2d01d82
Compare
Use String#check_no_null_byte
2d01d82 to
7adb2cc
Compare
Add spec for empty environment variable. This fails on win32 because `GetEnvironmentVariableW` doesnt differentiate between unset variable and empty string. In contrast, `GetEnvironmentStringsW` contains empty variables.
|
I discovered an issue on win32: This could be used to identify empty values if Unless someone has a idea how to prevent this, I don't think there is anything to be done. |
|
@straight-shoota i'm pretty skeptical that |
|
This stackoverflow answer seems to show the Win32 API correctly returning the value of an empty env var: https://stackoverflow.com/questions/26993642/call-to-environment-getenvironmentvariable-affects-subsequent-calls But it does show there's definitely weirdness around this part of the API - perhaps |
Fix implementation for empty string on win32 using `SetLastError`.
|
It actually seems to be the case that
I fixed it by adding a call to |
|
Oh actually, can you uncomment out the file/dir specs which were commented out due to using ENV? |
|
As far as I can see, they're all pending for other reasons. |
|
@straight-shoota these should go from macroed out to just pending Line 585 in 8d28cbb |
66166cb to
0b33619
Compare
spec/std/file_spec.cr
Outdated
| end | ||
|
|
||
| # TODO: these specs don't compile on windows because ENV isn't ported | ||
| # TODO: remove /\A\/\// hack after this is removed from macros |
There was a problem hiding this comment.
please check the diff for #5623 and do this TODO too please
There was a problem hiding this comment.
Sorry, I'm not sure what you mean... reinstating %r{\A//}?
There was a problem hiding this comment.
yes, and remove the TODO comment
|
ping, needs a second review |
|
@RX14 The commit message is a mess... it should've been squashed without all these fixups. |
Requires #5623 (for win32).