Skip to content

Commit cefe508

Browse files
Turn SystemError.from_errno into a macro (#15874)
`SystemError.from_errno` calls `Errno.value` to retrieve the system error value from the last lib call. But this happens in the body of the method which only executes after its arguments are evaluated (for example building the error message). This can lead to a change in `Errno.value` which `.from_errno` would then mistake for the original error. In order to fix this, we must query `Errno.value` immediately after the lib call. Changing `.from_errno` into a macro achieves that because it expands into a call to `Errno.value` before constructing the actual error object and its arguments. We need to change the deprecated `from_errno` method which receives an `Errno` value as well because the macro would shadow the method. One downside of the macro approach is that it won't work with file-private error types (#15496 (comment)). But this is probably not a very relevant use case.
1 parent 70431fb commit cefe508

File tree

2 files changed

+81
-43
lines changed

2 files changed

+81
-43
lines changed

spec/std/system_error_spec.cr

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,41 @@ require "spec"
33
describe SystemError do
44
describe ".from_os_error" do
55
it "Can build an error from an errno" do
6-
errno = Errno.new(2)
7-
error = ::IO::Error.from_os_error(message: nil, os_error: errno)
6+
errno = Errno::ENOENT
7+
error = ::RuntimeError.from_os_error(message: nil, os_error: errno)
88
error.message.should eq("No such file or directory")
99
end
1010
end
11+
12+
describe ".from_errno" do
13+
it "captures `Errno.value`" do
14+
Errno.value = :ENOENT
15+
error = ::RuntimeError.from_errno "foobar"
16+
error.os_error.should eq Errno::ENOENT
17+
end
18+
19+
it "avoid reset from message" do
20+
Errno.value = :ENOENT
21+
error = ::RuntimeError.from_errno("foobar".tap { Errno.value = :EPERM })
22+
error.os_error.should eq Errno::ENOENT
23+
end
24+
end
25+
26+
{% if flag?(:win32) %}
27+
describe ".from_winerror" do
28+
it "avoid reset from message" do
29+
WinError.value = :ERROR_FILE_NOT_FOUND
30+
error = ::RuntimeError.from_winerror("foobar".tap { WinError.value = :ERROR_ACCESS_DENIED })
31+
error.os_error.should eq WinError::ERROR_FILE_NOT_FOUND
32+
end
33+
end
34+
35+
describe ".from_wsa_error" do
36+
it "avoid reset from message" do
37+
WinError.wsa_value = :ERROR_FILE_NOT_FOUND
38+
error = ::RuntimeError.from_wsa_error("foobar".tap { WinError.wsa_value = :ERROR_ACCESS_DENIED })
39+
error.os_error.should eq WinError::ERROR_FILE_NOT_FOUND
40+
end
41+
end
42+
{% end %}
1143
end

src/system_error.cr

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,53 @@
4040
module SystemError
4141
macro included
4242
extend ::SystemError::ClassMethods
43+
44+
# The following are macros so we can get the `errno` or `winerror` _before_
45+
# evaluating `message` and `opts` that may change the `errno` or `winerror` value.
46+
47+
# Builds an instance of the exception from the current system error value (`Errno.value`).
48+
#
49+
# The system message corresponding to the OS error value amends the *message*.
50+
# Additional keyword arguments are forwarded to the exception initializer `.new_from_os_error`.
51+
macro from_errno(message, **opts)
52+
%errno = ::Errno.value
53+
::\{{ @type }}.from_os_error(\{{ message }}, %errno, \{{ opts.double_splat }})
54+
end
55+
56+
@[Deprecated("Use `.from_os_error` instead")]
57+
macro from_errno(message = nil, errno = nil, **opts)
58+
::\{{ @type }}.from_os_error(\{{ message }}, \{{ errno }}, \{{ opts.double_splat }})
59+
end
60+
61+
# Builds an instance of the exception from the current windows error value (`WinError.value`).
62+
#
63+
# The system message corresponding to the OS error value amends the *message*.
64+
# Additional keyword arguments are forwarded to the exception initializer `.new_from_os_error`.
65+
macro from_winerror(message, **opts)
66+
%error = ::WinError.value
67+
::\{{ @type }}.from_os_error(\{{ message }}, %error, \{{ opts.double_splat }})
68+
end
69+
70+
# Builds an instance of the exception from the current Windows Socket API error value (`WinError.wsa_value`).
71+
#
72+
# The system message corresponding to the OS error value amends the *message*.
73+
# Additional keyword arguments are forwarded to the exception initializer.
74+
macro from_wsa_error(message = nil, **opts)
75+
%error = ::WinError.wsa_value
76+
::\{{ @type }}.from_os_error(\{{ message }}, %error, \{{ opts.double_splat }})
77+
end
78+
79+
{% if flag?(:win32) %}
80+
@[Deprecated("Use `.from_os_error` instead")]
81+
macro from_winerror(message, winerror, **opts)
82+
::\{{ @type }}.from_os_error(\{{ message }}, \{{ winerror }}, \{{ opts.double_splat }})
83+
end
84+
85+
@[Deprecated("Use `.from_os_error` instead")]
86+
macro from_winerror(*, winerror = ::WinError.value, **opts)
87+
::\{{ @type }}.from_os_error(nil, \{{ winerror }}, \{{ opts.double_splat }})
88+
end
89+
{% end %}
4390
end
4491

4592
# The original system error wrapped by this exception
@@ -68,19 +115,6 @@ module SystemError
68115
end
69116
end
70117

71-
# Builds an instance of the exception from the current system error value (`Errno.value`).
72-
#
73-
# The system message corresponding to the OS error value amends the *message*.
74-
# Additional keyword arguments are forwarded to the exception initializer `.new_from_os_error`.
75-
def from_errno(message : String, **opts)
76-
from_os_error(message, Errno.value, **opts)
77-
end
78-
79-
@[Deprecated("Use `.from_os_error` instead")]
80-
def from_errno(message : String? = nil, errno : Errno? = nil, **opts)
81-
from_os_error(message, errno, **opts)
82-
end
83-
84118
# Prepares the message that goes before the system error description.
85119
#
86120
# By default it returns the original message unchanged. But that could be
@@ -106,33 +140,5 @@ module SystemError
106140
protected def new_from_os_error(message : String?, os_error, **opts)
107141
self.new(message, **opts)
108142
end
109-
110-
# Builds an instance of the exception from the current windows error value (`WinError.value`).
111-
#
112-
# The system message corresponding to the OS error value amends the *message*.
113-
# Additional keyword arguments are forwarded to the exception initializer `.new_from_os_error`.
114-
def from_winerror(message : String?, **opts)
115-
from_os_error(message, WinError.value, **opts)
116-
end
117-
118-
# Builds an instance of the exception from the current Windows Socket API error value (`WinError.wsa_value`).
119-
#
120-
# The system message corresponding to the OS error value amends the *message*.
121-
# Additional keyword arguments are forwarded to the exception initializer.
122-
def from_wsa_error(message : String? = nil, **opts)
123-
from_os_error(message, WinError.wsa_value, **opts)
124-
end
125-
126-
{% if flag?(:win32) %}
127-
@[Deprecated("Use `.from_os_error` instead")]
128-
def from_winerror(message : String?, winerror : WinError, **opts)
129-
from_os_error(message, winerror, **opts)
130-
end
131-
132-
@[Deprecated("Use `.from_os_error` instead")]
133-
def from_winerror(*, winerror : WinError = WinError.value, **opts)
134-
from_os_error(message, winerror, **opts)
135-
end
136-
{% end %}
137143
end
138144
end

0 commit comments

Comments
 (0)