winreg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/ext/winreg/extconf.rb +25 -0
- data/ext/winreg/winreg.c +909 -0
- data/lib/winreg/version.rb +5 -0
- data/lib/winreg.rb +795 -0
- metadata +124 -0
data/lib/winreg.rb
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "winreg/version"
|
|
4
|
+
require "winreg/winreg" # native extension: Key/Watch classes + errors + HKEY_* roots
|
|
5
|
+
|
|
6
|
+
# winreg — typed Windows registry access for Ruby: exact wire formats,
|
|
7
|
+
# least-privilege opens, WOW64 views as a first-class option, and change
|
|
8
|
+
# notification that cooperates with a fiber scheduler.
|
|
9
|
+
#
|
|
10
|
+
# Winreg.open('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion') do |k|
|
|
11
|
+
# k.string("ProductName") # => "Windows 10 ..."
|
|
12
|
+
# k.read("CurrentMajorVersionNumber") # => [:dword, 10]
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Winreg.create('HKCU\Software\Vendor\App') do |k|
|
|
16
|
+
# k.write_multi_string("Plugins", %w[alpha beta]) # correct double-NUL wire format
|
|
17
|
+
# k.watch(filter: :values) { |event| break if event == :deleted }
|
|
18
|
+
# end
|
|
19
|
+
module Winreg
|
|
20
|
+
# Registry type tags (verified, winnt.h) — Symbol <-> tag map.
|
|
21
|
+
TYPES = {
|
|
22
|
+
none: 0, sz: 1, expand_sz: 2, binary: 3, dword: 4,
|
|
23
|
+
dword_be: 5, link: 6, multi_sz: 7, qword: 11
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
TYPE_NAMES = TYPES.invert.freeze
|
|
27
|
+
private_constant :TYPE_NAMES
|
|
28
|
+
|
|
29
|
+
# ---- Win32 flag values (verified, winnt.h) ------------------------------
|
|
30
|
+
KEY_QUERY_VALUE = 0x0001
|
|
31
|
+
KEY_NOTIFY = 0x0010
|
|
32
|
+
KEY_READ = 0x20019 # STANDARD_RIGHTS_READ | QUERY_VALUE | ENUMERATE_SUB_KEYS | NOTIFY
|
|
33
|
+
KEY_WRITE = 0x20006 # STANDARD_RIGHTS_WRITE | SET_VALUE | CREATE_SUB_KEY
|
|
34
|
+
KEY_WOW64_64KEY = 0x0100
|
|
35
|
+
KEY_WOW64_32KEY = 0x0200
|
|
36
|
+
REG_NOTIFY_CHANGE_NAME = 0x1
|
|
37
|
+
REG_NOTIFY_CHANGE_ATTRIBUTES = 0x2
|
|
38
|
+
REG_NOTIFY_CHANGE_LAST_SET = 0x4
|
|
39
|
+
REG_NOTIFY_CHANGE_SECURITY = 0x8
|
|
40
|
+
|
|
41
|
+
# Root tokens (case-insensitive, short and long forms) -> predefined handle.
|
|
42
|
+
ROOTS = {
|
|
43
|
+
"HKCR" => HKEY_CLASSES_ROOT, "HKEY_CLASSES_ROOT" => HKEY_CLASSES_ROOT,
|
|
44
|
+
"HKCU" => HKEY_CURRENT_USER, "HKEY_CURRENT_USER" => HKEY_CURRENT_USER,
|
|
45
|
+
"HKLM" => HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE" => HKEY_LOCAL_MACHINE,
|
|
46
|
+
"HKU" => HKEY_USERS, "HKEY_USERS" => HKEY_USERS,
|
|
47
|
+
"HKCC" => HKEY_CURRENT_CONFIG, "HKEY_CURRENT_CONFIG" => HKEY_CURRENT_CONFIG
|
|
48
|
+
}.freeze
|
|
49
|
+
private_constant :ROOTS
|
|
50
|
+
|
|
51
|
+
ROOT_NAMES = {
|
|
52
|
+
HKEY_CLASSES_ROOT => "HKEY_CLASSES_ROOT",
|
|
53
|
+
HKEY_CURRENT_USER => "HKEY_CURRENT_USER",
|
|
54
|
+
HKEY_LOCAL_MACHINE => "HKEY_LOCAL_MACHINE",
|
|
55
|
+
HKEY_USERS => "HKEY_USERS",
|
|
56
|
+
HKEY_CURRENT_CONFIG => "HKEY_CURRENT_CONFIG"
|
|
57
|
+
}.freeze
|
|
58
|
+
private_constant :ROOT_NAMES
|
|
59
|
+
|
|
60
|
+
# FILETIME epoch (1601-01-01) offset from the Unix epoch, in 100ns ticks.
|
|
61
|
+
FILETIME_EPOCH_DELTA = 116_444_736_000_000_000
|
|
62
|
+
private_constant :FILETIME_EPOCH_DELTA
|
|
63
|
+
|
|
64
|
+
# A Windows API failure carries the originating error code (the LSTATUS /
|
|
65
|
+
# Win32 code), set on the exception in C.
|
|
66
|
+
class OSError
|
|
67
|
+
def code
|
|
68
|
+
@code
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module_function
|
|
73
|
+
|
|
74
|
+
# Run a blocking native call cooperatively. Under a Fiber scheduler (e.g.
|
|
75
|
+
# winloop) the call is offloaded to a worker Thread so the calling fiber
|
|
76
|
+
# parks (Thread#value routes through the scheduler) and the event loop keeps
|
|
77
|
+
# serving other fibers; with no scheduler it runs inline (the C call already
|
|
78
|
+
# releases the GVL). On fiber unwind the worker is killed+joined so it can't
|
|
79
|
+
# leak. Registry watch registrations are REG_NOTIFY_THREAD_AGNOSTIC, so the
|
|
80
|
+
# ephemeral workers are sound (a registration does not die with its thread).
|
|
81
|
+
#
|
|
82
|
+
# Caveat: a fiber unwound (e.g. Timeout) after the worker observed :changed
|
|
83
|
+
# but before value delivery loses that one delivery — harmless here, because
|
|
84
|
+
# :changed is stateless ("go look") and the registration stays armed.
|
|
85
|
+
def run_blocking
|
|
86
|
+
sched = Fiber.scheduler
|
|
87
|
+
return yield unless sched
|
|
88
|
+
|
|
89
|
+
worker = Thread.new do
|
|
90
|
+
Thread.current.report_on_exception = false
|
|
91
|
+
yield
|
|
92
|
+
end
|
|
93
|
+
begin
|
|
94
|
+
worker.value
|
|
95
|
+
ensure
|
|
96
|
+
if worker.alive?
|
|
97
|
+
worker.kill
|
|
98
|
+
worker.join
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Seconds (nil = infinite) -> milliseconds for the C layer (-1 = INFINITE).
|
|
104
|
+
# Never collapse a tiny-but-positive wait into a non-blocking poll.
|
|
105
|
+
def ms_for(timeout)
|
|
106
|
+
return -1 if timeout.nil?
|
|
107
|
+
|
|
108
|
+
t = Float(timeout)
|
|
109
|
+
raise ArgumentError, "timeout must be non-negative, got #{timeout.inspect}" if t.negative?
|
|
110
|
+
|
|
111
|
+
ms = (t * 1000).round
|
|
112
|
+
ms.zero? && t.positive? ? 1 : ms
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Open an existing key. access: :read (default) | :read_write | Integer raw
|
|
116
|
+
# samDesired (must not contain the WOW64 view bits — the view comes from
|
|
117
|
+
# view:). view: :default | :v64 | :v32. Yields the Key (ensure-closed) if a
|
|
118
|
+
# block is given, returning the block's value.
|
|
119
|
+
def open(path, access: :read, view: :default)
|
|
120
|
+
root, sub = parse_path(path)
|
|
121
|
+
sam = access_mask(access) | view_mask(view)
|
|
122
|
+
k = Key.send(:_open, root, sub, sam)
|
|
123
|
+
k.send(:_init_meta, root, sub, view)
|
|
124
|
+
return k unless block_given?
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
yield k
|
|
128
|
+
ensure
|
|
129
|
+
k.close
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Open-or-create a key (creates all missing intermediate keys). Default
|
|
134
|
+
# access is :read_write — you create keys to write to them. The security
|
|
135
|
+
# descriptor is inherited from the parent (NULL SECURITY_ATTRIBUTES).
|
|
136
|
+
def create(path, access: :read_write, view: :default)
|
|
137
|
+
root, sub = parse_path(path)
|
|
138
|
+
sam = access_mask(access) | view_mask(view)
|
|
139
|
+
k = sub.nil? ? Key.send(:_open, root, nil, sam) : Key.send(:_create, root, sub, sam)
|
|
140
|
+
k.send(:_init_meta, root, sub, view)
|
|
141
|
+
return k unless block_given?
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
yield k
|
|
145
|
+
ensure
|
|
146
|
+
k.close
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Expand %VAR% references using ExpandEnvironmentStringsW (NOT a Ruby gsub
|
|
151
|
+
# over ENV — semantics differ: unknown vars stay literal, exactly as the OS
|
|
152
|
+
# defines). Input/output UTF-8 String.
|
|
153
|
+
def expand_string(str)
|
|
154
|
+
_expand(str)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private_class_method :_expand
|
|
158
|
+
|
|
159
|
+
# "<ROOT>\sub\key" -> [root_handle_value, subpath_or_nil]. Backslash is the
|
|
160
|
+
# ONLY separator; forward slash is a legal key-name character and is never
|
|
161
|
+
# translated. One trailing backslash is stripped.
|
|
162
|
+
def parse_path(path)
|
|
163
|
+
s = String(path)
|
|
164
|
+
if s.start_with?("\\\\")
|
|
165
|
+
raise ArgumentError, "winreg: remote registry is not supported"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
s = s[0..-2] if s.end_with?("\\")
|
|
169
|
+
tok, rest = s.split("\\", 2)
|
|
170
|
+
root = ROOTS[tok.to_s.upcase]
|
|
171
|
+
unless root
|
|
172
|
+
raise ArgumentError,
|
|
173
|
+
"winreg: unknown registry root #{tok.inspect} (accepted: HKCR, HKCU, HKLM, HKU, " \
|
|
174
|
+
"HKCC or their long HKEY_* forms)"
|
|
175
|
+
end
|
|
176
|
+
rest = nil if rest && rest.empty?
|
|
177
|
+
[root, rest]
|
|
178
|
+
end
|
|
179
|
+
private_class_method :parse_path
|
|
180
|
+
|
|
181
|
+
def access_mask(access)
|
|
182
|
+
case access
|
|
183
|
+
when :read then KEY_READ
|
|
184
|
+
when :read_write then KEY_READ | KEY_WRITE
|
|
185
|
+
when Integer
|
|
186
|
+
if (access & (KEY_WOW64_64KEY | KEY_WOW64_32KEY)) != 0
|
|
187
|
+
raise ArgumentError,
|
|
188
|
+
"winreg: raw access mask must not contain the WOW64 view bits (0x300); " \
|
|
189
|
+
"pass view: :v64 / :v32 instead"
|
|
190
|
+
end
|
|
191
|
+
access
|
|
192
|
+
else
|
|
193
|
+
raise ArgumentError, "winreg: access must be :read, :read_write or an Integer mask, " \
|
|
194
|
+
"got #{access.inspect}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
private_class_method :access_mask
|
|
198
|
+
|
|
199
|
+
def view_mask(view)
|
|
200
|
+
case view
|
|
201
|
+
when :default then 0
|
|
202
|
+
when :v64 then KEY_WOW64_64KEY
|
|
203
|
+
when :v32 then KEY_WOW64_32KEY
|
|
204
|
+
else raise ArgumentError, "winreg: view must be :default, :v64 or :v32, got #{view.inspect}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
private_class_method :view_mask
|
|
208
|
+
|
|
209
|
+
def filter_mask(filter)
|
|
210
|
+
syms = filter.is_a?(Array) ? filter : [filter]
|
|
211
|
+
raise ArgumentError, "winreg: watch filter must not be empty" if syms.empty?
|
|
212
|
+
|
|
213
|
+
syms.reduce(0) do |mask, sym|
|
|
214
|
+
mask | case sym
|
|
215
|
+
when :values then REG_NOTIFY_CHANGE_LAST_SET
|
|
216
|
+
when :keys then REG_NOTIFY_CHANGE_NAME
|
|
217
|
+
when :attributes then REG_NOTIFY_CHANGE_ATTRIBUTES
|
|
218
|
+
when :security then REG_NOTIFY_CHANGE_SECURITY
|
|
219
|
+
when :default then REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
|
|
220
|
+
when :all
|
|
221
|
+
REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES |
|
|
222
|
+
REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_CHANGE_SECURITY
|
|
223
|
+
else
|
|
224
|
+
raise ArgumentError, "winreg: unknown watch filter #{sym.inspect} (accepted: " \
|
|
225
|
+
":values, :keys, :attributes, :security, :default, :all)"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
private_class_method :filter_mask
|
|
230
|
+
|
|
231
|
+
# An open registry key. Instances come only from Winreg.open/.create and
|
|
232
|
+
# Key#open/#create (no public .new). All methods raise Winreg::Closed on a
|
|
233
|
+
# closed key except close/closed?/path/view/created?/inspect.
|
|
234
|
+
class Key
|
|
235
|
+
# The C constructors are plumbing; the validated keyword API above is the
|
|
236
|
+
# only way in (phylax's private-bridge discipline).
|
|
237
|
+
private_class_method :_open, :_create
|
|
238
|
+
|
|
239
|
+
# RegQueryInfoKeyW snapshot. last_write_time is a Time with full 100ns
|
|
240
|
+
# FILETIME precision (Rational arithmetic).
|
|
241
|
+
Info = Struct.new(:subkey_count, :value_count, :max_subkey_name_len,
|
|
242
|
+
:max_value_name_len, :max_value_data_bytes, :last_write_time)
|
|
243
|
+
|
|
244
|
+
# Canonical full path, long-form root ("HKEY_CURRENT_USER\\Software\\X").
|
|
245
|
+
attr_reader :path
|
|
246
|
+
|
|
247
|
+
# :default | :v64 | :v32
|
|
248
|
+
attr_reader :view
|
|
249
|
+
|
|
250
|
+
def inspect
|
|
251
|
+
closed? ? "#<Winreg::Key (closed)>" : "#<Winreg::Key #{@path} view=#{@view.inspect}>"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ---- reading ----------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
# Generic typed read -> [type, value]. type is a Symbol from Winreg::TYPES
|
|
257
|
+
# (or the raw Integer tag for types outside the table). Strings are
|
|
258
|
+
# returned UNEXPANDED with at most one trailing NUL stripped; :multi_sz as
|
|
259
|
+
# Array<String>; integers range-decoded; :binary/:none/:link/unknown as
|
|
260
|
+
# raw BINARY bytes.
|
|
261
|
+
def read(name)
|
|
262
|
+
t, bytes = _read_raw(norm_name(name))
|
|
263
|
+
[Winreg.send(:type_symbol, t), Winreg.send(:decode_value, t, bytes)]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Like #read, but nil when the value does not exist.
|
|
267
|
+
def read?(name)
|
|
268
|
+
read(name)
|
|
269
|
+
rescue Winreg::NotFound
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# REG_SZ or REG_EXPAND_SZ -> String (UTF-8), unexpanded unless expand:
|
|
274
|
+
# true (ExpandEnvironmentStringsW on the result, whatever the type).
|
|
275
|
+
def string(name, expand: false)
|
|
276
|
+
bytes = typed_bytes(name, [TYPES[:sz], TYPES[:expand_sz]], "REG_SZ/REG_EXPAND_SZ")
|
|
277
|
+
s = Winreg.send(:decode_string, bytes)
|
|
278
|
+
expand ? Winreg.expand_string(s) : s
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# nil for a MISSING value (TypeMismatch still raises — a wrong type is a
|
|
282
|
+
# bug, not an absence). Same pattern for the other ? readers.
|
|
283
|
+
def string?(name, expand: false)
|
|
284
|
+
string(name, expand: expand)
|
|
285
|
+
rescue Winreg::NotFound
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def dword(name)
|
|
290
|
+
Winreg.send(:decode_int, typed_bytes(name, [TYPES[:dword]], "REG_DWORD"), 4, false, :dword)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def dword?(name)
|
|
294
|
+
dword(name)
|
|
295
|
+
rescue Winreg::NotFound
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def qword(name)
|
|
300
|
+
Winreg.send(:decode_int, typed_bytes(name, [TYPES[:qword]], "REG_QWORD"), 8, false, :qword)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def qword?(name)
|
|
304
|
+
qword(name)
|
|
305
|
+
rescue Winreg::NotFound
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def multi_string(name)
|
|
310
|
+
Winreg.send(:decode_multi, typed_bytes(name, [TYPES[:multi_sz]], "REG_MULTI_SZ"))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def multi_string?(name)
|
|
314
|
+
multi_string(name)
|
|
315
|
+
rescue Winreg::NotFound
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def binary(name)
|
|
320
|
+
typed_bytes(name, [TYPES[:binary]], "REG_BINARY")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def binary?(name)
|
|
324
|
+
binary(name)
|
|
325
|
+
rescue Winreg::NotFound
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Raw escape hatch: NEVER decodes, never raises MalformedValue.
|
|
330
|
+
# -> [Integer type_tag, String bytes (Encoding::BINARY)]
|
|
331
|
+
def raw(name)
|
|
332
|
+
_read_raw(norm_name(name))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# ---- writing ----------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
# All writers return nil and require access: :read_write (else the OS
|
|
338
|
+
# raises AccessDenied). The gem owns serialization, so the wire format is
|
|
339
|
+
# correct by construction (double-NUL REG_MULTI_SZ, cb incl. terminators).
|
|
340
|
+
|
|
341
|
+
def write_string(name, str)
|
|
342
|
+
_write_raw(norm_name(name), TYPES[:sz], Winreg.send(:encode_sz, str))
|
|
343
|
+
nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def write_expand_string(name, str)
|
|
347
|
+
_write_raw(norm_name(name), TYPES[:expand_sz], Winreg.send(:encode_sz, str))
|
|
348
|
+
nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def write_multi_string(name, ary)
|
|
352
|
+
_write_raw(norm_name(name), TYPES[:multi_sz], Winreg.send(:encode_multi, ary))
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def write_dword(name, int)
|
|
357
|
+
v = Winreg.send(:int_in_range, int, 0xFFFF_FFFF, "REG_DWORD")
|
|
358
|
+
_write_raw(norm_name(name), TYPES[:dword], [v].pack("V"))
|
|
359
|
+
nil
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def write_qword(name, int)
|
|
363
|
+
v = Winreg.send(:int_in_range, int, 0xFFFF_FFFF_FFFF_FFFF, "REG_QWORD")
|
|
364
|
+
_write_raw(norm_name(name), TYPES[:qword], [v].pack("Q<"))
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def write_binary(name, bytes)
|
|
369
|
+
Winreg.send(:string_arg, bytes, "binary data")
|
|
370
|
+
_write_raw(norm_name(name), TYPES[:binary], bytes)
|
|
371
|
+
nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Generic typed write — symmetric with #read; validates identically to the
|
|
375
|
+
# typed writers (it dispatches to them). :dword_be packs 4 bytes BE; :none
|
|
376
|
+
# expects bytes; :link raises (links are read-surface only in v1); a raw
|
|
377
|
+
# Integer tag goes through #write_raw.
|
|
378
|
+
def write(name, type, value)
|
|
379
|
+
case type
|
|
380
|
+
when :sz then write_string(name, value)
|
|
381
|
+
when :expand_sz then write_expand_string(name, value)
|
|
382
|
+
when :multi_sz then write_multi_string(name, value)
|
|
383
|
+
when :dword then write_dword(name, value)
|
|
384
|
+
when :qword then write_qword(name, value)
|
|
385
|
+
when :dword_be
|
|
386
|
+
v = Winreg.send(:int_in_range, value, 0xFFFF_FFFF, "REG_DWORD_BIG_ENDIAN")
|
|
387
|
+
_write_raw(norm_name(name), TYPES[:dword_be], [v].pack("N"))
|
|
388
|
+
when :binary then write_binary(name, value)
|
|
389
|
+
when :none
|
|
390
|
+
Winreg.send(:string_arg, value, ":none data")
|
|
391
|
+
_write_raw(norm_name(name), TYPES[:none], value)
|
|
392
|
+
when :link
|
|
393
|
+
raise ArgumentError, "winreg: :link values are read-surface only (v1); " \
|
|
394
|
+
"registry symlink creation is out of scope"
|
|
395
|
+
when Integer then write_raw(name, type, value)
|
|
396
|
+
else
|
|
397
|
+
raise ArgumentError, "winreg: unknown registry type #{type.inspect}"
|
|
398
|
+
end
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Raw escape hatch, symmetric with #raw: exact bytes under an arbitrary
|
|
403
|
+
# type tag. No validation beyond bytes being a String + the 4 GiB guard.
|
|
404
|
+
def write_raw(name, type_tag, bytes)
|
|
405
|
+
raise TypeError, "winreg: type tag must be an Integer, got #{type_tag.inspect}" unless type_tag.is_a?(Integer)
|
|
406
|
+
|
|
407
|
+
Winreg.send(:string_arg, bytes, "raw data")
|
|
408
|
+
_write_raw(norm_name(name), type_tag, bytes)
|
|
409
|
+
nil
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Delete a value (nil/"" addresses the default value). NotFound if absent.
|
|
413
|
+
def delete_value(name)
|
|
414
|
+
_delete_value(norm_name(name))
|
|
415
|
+
nil
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# ---- existence probes --------------------------------------------------
|
|
419
|
+
|
|
420
|
+
# RegQueryValueExW size probe; error 2 -> false.
|
|
421
|
+
def value?(name)
|
|
422
|
+
_value_p(norm_name(name))
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# NotFound -> false; ACCESS_DENIED -> true (the key exists, you may not
|
|
426
|
+
# open it); anything else raises. View-consistent (probe carries the view).
|
|
427
|
+
def key?(name)
|
|
428
|
+
probe = _open_child(subpath_arg(name), Winreg::KEY_QUERY_VALUE)
|
|
429
|
+
probe.close
|
|
430
|
+
true
|
|
431
|
+
rescue Winreg::NotFound
|
|
432
|
+
false
|
|
433
|
+
rescue Winreg::AccessDenied
|
|
434
|
+
true
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# ---- subkeys -----------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
# Child open. Relative path, backslashes allowed ("A\\B"). The parent's
|
|
440
|
+
# WOW64 view is inherited automatically and CANNOT be overridden (the API
|
|
441
|
+
# encoding of the view-consistency rule). Block form ensure-closes.
|
|
442
|
+
def open(subpath, access: :read)
|
|
443
|
+
sam = Winreg.send(:access_mask, access)
|
|
444
|
+
k = _open_child(subpath_arg(subpath), sam)
|
|
445
|
+
k.send(:_init_meta, @root, child_subpath(subpath), @view)
|
|
446
|
+
return k unless block_given?
|
|
447
|
+
|
|
448
|
+
begin
|
|
449
|
+
yield k
|
|
450
|
+
ensure
|
|
451
|
+
k.close
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Child open-or-create, same view rules.
|
|
456
|
+
def create(subpath, access: :read_write)
|
|
457
|
+
sam = Winreg.send(:access_mask, access)
|
|
458
|
+
k = _create_child(subpath_arg(subpath), sam)
|
|
459
|
+
k.send(:_init_meta, @root, child_subpath(subpath), @view)
|
|
460
|
+
return k unless block_given?
|
|
461
|
+
|
|
462
|
+
begin
|
|
463
|
+
yield k
|
|
464
|
+
ensure
|
|
465
|
+
k.close
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Delete the named child subkey (handle-relative; the view is applied via
|
|
470
|
+
# RegDeleteKeyExW samDesired). Non-recursive delete of a key that still
|
|
471
|
+
# has subkeys fails with an OSError; recursive: true performs a deepest-
|
|
472
|
+
# first walk in Ruby over the raise-safe primitives (interruptible; NOT
|
|
473
|
+
# atomic — same as RegDeleteTreeW), with the view applied to every open.
|
|
474
|
+
def delete_key(name, recursive: false)
|
|
475
|
+
n = subpath_arg(name)
|
|
476
|
+
if recursive
|
|
477
|
+
_delete_tree(n)
|
|
478
|
+
else
|
|
479
|
+
begin
|
|
480
|
+
_delete_key(n)
|
|
481
|
+
rescue Winreg::AccessDenied => e
|
|
482
|
+
err = Winreg::AccessDenied.new("#{e.message} (key may have subkeys; use recursive: true)")
|
|
483
|
+
err.instance_variable_set(:@code, e.code)
|
|
484
|
+
raise err
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
nil
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# ---- enumeration / metadata ---------------------------------------------
|
|
491
|
+
|
|
492
|
+
# Yields (name, type, value) decoded exactly as #read. Live, snapshot-less,
|
|
493
|
+
# kernel order arbitrary; concurrent mutation may skip or repeat entries.
|
|
494
|
+
# A value whose DATA is malformed raises MalformedValue mid-iteration —
|
|
495
|
+
# use #value_names + #raw for forensic robustness.
|
|
496
|
+
def each_value
|
|
497
|
+
return enum_for(:each_value) unless block_given?
|
|
498
|
+
|
|
499
|
+
i = 0
|
|
500
|
+
while (entry = _enum_value(i))
|
|
501
|
+
name, t, bytes = entry
|
|
502
|
+
yield name, Winreg.send(:type_symbol, t), Winreg.send(:decode_value, t, bytes)
|
|
503
|
+
i += 1
|
|
504
|
+
end
|
|
505
|
+
self
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Yields each subkey name.
|
|
509
|
+
def each_key
|
|
510
|
+
return enum_for(:each_key) unless block_given?
|
|
511
|
+
|
|
512
|
+
i = 0
|
|
513
|
+
while (name = _enum_key(i))
|
|
514
|
+
yield name
|
|
515
|
+
i += 1
|
|
516
|
+
end
|
|
517
|
+
self
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Value names only — never decodes data, so hostile values can't abort it.
|
|
521
|
+
def value_names
|
|
522
|
+
out = []
|
|
523
|
+
i = 0
|
|
524
|
+
while (entry = _enum_value(i))
|
|
525
|
+
out << entry[0]
|
|
526
|
+
i += 1
|
|
527
|
+
end
|
|
528
|
+
out
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def key_names
|
|
532
|
+
out = []
|
|
533
|
+
each_key { |n| out << n }
|
|
534
|
+
out
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def info
|
|
538
|
+
a = _query_info
|
|
539
|
+
t = Time.at((a[5] - Winreg.send(:filetime_epoch_delta)) / 10_000_000r)
|
|
540
|
+
Info.new(a[0], a[2], a[1], a[3], a[4], t).freeze
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# ---- watching ------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
# Without a block: an armed Winreg::Watch (changes between construction
|
|
546
|
+
# and the first #wait are caught). With a block: loops, yielding :changed
|
|
547
|
+
# per coalesced change; yields :deleted once and returns if the watched
|
|
548
|
+
# key is deleted; the Watch is ensure-closed. The Watch opens its OWN
|
|
549
|
+
# private KEY_NOTIFY|view handle from this key's path — closing this Key
|
|
550
|
+
# afterwards does not disturb it.
|
|
551
|
+
def watch(subtree: false, filter: :default)
|
|
552
|
+
raise Winreg::Closed, "winreg: key is closed" if closed?
|
|
553
|
+
|
|
554
|
+
mask = Winreg.send(:filter_mask, filter)
|
|
555
|
+
vsam = Winreg.send(:view_mask, @view || :default)
|
|
556
|
+
w = Watch.send(:_new, @root, @subpath, vsam, subtree ? true : false, mask)
|
|
557
|
+
return w unless block_given?
|
|
558
|
+
|
|
559
|
+
begin
|
|
560
|
+
loop do
|
|
561
|
+
event = w.wait
|
|
562
|
+
yield event
|
|
563
|
+
break if event == :deleted
|
|
564
|
+
end
|
|
565
|
+
nil
|
|
566
|
+
ensure
|
|
567
|
+
w.close
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
private
|
|
572
|
+
|
|
573
|
+
# Set by the Ruby layer right after _open/_create — ivars, not native-
|
|
574
|
+
# struct VALUEs, so the rb_data_type_t needs no dmark.
|
|
575
|
+
def _init_meta(root, subpath, view)
|
|
576
|
+
@root = root
|
|
577
|
+
@subpath = subpath
|
|
578
|
+
@view = view
|
|
579
|
+
root_name = Winreg.send(:root_name, root)
|
|
580
|
+
@path = subpath ? "#{root_name}\\#{subpath}" : root_name
|
|
581
|
+
self
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Value names: String, or nil/"" for the key's default (unnamed) value
|
|
585
|
+
# ("" normalized to nil; C passes NULL).
|
|
586
|
+
def norm_name(name)
|
|
587
|
+
return nil if name.nil?
|
|
588
|
+
raise TypeError, "winreg: value name must be a String or nil, got #{name.inspect}" unless name.is_a?(String)
|
|
589
|
+
|
|
590
|
+
name.empty? ? nil : name
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Subkey names/paths must be real Strings (nil has no meaning here).
|
|
594
|
+
def subpath_arg(name)
|
|
595
|
+
raise TypeError, "winreg: subkey name must be a String, got #{name.inspect}" unless name.is_a?(String)
|
|
596
|
+
raise ArgumentError, "winreg: subkey name must not be empty" if name.empty?
|
|
597
|
+
|
|
598
|
+
name
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def child_subpath(subpath)
|
|
602
|
+
@subpath ? "#{@subpath}\\#{subpath}" : subpath.dup
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Strict typed read: bytes if the stored tag is in +allowed+, else
|
|
606
|
+
# TypeMismatch naming expected vs actual.
|
|
607
|
+
def typed_bytes(name, allowed, expected_desc)
|
|
608
|
+
t, bytes = _read_raw(norm_name(name))
|
|
609
|
+
unless allowed.include?(t)
|
|
610
|
+
actual = Winreg.send(:type_symbol, t)
|
|
611
|
+
expected = allowed.map { |tag| Winreg.send(:type_symbol, tag).inspect }.join(" or ")
|
|
612
|
+
raise Winreg::TypeMismatch,
|
|
613
|
+
"winreg: value #{name.inspect} has type #{actual.inspect}, expected #{expected} " \
|
|
614
|
+
"(#{expected_desc})"
|
|
615
|
+
end
|
|
616
|
+
bytes
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Deepest-first recursive delete: always enumerate index 0 (children shift
|
|
620
|
+
# down as they are deleted); every child open is ensure-closed; interrupts
|
|
621
|
+
# are delivered between primitive calls (no C frame holds a child HKEY
|
|
622
|
+
# across a potential raise).
|
|
623
|
+
def _delete_tree(name)
|
|
624
|
+
child = _open_child(name, Winreg::KEY_READ)
|
|
625
|
+
begin
|
|
626
|
+
while (sub = child.send(:_enum_key, 0))
|
|
627
|
+
child.send(:_delete_tree, sub)
|
|
628
|
+
end
|
|
629
|
+
ensure
|
|
630
|
+
child.close
|
|
631
|
+
end
|
|
632
|
+
_delete_key(name)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# A change-notification registration (constructed only via Key#watch).
|
|
637
|
+
# Armed at construction and re-armed before every delivery, so no final
|
|
638
|
+
# state is ever missed; deliveries coalesce (:changed means ">= 1 matching
|
|
639
|
+
# change since the previous delivery") and carry no payload.
|
|
640
|
+
class Watch
|
|
641
|
+
private_class_method :_new
|
|
642
|
+
|
|
643
|
+
# -> :changed (rearmed; ready to wait again)
|
|
644
|
+
# :deleted (watched key was deleted; the watch auto-closes)
|
|
645
|
+
# nil (timeout elapsed; registration still armed)
|
|
646
|
+
# timeout in seconds (nil = infinite). Cooperates with a fiber scheduler
|
|
647
|
+
# via Winreg.run_blocking; standalone it releases the GVL and is
|
|
648
|
+
# interruptible. Raises Closed if closed (incl. closed by another thread
|
|
649
|
+
# mid-wait); Winreg::Error if another wait is already in flight
|
|
650
|
+
# (single-waiter); OSError on a rearm failure other than ERROR_KEY_DELETED.
|
|
651
|
+
def wait(timeout: nil)
|
|
652
|
+
ms = Winreg.ms_for(timeout)
|
|
653
|
+
Winreg.run_blocking { _wait(ms) }
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# ---- internal codecs (validation + exact wire formats) -------------------
|
|
658
|
+
# These are module-private: the public surface is Key's typed methods.
|
|
659
|
+
|
|
660
|
+
def type_symbol(tag)
|
|
661
|
+
TYPE_NAMES[tag] || tag
|
|
662
|
+
end
|
|
663
|
+
private_class_method :type_symbol
|
|
664
|
+
|
|
665
|
+
def root_name(root)
|
|
666
|
+
ROOT_NAMES[root]
|
|
667
|
+
end
|
|
668
|
+
private_class_method :root_name
|
|
669
|
+
|
|
670
|
+
def filetime_epoch_delta
|
|
671
|
+
FILETIME_EPOCH_DELTA
|
|
672
|
+
end
|
|
673
|
+
private_class_method :filetime_epoch_delta
|
|
674
|
+
|
|
675
|
+
def decode_value(tag, bytes)
|
|
676
|
+
case tag
|
|
677
|
+
when TYPES[:sz], TYPES[:expand_sz] then decode_string(bytes)
|
|
678
|
+
when TYPES[:multi_sz] then decode_multi(bytes)
|
|
679
|
+
when TYPES[:dword] then decode_int(bytes, 4, false, :dword)
|
|
680
|
+
when TYPES[:dword_be] then decode_int(bytes, 4, true, :dword_be)
|
|
681
|
+
when TYPES[:qword] then decode_int(bytes, 8, false, :qword)
|
|
682
|
+
else bytes # :binary, :none, :link, unknown tags: raw BINARY bytes
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
private_class_method :decode_value
|
|
686
|
+
|
|
687
|
+
# E1: returned bytes are ground truth; strip AT MOST one trailing UTF-16 NUL
|
|
688
|
+
# code unit (never an unconditional chop — unterminated values round-trip).
|
|
689
|
+
# E2/E3: odd byte counts and invalid UTF-16 raise MalformedValue.
|
|
690
|
+
def decode_string(bytes)
|
|
691
|
+
if bytes.bytesize.odd?
|
|
692
|
+
raise MalformedValue,
|
|
693
|
+
"winreg: string value has an odd byte count (#{bytes.bytesize})"
|
|
694
|
+
end
|
|
695
|
+
b = bytes
|
|
696
|
+
b = b.byteslice(0, b.bytesize - 2) if b.bytesize >= 2 && b.getbyte(-1).zero? && b.getbyte(-2).zero?
|
|
697
|
+
utf16le_to_utf8(b)
|
|
698
|
+
end
|
|
699
|
+
private_class_method :decode_string
|
|
700
|
+
|
|
701
|
+
# E6: tolerate missing final terminator, 0-byte data, and excess
|
|
702
|
+
# terminators — decode the whole buffer, split on NUL, stop at the first
|
|
703
|
+
# empty element (an empty string terminates the list by definition).
|
|
704
|
+
def decode_multi(bytes)
|
|
705
|
+
if bytes.bytesize.odd?
|
|
706
|
+
raise MalformedValue,
|
|
707
|
+
"winreg: REG_MULTI_SZ value has an odd byte count (#{bytes.bytesize})"
|
|
708
|
+
end
|
|
709
|
+
return [] if bytes.empty?
|
|
710
|
+
|
|
711
|
+
out = []
|
|
712
|
+
utf16le_to_utf8(bytes).split("\0", -1).each do |part|
|
|
713
|
+
break if part.empty?
|
|
714
|
+
|
|
715
|
+
out << part
|
|
716
|
+
end
|
|
717
|
+
out
|
|
718
|
+
end
|
|
719
|
+
private_class_method :decode_multi
|
|
720
|
+
|
|
721
|
+
def decode_int(bytes, size, big_endian, type)
|
|
722
|
+
unless bytes.bytesize == size
|
|
723
|
+
raise MalformedValue,
|
|
724
|
+
"winreg: #{type.inspect} value has #{bytes.bytesize} bytes (expected #{size})"
|
|
725
|
+
end
|
|
726
|
+
if size == 4
|
|
727
|
+
bytes.unpack1(big_endian ? "N" : "V")
|
|
728
|
+
else
|
|
729
|
+
bytes.unpack1("Q<")
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
private_class_method :decode_int
|
|
733
|
+
|
|
734
|
+
def utf16le_to_utf8(bytes)
|
|
735
|
+
bytes.dup.force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8)
|
|
736
|
+
rescue EncodingError => e
|
|
737
|
+
raise MalformedValue, "winreg: string value is not valid UTF-16LE (#{e.message})"
|
|
738
|
+
end
|
|
739
|
+
private_class_method :utf16le_to_utf8
|
|
740
|
+
|
|
741
|
+
# Writers: NUL/empty-element validation BEFORE encoding; Encoding::*Error
|
|
742
|
+
# surfaces as ArgumentError (with cause).
|
|
743
|
+
def encode_sz(str)
|
|
744
|
+
string_arg(str, "string data")
|
|
745
|
+
raise ArgumentError, "winreg: string data must not contain an embedded NUL" if str.include?("\0")
|
|
746
|
+
|
|
747
|
+
utf8_to_utf16le(str) + "\0\0".b
|
|
748
|
+
end
|
|
749
|
+
private_class_method :encode_sz
|
|
750
|
+
|
|
751
|
+
def encode_multi(ary)
|
|
752
|
+
raise TypeError, "winreg: multi_string takes an Array of Strings, got #{ary.inspect}" unless ary.is_a?(Array)
|
|
753
|
+
|
|
754
|
+
ary.each do |el|
|
|
755
|
+
unless el.is_a?(String)
|
|
756
|
+
raise ArgumentError, "winreg: multi_string elements must be Strings, got #{el.inspect}"
|
|
757
|
+
end
|
|
758
|
+
if el.empty?
|
|
759
|
+
raise ArgumentError, "winreg: multi_string elements must not be empty " \
|
|
760
|
+
"(an empty element terminates the list)"
|
|
761
|
+
end
|
|
762
|
+
if el.include?("\0")
|
|
763
|
+
raise ArgumentError, "winreg: multi_string elements must not contain an embedded NUL"
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
return "\0\0".b if ary.empty? # the 2-byte empty list
|
|
767
|
+
|
|
768
|
+
ary.map { |el| utf8_to_utf16le(el) + "\0\0".b }.join + "\0\0".b
|
|
769
|
+
end
|
|
770
|
+
private_class_method :encode_multi
|
|
771
|
+
|
|
772
|
+
def utf8_to_utf16le(str)
|
|
773
|
+
str.encode(Encoding::UTF_16LE).force_encoding(Encoding::BINARY)
|
|
774
|
+
rescue EncodingError
|
|
775
|
+
raise ArgumentError, "winreg: string data cannot be encoded as UTF-16"
|
|
776
|
+
end
|
|
777
|
+
private_class_method :utf8_to_utf16le
|
|
778
|
+
|
|
779
|
+
def int_in_range(value, max, what)
|
|
780
|
+
raise TypeError, "winreg: #{what} value must be an Integer, got #{value.inspect}" unless value.is_a?(Integer)
|
|
781
|
+
unless value >= 0 && value <= max
|
|
782
|
+
raise RangeError, "winreg: #{what} must be in 0..#{max} (got #{value}); " \
|
|
783
|
+
"registry integers are unsigned"
|
|
784
|
+
end
|
|
785
|
+
value
|
|
786
|
+
end
|
|
787
|
+
private_class_method :int_in_range
|
|
788
|
+
|
|
789
|
+
def string_arg(value, what)
|
|
790
|
+
raise TypeError, "winreg: #{what} must be a String, got #{value.inspect}" unless value.is_a?(String)
|
|
791
|
+
|
|
792
|
+
value
|
|
793
|
+
end
|
|
794
|
+
private_class_method :string_arg
|
|
795
|
+
end
|