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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ukiryu/cache.rb +6 -0
  3. data/lib/ukiryu/cache_registry.rb +64 -0
  4. data/lib/ukiryu/cli_commands/base_command.rb +6 -5
  5. data/lib/ukiryu/cli_commands/config_command.rb +7 -10
  6. data/lib/ukiryu/cli_commands/register_command.rb +27 -18
  7. data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
  8. data/lib/ukiryu/command_builder.rb +83 -50
  9. data/lib/ukiryu/config.rb +13 -2
  10. data/lib/ukiryu/debug.rb +20 -9
  11. data/lib/ukiryu/definition/loader.rb +3 -3
  12. data/lib/ukiryu/errors.rb +37 -37
  13. data/lib/ukiryu/executable_locator.rb +40 -16
  14. data/lib/ukiryu/extractors/base_extractor.rb +2 -1
  15. data/lib/ukiryu/extractors/help_parser.rb +3 -0
  16. data/lib/ukiryu/logger.rb +51 -0
  17. data/lib/ukiryu/models/implementation_index.rb +2 -1
  18. data/lib/ukiryu/models/implementation_version.rb +18 -1
  19. data/lib/ukiryu/models/interface.rb +2 -1
  20. data/lib/ukiryu/models/run_environment.rb +0 -2
  21. data/lib/ukiryu/models/semantic_version.rb +174 -0
  22. data/lib/ukiryu/models/stage_metrics.rb +0 -1
  23. data/lib/ukiryu/register.rb +473 -232
  24. data/lib/ukiryu/shell/powershell.rb +209 -89
  25. data/lib/ukiryu/shell/sh.rb +4 -1
  26. data/lib/ukiryu/shell.rb +60 -2
  27. data/lib/ukiryu/tool/command_resolution.rb +2 -1
  28. data/lib/ukiryu/tool/executable_discovery.rb +14 -15
  29. data/lib/ukiryu/tool/loader.rb +543 -0
  30. data/lib/ukiryu/tool/version_detection.rb +1 -3
  31. data/lib/ukiryu/tool.rb +79 -87
  32. data/lib/ukiryu/tool_index.rb +127 -62
  33. data/lib/ukiryu/tools/base.rb +4 -2
  34. data/lib/ukiryu/type.rb +26 -15
  35. data/lib/ukiryu/version.rb +1 -1
  36. data/lib/ukiryu.rb +1 -1
  37. data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
  38. data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
  39. data/spec/spec_helper.rb +10 -6
  40. data/spec/support/tool_helper.rb +2 -0
  41. data/spec/ukiryu/definition/loader_spec.rb +2 -2
  42. data/spec/ukiryu/executor_spec.rb +6 -3
  43. data/spec/ukiryu/models/execution_report_spec.rb +3 -2
  44. data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
  45. data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
  46. data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
  47. data/spec/ukiryu/shell/powershell_spec.rb +286 -51
  48. data/spec/ukiryu/tool/loader_spec.rb +148 -0
  49. data/spec/ukiryu/tool_index_spec.rb +110 -18
  50. data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
  51. data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
  52. data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
  53. metadata +14 -2
  54. 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 backticks' do
16
- expect(shell.escape('hello`world')).to eq('hello``world')
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 special characters' do
32
- expect(shell.escape('`$')).to eq('```$')
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 that do not need escaping' do
36
- expect(shell.escape('hello-world')).to eq('hello-world')
37
- expect(shell.escape('hello_world')).to eq('hello_world')
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 'escapes semicolons to prevent command chaining' do
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 'escapes backticks to prevent command substitution' do
46
- expect(shell.escape('`malicious`')).to eq('``malicious``')
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 'escapes dollar signs to prevent variable expansion' do
50
- expect(shell.escape('$PATH')).to eq('`$PATH')
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 single quotes for arguments' do
58
- expect(shell.quote('hello')).to eq("'hello'")
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 'escapes special characters within single quotes' do
62
- # Single quotes don't need escaping in PowerShell (single-quoted strings are literal)
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("'hello world'")
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 'handles strings with special characters' do
76
- expect(shell.quote('$VAR')).to eq("'`$VAR'")
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 'still escapes special characters within double quotes' do
88
- # When for_exe: true, the implementation just wraps in double quotes without escaping
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
- # The dollar sign in $(...) is escaped, but parentheses are not special in single-quoted strings
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("'`$(malicious)'")
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("'arg1; malicious'")
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 \'hello world\' test')
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 special characters' do
162
- # $VAR doesn't need quoting in PowerShell (unquoted strings are literal)
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 $VAR')
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 \'arg with spaces\'')
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 \'arg with spaces\'')
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 \'hello; malicious\'')
235
+ expect(result).to eq('echo "hello; malicious"')
203
236
  end
204
237
 
205
- it 'handles special characters in arguments' do
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 \'``malicious``\'')
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