ukiryu 0.1.6 → 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/lib/ukiryu/cache.rb +6 -0
- data/lib/ukiryu/cache_registry.rb +64 -0
- data/lib/ukiryu/cli_commands/base_command.rb +6 -5
- data/lib/ukiryu/cli_commands/config_command.rb +7 -10
- data/lib/ukiryu/cli_commands/register_command.rb +27 -18
- data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
- data/lib/ukiryu/command_builder.rb +83 -50
- data/lib/ukiryu/config.rb +13 -2
- data/lib/ukiryu/debug.rb +20 -9
- data/lib/ukiryu/definition/loader.rb +3 -3
- data/lib/ukiryu/errors.rb +37 -37
- data/lib/ukiryu/executable_locator.rb +40 -16
- data/lib/ukiryu/extractors/base_extractor.rb +2 -1
- data/lib/ukiryu/extractors/help_parser.rb +3 -0
- data/lib/ukiryu/logger.rb +51 -0
- data/lib/ukiryu/models/implementation_index.rb +2 -1
- data/lib/ukiryu/models/implementation_version.rb +18 -1
- data/lib/ukiryu/models/interface.rb +2 -1
- data/lib/ukiryu/models/run_environment.rb +0 -2
- data/lib/ukiryu/models/semantic_version.rb +174 -0
- data/lib/ukiryu/models/stage_metrics.rb +0 -1
- data/lib/ukiryu/register.rb +473 -232
- data/lib/ukiryu/shell/powershell.rb +209 -89
- data/lib/ukiryu/shell/sh.rb +4 -1
- data/lib/ukiryu/shell.rb +60 -2
- data/lib/ukiryu/tool/command_resolution.rb +2 -1
- data/lib/ukiryu/tool/executable_discovery.rb +14 -15
- data/lib/ukiryu/tool/loader.rb +543 -0
- data/lib/ukiryu/tool/version_detection.rb +1 -3
- data/lib/ukiryu/tool.rb +79 -87
- data/lib/ukiryu/tool_index.rb +127 -62
- data/lib/ukiryu/tools/base.rb +4 -2
- data/lib/ukiryu/type.rb +26 -15
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu.rb +1 -1
- data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
- data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/tool_helper.rb +2 -0
- data/spec/ukiryu/definition/loader_spec.rb +2 -2
- data/spec/ukiryu/executor_spec.rb +6 -3
- data/spec/ukiryu/models/execution_report_spec.rb +3 -2
- data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
- data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
- data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
- data/spec/ukiryu/shell/powershell_spec.rb +286 -51
- data/spec/ukiryu/tool/loader_spec.rb +148 -0
- data/spec/ukiryu/tool_index_spec.rb +110 -18
- data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
- data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
- data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
- metadata +14 -2
- data/lib/ukiryu/register_auto_manager.rb +0 -342
|
@@ -12,70 +12,95 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
describe '#escape' do
|
|
15
|
-
it 'escapes
|
|
16
|
-
expect(shell.escape('
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it 'escapes dollar signs' do
|
|
20
|
-
expect(shell.escape('$VAR')).to eq('`$VAR')
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it 'escapes double quotes' do
|
|
24
|
-
expect(shell.escape('say "hello"')).to eq('say `"hello`"')
|
|
15
|
+
it 'escapes single quotes by doubling them (PowerShell convention for single-quoted strings)' do
|
|
16
|
+
expect(shell.escape("it's")).to eq("it''s")
|
|
25
17
|
end
|
|
26
18
|
|
|
27
19
|
it 'handles empty strings' do
|
|
28
20
|
expect(shell.escape('')).to eq('')
|
|
29
21
|
end
|
|
30
22
|
|
|
31
|
-
it 'handles strings with multiple
|
|
32
|
-
expect(shell.escape('
|
|
23
|
+
it 'handles strings with multiple single quotes' do
|
|
24
|
+
expect(shell.escape("'hello'world'")).to eq("''hello''world''")
|
|
33
25
|
end
|
|
34
26
|
|
|
35
|
-
it 'preserves characters
|
|
36
|
-
|
|
37
|
-
expect(shell.escape('
|
|
27
|
+
it 'preserves other characters (single-quoted strings are literal)' do
|
|
28
|
+
# In PowerShell single-quoted strings, these are all literal
|
|
29
|
+
expect(shell.escape('$VAR')).to eq('$VAR')
|
|
30
|
+
expect(shell.escape('hello`world')).to eq('hello`world')
|
|
31
|
+
expect(shell.escape('hello&world')).to eq('hello&world')
|
|
38
32
|
end
|
|
39
33
|
|
|
40
34
|
context 'security: command injection prevention' do
|
|
41
|
-
it '
|
|
35
|
+
it 'preserves semicolons (safe inside single quotes)' do
|
|
42
36
|
expect(shell.escape('arg1;rm -rf /')).to eq('arg1;rm -rf /')
|
|
43
37
|
end
|
|
44
38
|
|
|
45
|
-
it '
|
|
46
|
-
expect(shell.escape('
|
|
39
|
+
it 'preserves dollar signs (literal in single quotes)' do
|
|
40
|
+
expect(shell.escape('$PATH')).to eq('$PATH')
|
|
47
41
|
end
|
|
48
42
|
|
|
49
|
-
it '
|
|
50
|
-
expect(shell.escape('
|
|
43
|
+
it 'preserves backticks (literal in single quotes)' do
|
|
44
|
+
expect(shell.escape('`malicious`')).to eq('`malicious`')
|
|
51
45
|
end
|
|
52
46
|
end
|
|
53
47
|
end
|
|
54
48
|
|
|
49
|
+
describe '#escape_for_double_quotes' do
|
|
50
|
+
it 'escapes backticks' do
|
|
51
|
+
expect(shell.escape_for_double_quotes('hello`world')).to eq('hello``world')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'escapes dollar signs' do
|
|
55
|
+
expect(shell.escape_for_double_quotes('$VAR')).to eq('`$VAR')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'escapes double quotes' do
|
|
59
|
+
expect(shell.escape_for_double_quotes('say "hello"')).to eq('say `"hello`"')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'handles empty strings' do
|
|
63
|
+
expect(shell.escape_for_double_quotes('')).to eq('')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'handles strings with multiple special characters' do
|
|
67
|
+
expect(shell.escape_for_double_quotes('`$')).to eq('```$')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'preserves single quotes (not special in double-quoted strings)' do
|
|
71
|
+
expect(shell.escape_for_double_quotes("it's")).to eq("it's")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
55
75
|
describe '#quote' do
|
|
56
76
|
context 'with default (for_exe: false)' do
|
|
57
|
-
it 'uses
|
|
58
|
-
expect(shell.quote('hello')).to eq("
|
|
77
|
+
it 'uses double quotes for arguments (to prevent PowerShell parameter binding)' do
|
|
78
|
+
expect(shell.quote('hello')).to eq('"hello"')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'preserves single quotes (not special in double-quoted strings)' do
|
|
82
|
+
# In double-quoted strings, single quotes are literal
|
|
83
|
+
expect(shell.quote("it's")).to eq('"it\'s"')
|
|
59
84
|
end
|
|
60
85
|
|
|
61
|
-
it '
|
|
62
|
-
|
|
63
|
-
# Only backtick, dollar, and double quotes need escaping
|
|
64
|
-
expect(shell.quote("it's")).to eq("'it's'")
|
|
86
|
+
it 'handles multiple single quotes in a string' do
|
|
87
|
+
expect(shell.quote("it's a test's")).to eq('"it\'s a test\'s"')
|
|
65
88
|
end
|
|
66
89
|
|
|
67
90
|
it 'handles empty strings' do
|
|
68
|
-
expect(shell.quote('')).to eq("'
|
|
91
|
+
expect(shell.quote('')).to eq('""')
|
|
69
92
|
end
|
|
70
93
|
|
|
71
94
|
it 'handles strings with spaces' do
|
|
72
|
-
expect(shell.quote('hello world')).to eq("
|
|
95
|
+
expect(shell.quote('hello world')).to eq('"hello world"')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'escapes dollar signs in double-quoted strings' do
|
|
99
|
+
expect(shell.quote('$VAR')).to eq('"`$VAR"')
|
|
73
100
|
end
|
|
74
101
|
|
|
75
|
-
it '
|
|
76
|
-
expect(shell.quote('
|
|
77
|
-
# Double quotes are escaped with backtick
|
|
78
|
-
expect(shell.quote('"quoted"')).to eq(%q('`"quoted`"'))
|
|
102
|
+
it 'escapes double quotes in double-quoted strings' do
|
|
103
|
+
expect(shell.quote('"quoted"')).to eq('"`"quoted`""')
|
|
79
104
|
end
|
|
80
105
|
end
|
|
81
106
|
|
|
@@ -84,10 +109,8 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
84
109
|
expect(shell.quote('C:\\Program Files\\app.exe', for_exe: true)).to eq('"C:\\Program Files\\app.exe"')
|
|
85
110
|
end
|
|
86
111
|
|
|
87
|
-
it '
|
|
88
|
-
|
|
89
|
-
# This is intentional for executable paths
|
|
90
|
-
expect(shell.quote('path "with" quotes', for_exe: true)).to eq('"path "with" quotes"')
|
|
112
|
+
it 'escapes special characters within double quotes' do
|
|
113
|
+
expect(shell.quote('path "with" quotes', for_exe: true)).to eq('"path `"with`" quotes"')
|
|
91
114
|
end
|
|
92
115
|
|
|
93
116
|
it 'handles paths with spaces' do
|
|
@@ -99,21 +122,32 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
99
122
|
end
|
|
100
123
|
|
|
101
124
|
it 'does not escape backslashes in paths (Windows paths)' do
|
|
102
|
-
# Backslashes in Windows paths should NOT be escaped inside double quotes
|
|
103
125
|
expect(shell.quote('C:\\Users\\file.txt', for_exe: true)).to eq('"C:\\Users\\file.txt"')
|
|
104
126
|
end
|
|
127
|
+
|
|
128
|
+
it 'escapes dollar signs in double-quoted strings' do
|
|
129
|
+
expect(shell.quote('$PATH', for_exe: true)).to eq('"`$PATH"')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'escapes backticks in double-quoted strings' do
|
|
133
|
+
expect(shell.quote('hello`world', for_exe: true)).to eq('"hello``world"')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'preserves single quotes (not special in double-quoted strings)' do
|
|
137
|
+
expect(shell.quote("it's", for_exe: true)).to eq('"it\'s"')
|
|
138
|
+
end
|
|
105
139
|
end
|
|
106
140
|
|
|
107
141
|
context 'security: command injection prevention' do
|
|
108
|
-
it 'properly quotes arguments to prevent injection' do
|
|
109
|
-
#
|
|
142
|
+
it 'properly quotes and escapes arguments to prevent injection' do
|
|
143
|
+
# In double quotes, $() would expand unless escaped
|
|
110
144
|
quoted = shell.quote('$(malicious)')
|
|
111
|
-
expect(quoted).to eq("
|
|
145
|
+
expect(quoted).to eq('"`$(malicious)"')
|
|
112
146
|
end
|
|
113
147
|
|
|
114
148
|
it 'properly quotes command chaining attempts' do
|
|
115
149
|
quoted = shell.quote('arg1; malicious')
|
|
116
|
-
expect(quoted).to eq("
|
|
150
|
+
expect(quoted).to eq('"arg1; malicious"')
|
|
117
151
|
end
|
|
118
152
|
end
|
|
119
153
|
end
|
|
@@ -150,7 +184,7 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
150
184
|
|
|
151
185
|
it 'quotes arguments that need quoting' do
|
|
152
186
|
result = shell.join('echo', 'hello world', 'test')
|
|
153
|
-
expect(result).to eq('echo
|
|
187
|
+
expect(result).to eq('echo "hello world" test')
|
|
154
188
|
end
|
|
155
189
|
|
|
156
190
|
it 'handles empty args array' do
|
|
@@ -158,11 +192,10 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
158
192
|
expect(result).to eq('echo')
|
|
159
193
|
end
|
|
160
194
|
|
|
161
|
-
it 'handles arguments with
|
|
162
|
-
# $VAR
|
|
163
|
-
# needs_quoting? returns false for $VAR
|
|
195
|
+
it 'handles arguments with dollar sign (quotes and escapes to prevent variable expansion)' do
|
|
196
|
+
# $VAR must be quoted and escaped to prevent PowerShell variable expansion
|
|
164
197
|
result = shell.join('echo', '$VAR')
|
|
165
|
-
expect(result).to eq('echo
|
|
198
|
+
expect(result).to eq('echo "`$VAR"')
|
|
166
199
|
end
|
|
167
200
|
|
|
168
201
|
it 'uses double quotes for executables with spaces' do
|
|
@@ -179,7 +212,7 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
179
212
|
|
|
180
213
|
it 'quotes subsequent arguments after -Command' do
|
|
181
214
|
result = shell.join('powershell', '-Command', 'script.ps1', 'arg with spaces')
|
|
182
|
-
expect(result).to eq('powershell -Command script.ps1
|
|
215
|
+
expect(result).to eq('powershell -Command script.ps1 "arg with spaces"')
|
|
183
216
|
end
|
|
184
217
|
end
|
|
185
218
|
|
|
@@ -191,7 +224,7 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
191
224
|
|
|
192
225
|
it 'quotes subsequent arguments after -File' do
|
|
193
226
|
result = shell.join('powershell', '-File', 'script.ps1', 'arg with spaces')
|
|
194
|
-
expect(result).to eq('powershell -File script.ps1
|
|
227
|
+
expect(result).to eq('powershell -File script.ps1 "arg with spaces"')
|
|
195
228
|
end
|
|
196
229
|
end
|
|
197
230
|
|
|
@@ -199,16 +232,85 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
199
232
|
it 'properly quotes arguments to prevent injection' do
|
|
200
233
|
result = shell.join('echo', 'hello; malicious')
|
|
201
234
|
# Arguments with spaces are quoted
|
|
202
|
-
expect(result).to eq('echo
|
|
235
|
+
expect(result).to eq('echo "hello; malicious"')
|
|
203
236
|
end
|
|
204
237
|
|
|
205
|
-
it '
|
|
238
|
+
it 'escapes backticks in double quotes' do
|
|
239
|
+
# In double-quoted strings, backticks must be escaped with backtick
|
|
206
240
|
result = shell.join('echo', '`malicious`')
|
|
207
|
-
expect(result).to eq('echo
|
|
241
|
+
expect(result).to eq('echo "``malicious``"')
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
context 'quoting dash-prefixed arguments' do
|
|
246
|
+
it 'quotes arguments starting with dash' do
|
|
247
|
+
result = shell.join('gswin64c.exe', '-sDEVICE=pdfwrite', 'input.eps')
|
|
248
|
+
expect(result).to eq('gswin64c.exe "-sDEVICE=pdfwrite" input.eps')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'quotes multiple dash-prefixed arguments' do
|
|
252
|
+
result = shell.join('gswin64c.exe', '-sDEVICE=pdfwrite',
|
|
253
|
+
'-sOutputFile=output.pdf', '-dBATCH', 'input.eps')
|
|
254
|
+
expect(result).to eq('gswin64c.exe "-sDEVICE=pdfwrite" ' \
|
|
255
|
+
'"-sOutputFile=output.pdf" "-dBATCH" input.eps')
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'quotes ImageMagick-style options' do
|
|
259
|
+
result = shell.join('magick', 'input.png', '-resize', '50x50', 'output.png')
|
|
260
|
+
expect(result).to eq('magick input.png "-resize" 50x50 output.png')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'still handles -Command specially (not quoted, next arg not quoted)' do
|
|
264
|
+
result = shell.join('powershell', '-Command', 'Write-Host hello')
|
|
265
|
+
expect(result).to eq('powershell -Command Write-Host hello')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'still handles -File specially (not quoted, next arg not quoted)' do
|
|
269
|
+
result = shell.join('powershell', '-File', 'script.ps1')
|
|
270
|
+
expect(result).to eq('powershell -File script.ps1')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it 'quotes dash-prefixed args after -Command script' do
|
|
274
|
+
result = shell.join('powershell', '-Command', 'script.ps1', '-SomeFlag')
|
|
275
|
+
expect(result).to eq('powershell -Command script.ps1 "-SomeFlag"')
|
|
208
276
|
end
|
|
209
277
|
end
|
|
210
278
|
end
|
|
211
279
|
|
|
280
|
+
describe '#needs_quoting?' do
|
|
281
|
+
it 'returns true for empty strings' do
|
|
282
|
+
expect(shell.needs_quoting?('')).to be true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'returns true for strings with whitespace' do
|
|
286
|
+
expect(shell.needs_quoting?('hello world')).to be true
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'returns true for strings with special characters' do
|
|
290
|
+
expect(shell.needs_quoting?('hello;world')).to be true
|
|
291
|
+
expect(shell.needs_quoting?('hello&world')).to be true
|
|
292
|
+
expect(shell.needs_quoting?('hello|world')).to be true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'returns true for strings starting with dash' do
|
|
296
|
+
expect(shell.needs_quoting?('-sDEVICE=pdfwrite')).to be true
|
|
297
|
+
expect(shell.needs_quoting?('-resize')).to be true
|
|
298
|
+
expect(shell.needs_quoting?('-dBATCH')).to be true
|
|
299
|
+
expect(shell.needs_quoting?('-')).to be true
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'returns true for strings containing dollar sign (to prevent variable expansion)' do
|
|
303
|
+
expect(shell.needs_quoting?('$VAR')).to be true
|
|
304
|
+
expect(shell.needs_quoting?('price$100')).to be true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'returns false for simple strings' do
|
|
308
|
+
expect(shell.needs_quoting?('hello')).to be false
|
|
309
|
+
expect(shell.needs_quoting?('input.eps')).to be false
|
|
310
|
+
expect(shell.needs_quoting?('output.pdf')).to be false
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
212
314
|
describe '#format_path' do
|
|
213
315
|
it 'returns paths unchanged' do
|
|
214
316
|
expect(shell.format_path('C:\\Users\\file.txt')).to eq('C:\\Users\\file.txt')
|
|
@@ -218,6 +320,11 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
218
320
|
expect(shell.format_path('/usr/bin/file')).to eq('/usr/bin/file')
|
|
219
321
|
end
|
|
220
322
|
|
|
323
|
+
it 'handles paths with spaces (quoting handled by quote method)' do
|
|
324
|
+
# format_path returns path as-is; quote method handles quoting
|
|
325
|
+
expect(shell.format_path('/path with spaces/file')).to eq('/path with spaces/file')
|
|
326
|
+
end
|
|
327
|
+
|
|
221
328
|
it 'handles relative paths' do
|
|
222
329
|
expect(shell.format_path('relative/path')).to eq('relative/path')
|
|
223
330
|
end
|
|
@@ -228,4 +335,132 @@ RSpec.describe Ukiryu::Shell::PowerShell do
|
|
|
228
335
|
expect(shell.headless_environment).to eq({})
|
|
229
336
|
end
|
|
230
337
|
end
|
|
338
|
+
|
|
339
|
+
# Tests for issues reported by Vectory team
|
|
340
|
+
context 'Vectory team reported issues' do
|
|
341
|
+
describe 'Issue 1: Prefix stripping with -sDEVICE=pdfwrite style arguments' do
|
|
342
|
+
it 'preserves the full argument when passed via join' do
|
|
343
|
+
# The argument -sDEVICE=pdfwrite should be preserved as a complete string
|
|
344
|
+
# The join() method quotes it with double quotes to prevent parameter binding
|
|
345
|
+
result = shell.join('gswin64c.exe', '-sDEVICE=pdfwrite', 'input.eps')
|
|
346
|
+
expect(result).to include('"-sDEVICE=pdfwrite"')
|
|
347
|
+
# Verify the prefix is NOT stripped (the full -sDEVICE=pdfwrite is present)
|
|
348
|
+
expect(result).to include('-sDEVICE=pdfwrite')
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'quotes dash-prefixed arguments with double quotes to prevent PowerShell parameter binding' do
|
|
352
|
+
# PowerShell's parameter binder can strip the -prefix from arguments
|
|
353
|
+
# Quoting with double quotes prevents this
|
|
354
|
+
arg = '-sDEVICE=pdfwrite'
|
|
355
|
+
expect(shell.needs_quoting?(arg)).to be true
|
|
356
|
+
expect(shell.quote(arg)).to eq('"-sDEVICE=pdfwrite"')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'handles Ghostscript-style device arguments correctly' do
|
|
360
|
+
result = shell.join('gswin64c.exe', '-sDEVICE=pdfwrite',
|
|
361
|
+
'-sOutputFile=output.pdf', '-dBATCH')
|
|
362
|
+
expect(result).to include('"-sDEVICE=pdfwrite"')
|
|
363
|
+
expect(result).to include('"-sOutputFile=output.pdf"')
|
|
364
|
+
expect(result).to include('"-dBATCH"')
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it 'quotes dash-prefixed args when executable path has spaces (needs_cmd branch)' do
|
|
368
|
+
# REGRESSION TEST for Ghostscript -sDEVICE=pdfwrite bug
|
|
369
|
+
#
|
|
370
|
+
# Ghostscript on Windows is installed in "C:\Program Files\gs\..."
|
|
371
|
+
# which has a space in the path.
|
|
372
|
+
#
|
|
373
|
+
# BUG: Previous code used cmd /c for executables with spaces, which had
|
|
374
|
+
# nested quoting issues. The fix uses PowerShell single quotes for everything.
|
|
375
|
+
#
|
|
376
|
+
# This test verifies the join() method handles this correctly.
|
|
377
|
+
# See also: spec/ukiryu/tools/ghostscript_spec.rb - Windows-only integration test
|
|
378
|
+
exe_with_space = 'C:\Program Files\gs\gs10.00.0\bin\gswin64c.exe'
|
|
379
|
+
result = shell.join(exe_with_space, '-sDEVICE=pdfwrite', '-dBATCH', 'input.ps')
|
|
380
|
+
# With the fix, we use double quotes (from quote() method) for join()
|
|
381
|
+
expect(result).to include('"C:\Program Files\gs\gs10.00.0\bin\gswin64c.exe"')
|
|
382
|
+
expect(result).to include('"-sDEVICE=pdfwrite"')
|
|
383
|
+
expect(result).to include('"-dBATCH"')
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
it 'execute_command uses single quotes for all args on Windows (no cmd /c)' do
|
|
387
|
+
# This test verifies the execute_command() internal quoting
|
|
388
|
+
# by capturing the actual command passed to Open3.capture3
|
|
389
|
+
|
|
390
|
+
# Skip on non-Windows platforms
|
|
391
|
+
skip 'Windows-only test' unless Ukiryu::Platform.windows?
|
|
392
|
+
|
|
393
|
+
captured_command = nil
|
|
394
|
+
# Create a proper mock status that responds to all required methods
|
|
395
|
+
mock_status = double('Process::Status')
|
|
396
|
+
allow(mock_status).to receive(:exited?).and_return(true)
|
|
397
|
+
allow(mock_status).to receive(:exitstatus).and_return(0)
|
|
398
|
+
allow(mock_status).to receive(:success?).and_return(true)
|
|
399
|
+
allow(mock_status).to receive(:stopped?).and_return(false)
|
|
400
|
+
allow(mock_status).to receive(:signaled?).and_return(false)
|
|
401
|
+
allow(mock_status).to receive(:termsig).and_return(nil)
|
|
402
|
+
allow(mock_status).to receive(:stopsig).and_return(nil)
|
|
403
|
+
|
|
404
|
+
allow(Open3).to receive(:capture3).and_wrap_original do |_m, *args|
|
|
405
|
+
captured_command = args
|
|
406
|
+
['', '', mock_status]
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
exe_with_space = 'C:\Program Files\gs\gs10.00.0\bin\gswin64c.exe'
|
|
410
|
+
shell.execute_command(exe_with_space, ['-sDEVICE=pdfwrite', 'input.ps'],
|
|
411
|
+
Ukiryu::Environment.system, 30, nil)
|
|
412
|
+
|
|
413
|
+
# The command passed to PowerShell should use single quotes for all args
|
|
414
|
+
# Format: powershell -NoLogo -Command <ps_command>
|
|
415
|
+
expect(captured_command[1]).to eq('powershell')
|
|
416
|
+
expect(captured_command[2]).to eq('-NoLogo')
|
|
417
|
+
expect(captured_command[3]).to eq('-Command')
|
|
418
|
+
|
|
419
|
+
ps_command = captured_command[4]
|
|
420
|
+
# Should contain single-quoted executable path (with space)
|
|
421
|
+
expect(ps_command).to include("'C:\\Program Files\\gs\\gs10.00.0\\bin\\gswin64c.exe'")
|
|
422
|
+
# Should contain single-quoted dash-prefixed arg (preserves the full -sDEVICE=pdfwrite)
|
|
423
|
+
expect(ps_command).to include("'-sDEVICE=pdfwrite'")
|
|
424
|
+
# The critical check: the dash prefix must be present (not stripped)
|
|
425
|
+
expect(ps_command).to include("'-sDEVICE=")
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
describe 'Issue 2: Consistent quoting between join and execute_command (FIXED)' do
|
|
430
|
+
it 'join uses double quotes for dash-prefixed arguments' do
|
|
431
|
+
result = shell.join('gswin64c.exe', '-sDEVICE=pdfwrite')
|
|
432
|
+
expect(result).to eq('gswin64c.exe "-sDEVICE=pdfwrite"')
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it 'execute_command uses double quotes for consistency' do
|
|
436
|
+
# FIXED: Both join() and execute_command() now use double quotes
|
|
437
|
+
# to prevent PowerShell's parameter binder from stripping - prefixes
|
|
438
|
+
# Double-quoted strings with proper escaping prevent parameter binding issues
|
|
439
|
+
expect(shell.quote('-sDEVICE=pdfwrite')).to eq('"-sDEVICE=pdfwrite"')
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
describe 'Issue 3: Single quote handling in PowerShell (FIXED)' do
|
|
444
|
+
it 'preserves single quotes (not special in double-quoted strings)' do
|
|
445
|
+
# In PowerShell double-quoted strings, single quotes are literal
|
|
446
|
+
expect(shell.quote("it's")).to eq('"it\'s"')
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it 'handles strings with multiple single quotes' do
|
|
450
|
+
expect(shell.quote("it's a test's value")).to eq('"it\'s a test\'s value"')
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'handles string that is just a single quote' do
|
|
454
|
+
expect(shell.quote("'")).to eq('"\'"')
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it 'handles string starting with single quote' do
|
|
458
|
+
expect(shell.quote("'hello")).to eq('"\'hello"')
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'handles string ending with single quote' do
|
|
462
|
+
expect(shell.quote("hello'")).to eq('"hello\'"')
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
231
466
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Ukiryu::Tool::Loader do
|
|
6
|
+
describe '.extract_profile_data' do
|
|
7
|
+
# Make private class method accessible
|
|
8
|
+
before { Ukiryu::Tool::Loader.singleton_class.send(:public, :extract_profile_data) }
|
|
9
|
+
|
|
10
|
+
context 'with Hash profile' do
|
|
11
|
+
it 'extracts basic profile fields' do
|
|
12
|
+
profile = {
|
|
13
|
+
'name' => 'test_profile',
|
|
14
|
+
'display_name' => 'Test Profile',
|
|
15
|
+
'platforms' => ['windows'],
|
|
16
|
+
'shells' => ['powershell'],
|
|
17
|
+
'option_style' => 'single_dash_equals'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
21
|
+
|
|
22
|
+
expect(result[:name]).to eq('test_profile')
|
|
23
|
+
expect(result[:display_name]).to eq('Test Profile')
|
|
24
|
+
expect(result[:platforms]).to eq(['windows'])
|
|
25
|
+
expect(result[:shells]).to eq(['powershell'])
|
|
26
|
+
expect(result[:option_style]).to eq('single_dash_equals')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'extracts inherits field from hash with symbol keys' do
|
|
30
|
+
profile = {
|
|
31
|
+
name: 'windows',
|
|
32
|
+
platforms: ['windows'],
|
|
33
|
+
shells: ['powershell'],
|
|
34
|
+
inherits: 'unix'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
38
|
+
|
|
39
|
+
expect(result[:inherits]).to eq('unix')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'extracts inherits field from hash with string keys' do
|
|
43
|
+
profile = {
|
|
44
|
+
'name' => 'windows',
|
|
45
|
+
'platforms' => ['windows'],
|
|
46
|
+
'shells' => ['powershell'],
|
|
47
|
+
'inherits' => 'unix'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
51
|
+
|
|
52
|
+
expect(result[:inherits]).to eq('unix')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'extracts executable_name field' do
|
|
56
|
+
profile = {
|
|
57
|
+
'name' => 'windows',
|
|
58
|
+
'executable_name' => 'gswin64c'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
62
|
+
|
|
63
|
+
expect(result[:executable_name]).to eq('gswin64c')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns nil for missing inherits field' do
|
|
67
|
+
profile = {
|
|
68
|
+
'name' => 'standalone',
|
|
69
|
+
'platforms' => ['windows']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
73
|
+
|
|
74
|
+
expect(result[:inherits]).to be_nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'with object profile' do
|
|
79
|
+
it 'extracts fields from object with accessors' do
|
|
80
|
+
profile_class = Class.new do
|
|
81
|
+
attr_reader :name, :display_name, :platforms, :shells, :option_style, :inherits, :executable_name
|
|
82
|
+
|
|
83
|
+
def initialize
|
|
84
|
+
@name = 'object_profile'
|
|
85
|
+
@display_name = 'Object Profile'
|
|
86
|
+
@platforms = ['linux']
|
|
87
|
+
@shells = ['bash']
|
|
88
|
+
@option_style = 'double_dash_equals'
|
|
89
|
+
@inherits = 'base'
|
|
90
|
+
@executable_name = 'custom_exe'
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
profile = profile_class.new
|
|
95
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
96
|
+
|
|
97
|
+
expect(result[:name]).to eq('object_profile')
|
|
98
|
+
expect(result[:display_name]).to eq('Object Profile')
|
|
99
|
+
expect(result[:platforms]).to eq(['linux'])
|
|
100
|
+
expect(result[:shells]).to eq(['bash'])
|
|
101
|
+
expect(result[:option_style]).to eq('double_dash_equals')
|
|
102
|
+
expect(result[:inherits]).to eq('base')
|
|
103
|
+
expect(result[:executable_name]).to eq('custom_exe')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'returns nil for inherits when object does not respond to it' do
|
|
107
|
+
profile_class = Class.new do
|
|
108
|
+
attr_reader :name, :display_name, :platforms, :shells, :option_style
|
|
109
|
+
|
|
110
|
+
def initialize
|
|
111
|
+
@name = 'no_inherits'
|
|
112
|
+
@display_name = 'No Inherits'
|
|
113
|
+
@platforms = ['macos']
|
|
114
|
+
@shells = ['zsh']
|
|
115
|
+
@option_style = 'single_dash_space'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
profile = profile_class.new
|
|
120
|
+
result = Ukiryu::Tool::Loader.extract_profile_data(profile)
|
|
121
|
+
|
|
122
|
+
expect(result[:inherits]).to be_nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe 'profile inheritance integration' do
|
|
128
|
+
# This tests the full flow: extract_profile_data -> PlatformProfile -> resolve_inheritance!
|
|
129
|
+
it 'preserves inherits field through the conversion process' do
|
|
130
|
+
Ukiryu::Tool::Loader.singleton_class.send(:public, :extract_profile_data)
|
|
131
|
+
|
|
132
|
+
# Create a minimal test that verifies the inherits field is preserved
|
|
133
|
+
profile_hash = {
|
|
134
|
+
'name' => 'windows',
|
|
135
|
+
'platforms' => ['windows'],
|
|
136
|
+
'shells' => ['powershell'],
|
|
137
|
+
'inherits' => 'unix'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extracted = Ukiryu::Tool::Loader.extract_profile_data(profile_hash)
|
|
141
|
+
|
|
142
|
+
# The inherits field should be preserved
|
|
143
|
+
expect(extracted[:inherits]).to eq('unix')
|
|
144
|
+
|
|
145
|
+
# This would be used by PlatformProfile.resolve_inheritance! to inherit commands
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|