wineole 0.1.0 → 0.2.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 +4 -4
- data/bin/wineole-vba +45 -0
- data/lib/wineole/client.rb +454 -20
- data/lib/wineole/dispatcher.rb +534 -0
- data/lib/wineole/errors.rb +6 -0
- data/lib/wineole/events.rb +353 -0
- data/lib/wineole/msoffice/address.rb +67 -0
- data/lib/wineole/msoffice/book.rb +114 -0
- data/lib/wineole/msoffice/color.rb +87 -0
- data/lib/wineole/msoffice/controls.rb +450 -0
- data/lib/wineole/msoffice/excel.rb +311 -0
- data/lib/wineole/msoffice/format.rb +362 -0
- data/lib/wineole/msoffice/forms.rb +163 -0
- data/lib/wineole/msoffice/passthrough.rb +46 -0
- data/lib/wineole/msoffice/paths.rb +62 -0
- data/lib/wineole/msoffice/range.rb +149 -0
- data/lib/wineole/msoffice/sheet.rb +101 -0
- data/lib/wineole/msoffice/vba.rb +199 -0
- data/lib/wineole/msoffice/vba_api.rb +366 -0
- data/lib/wineole/msoffice/vba_block.rb +144 -0
- data/lib/wineole/msoffice.rb +28 -0
- data/lib/wineole/proxy.rb +126 -15
- data/lib/wineole.rb +10 -1
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +21 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
require_relative '../errors'
|
|
3
|
+
|
|
4
|
+
module WineOLE
|
|
5
|
+
module MSOffice
|
|
6
|
+
# Programmatic access to a workbook's VBA project is off by default, and
|
|
7
|
+
# turning it on means writing a macro security setting in the registry.
|
|
8
|
+
# This module can read and write it -- but nothing in the wrapper calls
|
|
9
|
+
# the writing half. That is for `wineole-vba`, which a human runs, and
|
|
10
|
+
# for tests, which need to exercise both sides of the switch.
|
|
11
|
+
module VBA
|
|
12
|
+
class Error < WineOLE::Error; end
|
|
13
|
+
class AccessDenied < Error; end
|
|
14
|
+
|
|
15
|
+
ACCESS_KEY = 'HKCU\Software\Microsoft\Office\11.0\Excel\Security'.freeze
|
|
16
|
+
ACCESS_VALUE = 'AccessVBOM'.freeze
|
|
17
|
+
CODEPAGE_KEY = 'HKLM\System\CurrentControlSet\Control\Nls\CodePage'.freeze
|
|
18
|
+
|
|
19
|
+
# :enabled, :disabled, or :unset when the value is not there at all.
|
|
20
|
+
def self.state
|
|
21
|
+
raw = read(ACCESS_KEY, ACCESS_VALUE)
|
|
22
|
+
return :unset if raw.nil?
|
|
23
|
+
|
|
24
|
+
raw.to_i(16).zero? ? :disabled : :enabled
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.enabled?
|
|
28
|
+
state == :enabled
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Touching VBProject when access is refused gives 0x800A03EC and a
|
|
32
|
+
# localized message -- the same HRESULT a rejected NumberFormat
|
|
33
|
+
# gives, and neither identifies the condition. The registry is what
|
|
34
|
+
# turns the refusal into advice.
|
|
35
|
+
def self.denied!
|
|
36
|
+
message =
|
|
37
|
+
case state
|
|
38
|
+
when :enabled
|
|
39
|
+
'access to the VBA project was refused even though the registry ' \
|
|
40
|
+
'has it enabled -- Excel reads that setting when it starts, so ' \
|
|
41
|
+
'restart Excel if it was switched on while this instance was running'
|
|
42
|
+
else
|
|
43
|
+
'access to the VBA project is disabled. Run `wineole-vba enable`, ' \
|
|
44
|
+
'then restart Excel -- it reads the setting at startup'
|
|
45
|
+
end
|
|
46
|
+
raise AccessDenied, message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.enable!
|
|
50
|
+
write(ACCESS_KEY, ACCESS_VALUE, '1')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.disable!
|
|
54
|
+
write(ACCESS_KEY, ACCESS_VALUE, '0')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The Windows ANSI codepage of this prefix, as a name Ruby knows.
|
|
58
|
+
# Never hardcoded: VBA source files are written and read in whatever
|
|
59
|
+
# this is, and it is not CP932 everywhere.
|
|
60
|
+
#
|
|
61
|
+
# Memoized because it costs a `wine reg` subprocess -- measured at
|
|
62
|
+
# 328 ms on this host -- and a machine's ANSI codepage does not change
|
|
63
|
+
# while a process runs. Without this an import pays it twice and a
|
|
64
|
+
# non-ASCII refusal pays it three times.
|
|
65
|
+
def self.codepage
|
|
66
|
+
@codepage ||= read_codepage
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# For tests, which swap the codepage to exercise both sides of it.
|
|
70
|
+
def self.forget_codepage
|
|
71
|
+
@codepage = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.read_codepage
|
|
75
|
+
raw = read(CODEPAGE_KEY, 'ACP')
|
|
76
|
+
raise Error, "could not read the ANSI codepage (ACP) from #{CODEPAGE_KEY}" if raw.nil?
|
|
77
|
+
|
|
78
|
+
name = "CP#{raw}"
|
|
79
|
+
begin
|
|
80
|
+
::Encoding.find(name)
|
|
81
|
+
rescue ArgumentError
|
|
82
|
+
raise Error, "the registry reports ANSI codepage #{raw.inspect}, which Ruby does not know"
|
|
83
|
+
end
|
|
84
|
+
name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# One explanation, used by both paths that hand text to Excel. They
|
|
88
|
+
# are bound by the same codepage and fail the same way, so the caller
|
|
89
|
+
# should not be able to tell from the message which one they hit --
|
|
90
|
+
# only which character stopped it.
|
|
91
|
+
#
|
|
92
|
+
# `where` says what the text was, because the way out differs: code
|
|
93
|
+
# given as a string can be rewritten with ChrW(), a file has to be
|
|
94
|
+
# edited.
|
|
95
|
+
def self.unrepresentable!(char, where)
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"#{where} contains #{char.inspect}, which the system codepage (#{codepage}) " \
|
|
98
|
+
"cannot represent. Excel stores a module's text in that codepage, so the " \
|
|
99
|
+
'character would be silently replaced rather than stored. Rewrite it with ' \
|
|
100
|
+
'Chr()/ChrW() escapes, which are built at run time and are not bound by the ' \
|
|
101
|
+
'codepage the source text is'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# BOMs, longest first -- FF FE 00 00 is UTF-32LE and also starts with
|
|
105
|
+
# the UTF-16LE BOM, so a shorter match must never be tried first.
|
|
106
|
+
BOMS = [
|
|
107
|
+
["\x00\x00\xFE\xFF".b, 'UTF-32BE'],
|
|
108
|
+
["\xFF\xFE\x00\x00".b, 'UTF-32LE'],
|
|
109
|
+
["\xEF\xBB\xBF".b, 'UTF-8'],
|
|
110
|
+
["\xFE\xFF".b, 'UTF-16BE'],
|
|
111
|
+
["\xFF\xFE".b, 'UTF-16LE']
|
|
112
|
+
].freeze
|
|
113
|
+
|
|
114
|
+
# What encoding a VBA source file should be read as. Three rules, and
|
|
115
|
+
# every one of them decides on evidence rather than on a guess -- an
|
|
116
|
+
# encoding a heuristic merely finds likely is exactly the "succeeded,
|
|
117
|
+
# returned a value, the value is wrong" failure this wrapper exists to
|
|
118
|
+
# remove.
|
|
119
|
+
#
|
|
120
|
+
# 1. A BOM is conclusive. Follow it.
|
|
121
|
+
# 2. Bytes that are not valid UTF-8 PROVE the file is not UTF-8, so
|
|
122
|
+
# read it as the ANSI codepage -- which is what Excel's own
|
|
123
|
+
# Export writes, and what every .bas from a Windows toolchain is.
|
|
124
|
+
# 3. Otherwise UTF-8.
|
|
125
|
+
#
|
|
126
|
+
# Rule 2 is the one that earns its place, and the direction matters:
|
|
127
|
+
# measured on this host, a CP932 file read as UTF-8 is invalid 95.07%
|
|
128
|
+
# of the time at ONE non-ASCII character and 99.99% by five, so real
|
|
129
|
+
# codepage files land here almost without exception. The reverse does
|
|
130
|
+
# not hold -- UTF-8 bytes read as CP932 come out VALID from two
|
|
131
|
+
# characters on, silently wrong. That asymmetry is why UTF-8 is the
|
|
132
|
+
# fallback in rule 3 and the codepage is never the default: guessing
|
|
133
|
+
# UTF-8 and being wrong is loud, guessing the codepage and being wrong
|
|
134
|
+
# is silent.
|
|
135
|
+
#
|
|
136
|
+
# What is left is a file in the codepage whose bytes happen to be
|
|
137
|
+
# valid UTF-8 -- undecidable, by construction, for anyone. Pass
|
|
138
|
+
# `encoding:` to skip all of this when you already know.
|
|
139
|
+
def self.detect_encoding(path)
|
|
140
|
+
head = ::File.binread(path, 4).to_s
|
|
141
|
+
BOMS.each { |bytes, name| return name if head.start_with?(bytes) }
|
|
142
|
+
|
|
143
|
+
::File.binread(path).force_encoding('UTF-8').valid_encoding? ? 'UTF-8' : codepage
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.read(key, value)
|
|
147
|
+
out, ok = run_reg(['query', key, '/v', value])
|
|
148
|
+
return nil unless ok
|
|
149
|
+
|
|
150
|
+
# " NAME TYPE VALUE", indented, and wine leaves a CR on the
|
|
151
|
+
# end. Stripping each line before splitting is what makes both go
|
|
152
|
+
# away: without it the leading spaces produce an empty first field
|
|
153
|
+
# and the value comes back as "REG_SZ 932\r\n".
|
|
154
|
+
#
|
|
155
|
+
# The name is matched as a whole field, not as a substring of the
|
|
156
|
+
# line -- otherwise a value name that happens to appear inside
|
|
157
|
+
# another value's data would match, and it would happen silently.
|
|
158
|
+
matches = out.lines.filter_map do |l|
|
|
159
|
+
parts = l.strip.split(/\s+/, 3)
|
|
160
|
+
parts if parts.length == 3 && parts[0] == value
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if matches.length > 1
|
|
164
|
+
raise Error,
|
|
165
|
+
"found #{matches.length} lines naming #{value} in `wine reg query #{key}` " \
|
|
166
|
+
"output, expected exactly one: #{out.inspect}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if matches.empty?
|
|
170
|
+
raise Error,
|
|
171
|
+
"the command succeeded but no line naming #{value} could be parsed from " \
|
|
172
|
+
"`wine reg query #{key}` output: #{out.inspect}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
matches.first[2]
|
|
176
|
+
end
|
|
177
|
+
private_class_method :read
|
|
178
|
+
|
|
179
|
+
def self.write(key, value, data)
|
|
180
|
+
_out, ok = run_reg(['add', key, '/v', value, '/t', 'REG_DWORD', '/d', data, '/f'])
|
|
181
|
+
ok
|
|
182
|
+
end
|
|
183
|
+
private_class_method :write
|
|
184
|
+
|
|
185
|
+
# The only place that shells out, and deliberately not private: it is
|
|
186
|
+
# the seam the tests replace so that no test touches a real registry.
|
|
187
|
+
#
|
|
188
|
+
# Exit status is the only trustworthy signal. `reg` writes both its
|
|
189
|
+
# success message and its not-found message to stdout, in the system
|
|
190
|
+
# language, and wine writes unrelated `fixme:` lines to stderr.
|
|
191
|
+
def self.run_reg(args)
|
|
192
|
+
out = IO.popen(['wine', 'reg', *args], err: File::NULL, &:read)
|
|
193
|
+
[out.to_s, $CHILD_STATUS&.success? || false]
|
|
194
|
+
rescue SystemCallError, IOError
|
|
195
|
+
['', false]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
require 'tmpdir'
|
|
2
|
+
require_relative 'paths'
|
|
3
|
+
require_relative 'vba'
|
|
4
|
+
require_relative 'vba_block'
|
|
5
|
+
|
|
6
|
+
module WineOLE
|
|
7
|
+
module MSOffice
|
|
8
|
+
# The VBA surface of a workbook, reached as `book.vba`.
|
|
9
|
+
#
|
|
10
|
+
# TWO GRANULARITIES LIVE HERE and the names are what keep them apart.
|
|
11
|
+
# `write` and `remove` act on a named BLOCK inside a module -- the span
|
|
12
|
+
# this wrapper owns, delimited by sentinel comments, which may sit in a
|
|
13
|
+
# module full of code somebody else wrote. `add_component`,
|
|
14
|
+
# `remove_component`, `import` and `export` act on whole COMPONENTS.
|
|
15
|
+
#
|
|
16
|
+
# The verbs are not interchangeable either, and the difference is
|
|
17
|
+
# deliberate. `write` is an upsert: writing the same name again replaces
|
|
18
|
+
# that block, so it is idempotent. `add_component` is create-only and
|
|
19
|
+
# refuses a name that is already taken -- "overwriting" a component
|
|
20
|
+
# would destroy whatever a person put in it, so there is no way to ask
|
|
21
|
+
# for that.
|
|
22
|
+
#
|
|
23
|
+
# WHERE CODE GOES DECIDES WHETHER IT CAN BE CALLED. Measured against a
|
|
24
|
+
# live Excel 11:
|
|
25
|
+
#
|
|
26
|
+
# standard module Run("Name") works and returns the value;
|
|
27
|
+
# a worksheet formula =Name() works too.
|
|
28
|
+
# Private does NOT hide it from either.
|
|
29
|
+
# ThisWorkbook, Run("Name") fails ("macro not found"). Run with
|
|
30
|
+
# a worksheet, the module qualified -- Run("Sheet1.Name") --
|
|
31
|
+
# a UserForm runs it but hands back nil, so a Function's
|
|
32
|
+
# return value cannot be collected. =Name() is
|
|
33
|
+
# #NAME?.
|
|
34
|
+
#
|
|
35
|
+
# So code meant to be called belongs in a standard module (which is
|
|
36
|
+
# where `into: nil` puts it), and code in a sheet or ThisWorkbook module
|
|
37
|
+
# is there to be reached by Excel itself -- an ActiveX control's
|
|
38
|
+
# `_Click`, a workbook event.
|
|
39
|
+
class BookVBA
|
|
40
|
+
# The module this wrapper makes for itself when `into:` is not given.
|
|
41
|
+
DEFAULT_MODULE = 'WineOLE'.freeze
|
|
42
|
+
|
|
43
|
+
# VBComponents.Add's type argument. 100 (a Document module -- a
|
|
44
|
+
# worksheet or ThisWorkbook) is deliberately absent: Excel owns those
|
|
45
|
+
# and neither creates nor destroys them on request.
|
|
46
|
+
KINDS = { standard: 1, class: 2, form: 3 }.freeze
|
|
47
|
+
|
|
48
|
+
# The Type of a component Excel owns. Cannot be added, cannot be
|
|
49
|
+
# removed -- only emptied.
|
|
50
|
+
DOCUMENT_TYPE = 100
|
|
51
|
+
|
|
52
|
+
def initialize(ole, convert_paths:)
|
|
53
|
+
@ole = ole
|
|
54
|
+
@convert_paths = convert_paths
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Put a named block of VBA into this workbook.
|
|
58
|
+
#
|
|
59
|
+
# `into:` names an EXISTING component -- a UserForm, ThisWorkbook, a
|
|
60
|
+
# worksheet's module, a module made with #add_component. It is not
|
|
61
|
+
# created on demand: a typo would otherwise become a new module in
|
|
62
|
+
# silence. Without it the block goes in this wrapper's own module,
|
|
63
|
+
# which is created on demand because its name is not the caller's to
|
|
64
|
+
# get wrong.
|
|
65
|
+
#
|
|
66
|
+
# The block is what this wrapper owns -- writing the same name again
|
|
67
|
+
# replaces it, and nothing else in the module is touched. That matters
|
|
68
|
+
# because the module may hold code somebody wrote by hand.
|
|
69
|
+
def write(code, name: 'main', into: nil)
|
|
70
|
+
VBABlock.write(target_module(into), name, code)
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Remove a named block. When `from:` is given, only the block is
|
|
75
|
+
# removed -- a component the caller named is the caller's, never swept
|
|
76
|
+
# up here (a UserForm can be deleted, unlike ThisWorkbook, so this has
|
|
77
|
+
# to be a rule rather than an accident of what COM allows). Use
|
|
78
|
+
# #remove_component to delete one deliberately.
|
|
79
|
+
#
|
|
80
|
+
# Without `from:`, this wrapper's own module is the target, and when
|
|
81
|
+
# it has nothing but whitespace left the module goes too -- an empty
|
|
82
|
+
# module is litter, and that one IS ours to clean up.
|
|
83
|
+
#
|
|
84
|
+
# VBABlock.remove already fetched the whole body to find the block; it
|
|
85
|
+
# hands back the remaining lines so this does not have to fetch the
|
|
86
|
+
# body a second time just to ask whether it is empty.
|
|
87
|
+
def remove(name, from: nil)
|
|
88
|
+
if from
|
|
89
|
+
VBABlock.remove(named_component!(from).CodeModule, name)
|
|
90
|
+
return self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
component = existing_component(DEFAULT_MODULE)
|
|
94
|
+
return self if component.nil?
|
|
95
|
+
|
|
96
|
+
remaining = VBABlock.remove(component.CodeModule, name)
|
|
97
|
+
project.VBComponents.Remove(component) if remaining && VBABlock.blank_lines?(remaining)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create an empty component. Create-only on purpose: see the class
|
|
102
|
+
# comment on why there is no "overwrite a component".
|
|
103
|
+
#
|
|
104
|
+
# The existence check has to come BEFORE Add, not after, and that is
|
|
105
|
+
# measured rather than defensive: Add and the rename that follows it
|
|
106
|
+
# are not atomic. Adding under a taken name SUCCEEDS, and only the
|
|
107
|
+
# rename fails (0x80020009), leaving a stray `Module1` behind that
|
|
108
|
+
# nobody asked for. Checking first is what keeps the failure clean.
|
|
109
|
+
def add_component(name, kind: :standard)
|
|
110
|
+
type = KINDS[kind]
|
|
111
|
+
unless type
|
|
112
|
+
raise ArgumentError,
|
|
113
|
+
"unknown component kind #{kind.inspect} -- expected one of #{KINDS.keys.inspect}. " \
|
|
114
|
+
'A worksheet module and ThisWorkbook are not on that list because Excel ' \
|
|
115
|
+
'owns them; they exist already and cannot be made'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if existing_component(name)
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"this workbook already has a VBA component named #{name.inspect}. " \
|
|
121
|
+
'add_component never overwrites one -- that would destroy whatever is ' \
|
|
122
|
+
'in it. Remove it first, or use write(into:) to put a block inside it'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
component = project.VBComponents.Add(type)
|
|
126
|
+
begin
|
|
127
|
+
component.Name = name
|
|
128
|
+
rescue WineOLE::RemoteError
|
|
129
|
+
# Add succeeded and the rename did not, so the component exists
|
|
130
|
+
# under a name nobody chose. Take it back out rather than leaving
|
|
131
|
+
# the litter the pre-check exists to prevent.
|
|
132
|
+
project.VBComponents.Remove(component)
|
|
133
|
+
raise ArgumentError,
|
|
134
|
+
"Excel refused #{name.inspect} as a component name. VBA names start with a " \
|
|
135
|
+
'letter and hold letters, digits and underscores, up to 31 characters'
|
|
136
|
+
end
|
|
137
|
+
component
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Delete a component outright, with whatever is inside it.
|
|
141
|
+
def remove_component(name)
|
|
142
|
+
component = named_component!(name)
|
|
143
|
+
if component.Type == DOCUMENT_TYPE
|
|
144
|
+
raise ArgumentError,
|
|
145
|
+
"#{name.inspect} is a module Excel owns (a worksheet's, or ThisWorkbook's) " \
|
|
146
|
+
'and cannot be deleted -- it exists for as long as the sheet or the workbook ' \
|
|
147
|
+
'does. To take this wrapper\'s code back out of it, use remove(name, from:)'
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
project.VBComponents.Remove(component)
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Read a VBA source file on this machine into the project as a new
|
|
155
|
+
# component. Excel's own Import does the work, so the component's name
|
|
156
|
+
# and kind come from the file.
|
|
157
|
+
#
|
|
158
|
+
# `encoding:` skips the detection when the caller already knows; the
|
|
159
|
+
# rules the default follows are on VBA.detect_encoding.
|
|
160
|
+
def import(path, encoding: nil)
|
|
161
|
+
local_bridge!('import')
|
|
162
|
+
reject_dotdot!(path)
|
|
163
|
+
source = encoding || VBA.detect_encoding(path)
|
|
164
|
+
text = decode(::File.binread(path), source, path, guessed: encoding.nil?)
|
|
165
|
+
|
|
166
|
+
Dir.mktmpdir('wineole-vba') do |dir|
|
|
167
|
+
staged = ::File.join(dir, ::File.basename(path))
|
|
168
|
+
::File.binwrite(staged, to_codepage(text, path))
|
|
169
|
+
project.VBComponents.Import(Paths.to_wine(staged))
|
|
170
|
+
end
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Write a component out as a file on this machine, in UTF-8 with LF --
|
|
175
|
+
# what Excel produces is the ANSI codepage with CRLF, and the
|
|
176
|
+
# destination is a Linux path.
|
|
177
|
+
def export(name, path)
|
|
178
|
+
local_bridge!('export')
|
|
179
|
+
reject_dotdot!(path)
|
|
180
|
+
component = named_component!(name)
|
|
181
|
+
Dir.mktmpdir('wineole-vba') do |dir|
|
|
182
|
+
staged = ::File.join(dir, ::File.basename(path))
|
|
183
|
+
component.Export(Paths.to_wine(staged))
|
|
184
|
+
text = ::File.binread(staged).force_encoding(VBA.codepage).encode('UTF-8')
|
|
185
|
+
::File.write(path, text.gsub("\r\n", "\n"))
|
|
186
|
+
end
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The workbook's VBA project, or an error that says what to do about
|
|
191
|
+
# it. The HRESULT and the message are both useless for telling this
|
|
192
|
+
# condition apart -- 0x800A03EC is what a rejected NumberFormat gives
|
|
193
|
+
# too, and the text is localized -- so the registry is what turns a
|
|
194
|
+
# refusal into advice.
|
|
195
|
+
def project
|
|
196
|
+
@ole.VBProject
|
|
197
|
+
rescue WineOLE::RemoteError
|
|
198
|
+
VBA.denied!
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
# The other direction, and it had been left bare: a file that reads
|
|
204
|
+
# cleanly can still hold a character the codepage cannot store, and
|
|
205
|
+
# #encode raised Encoding::UndefinedConversionError naming neither the
|
|
206
|
+
# file nor the way out. Same rule and same words as the string path --
|
|
207
|
+
# refuse rather than let Excel substitute in silence.
|
|
208
|
+
def to_codepage(text, path)
|
|
209
|
+
text.encode(VBA.codepage)
|
|
210
|
+
rescue ::Encoding::UndefinedConversionError => e
|
|
211
|
+
VBA.unrepresentable!(e.error_char, path.to_s)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Both encodings can be wrong at once: the bytes are not valid UTF-8
|
|
215
|
+
# (which is what put us on the codepage branch) and not valid in the
|
|
216
|
+
# codepage either. Nothing can be inferred from that, so say so --
|
|
217
|
+
# a bare Encoding::InvalidByteSequenceError names neither the file
|
|
218
|
+
# nor why that encoding was the one tried.
|
|
219
|
+
def decode(raw, source, path, guessed:)
|
|
220
|
+
raw.dup.force_encoding(source).encode('UTF-8').sub(/\A\uFEFF/, '')
|
|
221
|
+
rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError => e
|
|
222
|
+
why =
|
|
223
|
+
if guessed
|
|
224
|
+
"#{source} was tried because the file has no BOM and its bytes are not valid " \
|
|
225
|
+
"UTF-8, which rules UTF-8 out -- but they are not valid #{source} either, so " \
|
|
226
|
+
'there is nothing left to infer from. Pass `encoding:` if you know what this is'
|
|
227
|
+
else
|
|
228
|
+
"you passed encoding: #{source.inspect}, and the file's bytes are not valid in it"
|
|
229
|
+
end
|
|
230
|
+
raise ArgumentError, "cannot read #{path} as #{source} (#{e.message}). #{why}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# These hand Excel a path to a file on this machine. When the bridge
|
|
234
|
+
# is somewhere else that path means that machine's filesystem, and
|
|
235
|
+
# there is no sensible thing to do with it.
|
|
236
|
+
#
|
|
237
|
+
# Keying this off @convert_paths (rather than a separate "is this
|
|
238
|
+
# loopback" flag) is deliberate, and it means a caller who passed
|
|
239
|
+
# convert_paths: false for a legitimate reason on a *loopback* bridge
|
|
240
|
+
# gets refused here too, with a message that says "needs the bridge to
|
|
241
|
+
# be on this machine" when it already is. That is the wrong reason but
|
|
242
|
+
# never the wrong direction: this can only over-refuse a bridge that
|
|
243
|
+
# would have worked, never under-refuse one that would not.
|
|
244
|
+
def local_bridge!(what)
|
|
245
|
+
return if @convert_paths
|
|
246
|
+
|
|
247
|
+
raise ArgumentError,
|
|
248
|
+
"#{what} needs the bridge to be on this machine: it stages a file " \
|
|
249
|
+
'and hands Excel the path, which means nothing on another host'
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# File.basename of a path ending in ".." is literally "..". For import
|
|
253
|
+
# that path is read directly, so it already raises Errno::EISDIR of its
|
|
254
|
+
# own accord -- but a bare EISDIR does not say why. For export the same
|
|
255
|
+
# basename feeds File.join(dir, "..") when staging, which resolves to
|
|
256
|
+
# dir's *parent* rather than anywhere under our own tmpdir, and Export
|
|
257
|
+
# ends up trying to write a file over that directory -- also EISDIR,
|
|
258
|
+
# also confusing. Nothing escapes and nothing is corrupted either way;
|
|
259
|
+
# this just gives both a clear error instead of a bare Errno.
|
|
260
|
+
def reject_dotdot!(path)
|
|
261
|
+
return unless ::File.basename(path) == '..'
|
|
262
|
+
|
|
263
|
+
raise ArgumentError, "#{path.inspect} is not a usable file path (its basename is \"..\")"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def existing_component(name)
|
|
267
|
+
project.VBComponents.Item(name)
|
|
268
|
+
rescue WineOLE::RemoteError
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def target_module(into)
|
|
273
|
+
return own_module.CodeModule if into.nil?
|
|
274
|
+
|
|
275
|
+
named_component!(into).CodeModule
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# This wrapper's own module, made on demand. Unlike a name the caller
|
|
279
|
+
# passed, this one cannot be a typo, so creating it silently is safe.
|
|
280
|
+
def own_module
|
|
281
|
+
found = existing_component(DEFAULT_MODULE)
|
|
282
|
+
return found unless found.nil?
|
|
283
|
+
|
|
284
|
+
component = project.VBComponents.Add(KINDS[:standard])
|
|
285
|
+
component.Name = DEFAULT_MODULE
|
|
286
|
+
component
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def named_component!(name)
|
|
290
|
+
found = existing_component(name)
|
|
291
|
+
return found unless found.nil?
|
|
292
|
+
|
|
293
|
+
raise ArgumentError,
|
|
294
|
+
"this workbook has no VBA component named #{name.inspect}. " \
|
|
295
|
+
'Components are UserForms, ThisWorkbook, worksheet modules and ' \
|
|
296
|
+
'standard modules; add one with add_component, or omit `into:` ' \
|
|
297
|
+
'to use this wrapper\'s own module'
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The VBA surface of one worksheet, reached as `sheet.vba`.
|
|
302
|
+
#
|
|
303
|
+
# Blocks only. A worksheet's module cannot be created or deleted -- Excel
|
|
304
|
+
# makes it with the sheet and destroys it with the sheet -- so the
|
|
305
|
+
# component methods are absent here rather than present and always
|
|
306
|
+
# failing. Removing the last block empties the module; it does not
|
|
307
|
+
# remove it.
|
|
308
|
+
class SheetVBA
|
|
309
|
+
def initialize(ole)
|
|
310
|
+
@ole = ole
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def write(code, name: 'main')
|
|
314
|
+
VBABlock.write(code_module, name, code)
|
|
315
|
+
self
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def remove(name)
|
|
319
|
+
VBABlock.remove(code_module, name)
|
|
320
|
+
self
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
# A worksheet's handlers live in the worksheet's own code module --
|
|
326
|
+
# that is where Excel looks for `<ActiveX control>_Click`. The module
|
|
327
|
+
# is named by the sheet's CodeName, inside the parent workbook's
|
|
328
|
+
# project.
|
|
329
|
+
#
|
|
330
|
+
# THE ORDER OF THESE TWO LINES IS LOAD-BEARING, and it is not obvious.
|
|
331
|
+
# Worksheet.CodeName comes back as "" until something has touched that
|
|
332
|
+
# workbook's VBProject -- measured: "" before, "Sheet3" after, and
|
|
333
|
+
# "Sheet3" on every read from then on. Reading the name first and then
|
|
334
|
+
# the project (which looks like the same code, and is what extracting
|
|
335
|
+
# a local naturally produces) hands VBComponents.Item("") and gets
|
|
336
|
+
# 0x800A0009, "index out of range". So the project is fetched first,
|
|
337
|
+
# on purpose, and the name after it.
|
|
338
|
+
#
|
|
339
|
+
# The empty check is what keeps that from becoming a silent trap
|
|
340
|
+
# again: if this ever stops holding, it fails saying why instead of
|
|
341
|
+
# failing as a bare COM index error.
|
|
342
|
+
def code_module
|
|
343
|
+
vb_project = project
|
|
344
|
+
code_name = @ole.CodeName.to_s
|
|
345
|
+
if code_name.empty?
|
|
346
|
+
raise VBA::Error,
|
|
347
|
+
'this worksheet reports no CodeName even after its VBProject was opened, ' \
|
|
348
|
+
'so there is no way to find its code module'
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
vb_project.VBComponents.Item(code_name).CodeModule
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Only the VBProject fetch is the denial. Wrapping the lookup that
|
|
355
|
+
# follows it in the same rescue would report any other COM failure --
|
|
356
|
+
# a component that is not there, a module that will not open -- as
|
|
357
|
+
# "turn on AccessVBOM", which is advice for a condition the caller is
|
|
358
|
+
# not in.
|
|
359
|
+
def project
|
|
360
|
+
@ole.Parent.VBProject
|
|
361
|
+
rescue WineOLE::RemoteError
|
|
362
|
+
VBA.denied!
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|