ocran 1.4.6 → 1.5.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.txt +68 -0
  3. data/README.md +295 -10
  4. data/ext/extconf.rb +5 -1
  5. data/lib/ocran/aot_toolchain.rb +154 -0
  6. data/lib/ocran/command_output.rb +8 -1
  7. data/lib/ocran/compatibility_scan.rb +414 -0
  8. data/lib/ocran/cosmo_toolchain.rb +29 -7
  9. data/lib/ocran/dir_builder.rb +88 -38
  10. data/lib/ocran/direction.rb +190 -30
  11. data/lib/ocran/ed_resource.rb +113 -0
  12. data/lib/ocran/ed_resource_builder.rb +199 -0
  13. data/lib/ocran/file_path_set.rb +9 -5
  14. data/lib/ocran/gem_spec_queryable.rb +7 -3
  15. data/lib/ocran/host_config_helper.rb +11 -1
  16. data/lib/ocran/inno_setup_script_builder.rb +24 -14
  17. data/lib/ocran/launcher_batch_builder.rb +8 -11
  18. data/lib/ocran/npm_package.rb +106 -0
  19. data/lib/ocran/option.rb +425 -35
  20. data/lib/ocran/pe_imports.rb +96 -0
  21. data/lib/ocran/pe_resources.rb +226 -0
  22. data/lib/ocran/refine_pathname.rb +29 -32
  23. data/lib/ocran/roundhouse_builder.rb +316 -0
  24. data/lib/ocran/runner.rb +43 -11
  25. data/lib/ocran/runtime_sdk.rb +252 -0
  26. data/lib/ocran/spinel_builder.rb +183 -0
  27. data/lib/ocran/spinel_compatibility.rb +149 -0
  28. data/lib/ocran/static_runtime/boot.rb +443 -0
  29. data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
  30. data/lib/ocran/static_runtime_builder.rb +814 -0
  31. data/lib/ocran/stub_builder.rb +70 -25
  32. data/lib/ocran/version.rb +1 -1
  33. data/lib/ocran/wasm_builder.rb +570 -0
  34. data/lib/ocran/wasm_compatibility.rb +198 -0
  35. data/lib/ocran/windows_command_escaping.rb +21 -0
  36. data/lib/ocran/zip_payload_builder.rb +18 -9
  37. data/lib/ocran/zip_writer.rb +48 -9
  38. data/src/Makefile +18 -2
  39. data/src/Makefile.mswin +65 -0
  40. data/src/inst_dir.c +34 -6
  41. data/src/path_utils.c +144 -0
  42. data/src/script_info.c +5 -0
  43. data/src/stub.c +77 -19
  44. data/src/stub.rc +1 -1
  45. data/src/system_utils.c +276 -155
  46. data/src/system_utils.h +46 -3
  47. data/src/system_utils_posix.c +243 -148
  48. data/src/unpack.c +9 -0
  49. metadata +22 -4
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+ require_relative "compatibility_scan"
3
+
4
+ module Ocran
5
+ # Whether a program is likely to run in the browser on ruby.wasm, CRuby
6
+ # compiled to WebAssembly (--wasm).
7
+ #
8
+ # ruby.wasm is CRuby with its whole standard library, so the language is
9
+ # not the problem; the platform is. WASI has no threads, no processes and
10
+ # no sockets, the browser has no standard input, and a blocking call
11
+ # freezes the page. Gems reach the module only through a Gemfile, which
12
+ # rbwasm packs, cross-compiling C extensions for WASI where it can.
13
+ #
14
+ # All of it fails at run time, when the program gets there, so what is
15
+ # found here is reported as warnings: a program that never takes the path
16
+ # works fine.
17
+ class RubyWasmCompatibility < CompatibilityScan
18
+ # Libraries that load, but whose point is something the browser does not
19
+ # offer.
20
+ UNAVAILABLE = {
21
+ "socket" => "WebAssembly in the browser has no sockets; talk to servers with fetch through the js gem",
22
+ "net/http" => "WebAssembly in the browser has no sockets; use fetch through the js gem (JS.global.fetch(url).await)",
23
+ "net/https" => "WebAssembly in the browser has no sockets; use fetch through the js gem",
24
+ "open-uri" => "WebAssembly in the browser has no sockets; use fetch through the js gem",
25
+ "open3" => "WebAssembly has no processes to run",
26
+ "pty" => "WebAssembly has no processes or terminals",
27
+ "io/console" => "the browser has no console to control",
28
+ "readline" => "the browser has no terminal input",
29
+ "reline" => "the browser has no terminal input",
30
+ }.freeze
31
+
32
+ PROCESS_CALLS = %i[system spawn exec fork].freeze
33
+
34
+ # bundle: the Gem::Specifications of the application's Gemfile.lock, as
35
+ # Bundler resolves them (so a gem installed from a path is among
36
+ # them), or nil when it has no Gemfile
37
+ def initialize(script, bundle: nil, project_dirs: nil)
38
+ super(script, project_dirs: project_dirs)
39
+ @bundle = bundle
40
+ end
41
+
42
+ def target = "ruby.wasm"
43
+
44
+ private
45
+
46
+ def feature_problem(feature)
47
+ reason = UNAVAILABLE[feature] or return nil
48
+ [:warning, reason]
49
+ end
50
+
51
+ # The whole standard library is in ruby.wasm.
52
+ def provided_feature?(feature)
53
+ return true if stdlib_feature?(feature)
54
+
55
+ spec = find_gem(feature)
56
+ spec&.default_gem? || false
57
+ end
58
+
59
+ def missing_stdlib_message(_name) = nil
60
+
61
+ # The bundle's own gems first: a gem Bundler installs from a path or a
62
+ # git checkout is not among the installed gems.
63
+ def find_gem(feature)
64
+ (@bundle || []).find { |spec| spec.full_require_paths.any? { |dir| File.file?(File.join(dir, "#{feature}.rb")) } } ||
65
+ super
66
+ end
67
+
68
+ def gem_problem(gem)
69
+ return nil if gem.stdlib
70
+
71
+ if @bundle.nil?
72
+ [:error, "the application has no Gemfile, and ruby.wasm only contains the gems a Gemfile lists"]
73
+ elsif @bundle.none? { |spec| spec.name == gem.name }
74
+ [:error, "not in the application's Gemfile.lock, and ruby.wasm only contains the gems it lists"]
75
+ elsif gem.native
76
+ [:warning, "has a C extension, which rbwasm cross-compiles for WASI; extensions that link system " \
77
+ "libraries (libxml2, sqlite, openssl, ...) usually fail to build"]
78
+ end
79
+ end
80
+
81
+ def check_call(node, name, args, receiver, kernel)
82
+ case name
83
+ when *PROCESS_CALLS
84
+ if kernel || constant?(receiver, :Process)
85
+ flag(:warning, node, "#{name}: WebAssembly has no processes to run")
86
+ end
87
+ when :popen
88
+ flag(:warning, node, "IO.popen: WebAssembly has no processes to run") if constant?(receiver, :IO)
89
+ when :new, :start, :fork
90
+ if constant?(receiver, :Thread)
91
+ flag(:warning, node, "Thread.#{name}: ruby.wasm has no threads (WASI provides none); " \
92
+ "use Fiber, or JS promises with #await")
93
+ end
94
+ when :gets, :readline, :readlines
95
+ if kernel || constant?(receiver, :STDIN)
96
+ flag(:warning, node, "#{name}: the browser has no standard input; read from the page through the js gem")
97
+ end
98
+ when :sleep
99
+ if kernel && !args.empty?
100
+ flag(:warning, node, "sleep blocks the browser's only thread and freezes the page while it waits")
101
+ end
102
+ when :trap
103
+ flag(:warning, node, "trap: there are no signals in the browser") if kernel || constant?(receiver, :Signal)
104
+ end
105
+ end
106
+
107
+ def check_global(node)
108
+ return unless node.name == :$stdin
109
+
110
+ flag(:warning, node, "$stdin: the browser has no standard input")
111
+ end
112
+
113
+ def check_xstring(node)
114
+ flag(:warning, node, "backticks: WebAssembly has no processes to run")
115
+ end
116
+ end
117
+
118
+ # Whether a program is likely to run in the browser on PicoRuby
119
+ # (--wasm=picoruby), the mruby-based Ruby for microcontrollers and
120
+ # WebAssembly.
121
+ #
122
+ # PicoRuby implements the core language but not CRuby's standard library
123
+ # or RubyGems: `require` finds only the libraries compiled into the
124
+ # runtime and files on its own virtual file system, which in the browser
125
+ # holds nothing of the application. OCRAN therefore bundles the
126
+ # program's own files into one script, and anything else it requires
127
+ # has to be one of PicoRuby's built-in libraries.
128
+ class PicoRubyCompatibility < CompatibilityScan
129
+ # Libraries compiled into @picoruby/wasm-wasi, by require name: each
130
+ # picoruby-<name> gem in build_config/picoruby-wasm.rb and its stdlib
131
+ # gembox registers as <name>. PicoRuby's require answers nothing else
132
+ # from the runtime - not even `require "enumerator"` - while the mruby
133
+ # core gems (String/Array/Hash extensions, Math, IO, Dir, Task, eval,
134
+ # ObjectSpace) are loaded from the start and need no require at all.
135
+ FEATURES = %w[
136
+ js json yaml base64 base16 rng marshal data
137
+ indexeddb funicular markdown drb sqlite3 dfu
138
+ ].freeze
139
+
140
+ def target = "PicoRuby"
141
+
142
+ private
143
+
144
+ def provided_feature?(feature) = FEATURES.include?(feature)
145
+
146
+ def gem_problem(gem)
147
+ if gem.stdlib
148
+ [:error, "Ruby standard library that PicoRuby does not have"]
149
+ else
150
+ [:error, "PicoRuby cannot load RubyGems gems; only its built-in libraries (#{FEATURES.first(8).join(", ")}, ...)"]
151
+ end
152
+ end
153
+
154
+ def missing_stdlib_message(name)
155
+ "require #{name.inspect}: part of CRuby's standard library, which PicoRuby does not have"
156
+ end
157
+
158
+ def dynamic_require_message(name)
159
+ "#{name} with a computed name: OCRAN bundles the program's files ahead of time, so the name must be a string literal"
160
+ end
161
+
162
+ def check_call(node, name, args, receiver, kernel)
163
+ case name
164
+ when :system, :spawn, :exec, :fork
165
+ flag(:error, node, "#{name}: there are no processes in the browser") if kernel || constant?(receiver, :Process)
166
+ when :new, :start
167
+ if constant?(receiver, :Thread)
168
+ flag(:error, node, "Thread.#{name}: PicoRuby has no Thread; use Task, its cooperative tasks")
169
+ end
170
+ when :autoload
171
+ flag(:error, node, "autoload is not supported by PicoRuby; require the file instead") if kernel || receiver
172
+ when :refine, :using
173
+ if receiver.nil? && args.size == 1 && (node.block || name == :using)
174
+ flag(:error, node, "refinements (refine / using) are not supported by PicoRuby")
175
+ end
176
+ when :load
177
+ flag(:error, node, "load: the application's files are not on PicoRuby's file system in the browser") if kernel && !args.empty?
178
+ when :gets, :readline
179
+ flag(:warning, node, "#{name}: the browser has no standard input") if kernel || constant?(receiver, :STDIN)
180
+ when :force_encoding, :encode
181
+ flag(:warning, node, "#{name}: PicoRuby strings are UTF-8 only, without CRuby's encodings")
182
+ end
183
+ end
184
+
185
+ def check_constant(node)
186
+ case node.name
187
+ when :Ractor
188
+ flag(:error, node, "Ractor is not supported by PicoRuby")
189
+ when :Encoding
190
+ flag(:warning, node, "Encoding: PicoRuby strings are UTF-8 only, without CRuby's encodings")
191
+ end
192
+ end
193
+
194
+ def check_xstring(node)
195
+ flag(:error, node, "backticks: there are no processes in the browser")
196
+ end
197
+ end
198
+ end
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
+ require "pathname"
3
+ require_relative "build_constants"
2
4
 
3
5
  module Ocran
4
6
  module WindowsCommandEscaping
7
+ # The extraction root placeholder (see BuildConstants) at the start of a
8
+ # packed path, with either separator.
9
+ EXTRACT_ROOT_PREFIX = %r{#{Regexp.escape(BuildConstants::EXTRACT_ROOT.to_s)}[/\\]}
10
+
5
11
  module_function
6
12
 
7
13
  def escape_double_quotes(s)
@@ -11,5 +17,20 @@ module Ocran
11
17
  def quote_and_escape(s)
12
18
  "\"#{escape_double_quotes(s)}\""
13
19
  end
20
+
21
+ # Doubles percent signs so that cmd.exe does not expand text between
22
+ # them as a variable reference when the string is part of a batch file.
23
+ def escape_percent(s)
24
+ s.to_s.gsub("%", "%%")
25
+ end
26
+
27
+ # Renders a build-time value for a batch file, for use inside double
28
+ # quotes: percent signs are escaped so cmd.exe takes the value literally,
29
+ # then the extraction root placeholder becomes +root+, a batch
30
+ # expression for the application directory that ends in a backslash
31
+ # (e.g. "%~dp0").
32
+ def batch_value(s, root)
33
+ escape_percent(s).gsub(EXTRACT_ROOT_PREFIX) { root }
34
+ end
14
35
  end
15
36
  end
@@ -43,10 +43,11 @@ module Ocran
43
43
  # Uncompressed size of everything packed, for the build summary.
44
44
  attr_reader :data_size
45
45
 
46
- def initialize(path, cosmo_ruby:, chdir_before: false, debug_mode: false)
46
+ def initialize(path, cosmo_ruby:, chdir_before: false, chdir_exe_dir: false, debug_mode: false)
47
47
  @path = Pathname(path)
48
48
  @cosmo_ruby = Pathname(cosmo_ruby)
49
49
  @chdir_before = chdir_before
50
+ @chdir_exe_dir = chdir_exe_dir
50
51
  @debug_mode = debug_mode
51
52
  @entries = []
52
53
  @names = {}
@@ -200,15 +201,23 @@ module Ocran
200
201
  # here: the application lives inside the archive and zipos cannot be a
201
202
  # working directory. The directory holding the executable is the
202
203
  # closest equivalent, and the one an application that keeps data next
203
- # to itself actually wants.
204
+ # to itself actually wants. That is exactly what --chdir-exe-dir asks
205
+ # for, so both options do the same in this mode.
204
206
  def chdir_source
205
- return "" unless @chdir_before
206
-
207
- <<~RUBY.chomp
208
- # --chdir-first: the archive cannot be a working directory, so use
209
- # the directory the executable was started from.
210
- Dir.chdir(File.dirname(executable))
211
- RUBY
207
+ if @chdir_exe_dir
208
+ <<~RUBY.chomp
209
+ # --chdir-exe-dir: start in the directory holding the executable.
210
+ Dir.chdir(File.dirname(executable))
211
+ RUBY
212
+ elsif @chdir_before
213
+ <<~RUBY.chomp
214
+ # --chdir-first: the archive cannot be a working directory, so use
215
+ # the directory the executable was started from.
216
+ Dir.chdir(File.dirname(executable))
217
+ RUBY
218
+ else
219
+ ""
220
+ end
212
221
  end
213
222
 
214
223
  # Environment variables other than the load-path ones, exported as is.
@@ -41,6 +41,11 @@ module Ocran
41
41
  # Markers of the ZIP64 format extensions. OCRAN never writes them; an
42
42
  # input archive that uses them is rejected rather than corrupted.
43
43
  ZIP64_EOCD_LOCATOR_SIGNATURE = "PK\x06\x07".b
44
+ ZIP64_EOCD_LOCATOR_SIZE = 20
45
+
46
+ # Limits of the format without ZIP64.
47
+ MAX_ENTRIES = 0xffff
48
+ MAX_OFFSET = 0xffffffff
44
49
 
45
50
  CENTRAL_SIGNATURE = "PK\x01\x02".b
46
51
  LOCAL_SIGNATURE = "PK\x03\x04".b
@@ -87,6 +92,12 @@ module Ocran
87
92
 
88
93
  (data || File.binread(source)).b
89
94
  end
95
+
96
+ def content_size
97
+ return 0 if directory?
98
+
99
+ data ? data.bytesize : File.size(source)
100
+ end
90
101
  end
91
102
 
92
103
  module_function
@@ -115,6 +126,7 @@ module Ocran
115
126
  end
116
127
 
117
128
  entries = with_parent_directories(entries, existing)
129
+ check_limits(eocd, central, entries)
118
130
 
119
131
  before = io.size
120
132
  io.truncate(eocd[:cd_offset])
@@ -147,10 +159,31 @@ module Ocran
147
159
  }
148
160
  missing = parents.reject { |name| seen[name] }
149
161
  missing.each { |name| seen[name] = true }
150
- missing.map { |name| Entry.new(name: name) } << entry
162
+ # Dated like the entry, so that packing stays reproducible.
163
+ missing.map { |name| Entry.new(name: name, mtime: entry.mtime) } << entry
151
164
  }
152
165
  end
153
166
 
167
+ # Raises when the archive would outgrow the format without ZIP64 -
168
+ # before anything is written, so that a refused append leaves the file
169
+ # as it was. The size is an upper bound: an entry is never stored larger
170
+ # than its content (see compress).
171
+ def check_limits(eocd, central, entries)
172
+ total_entries = eocd[:total_entries] + entries.size
173
+ if total_entries > MAX_ENTRIES
174
+ raise "too many ZIP entries (#{total_entries}); OCRAN does not write ZIP64 archives"
175
+ end
176
+
177
+ size = eocd[:cd_offset] + central.bytesize + EOCD_SIZE
178
+ entries.each do |entry|
179
+ name = entry.name.bytesize
180
+ size += 30 + name + 46 + name + entry.content_size
181
+ end
182
+ if size > MAX_OFFSET
183
+ raise "the packaged archive would exceed 4 GiB; OCRAN does not write ZIP64 archives"
184
+ end
185
+ end
186
+
154
187
  # Locates and decodes the end-of-central-directory record. The record
155
188
  # is searched for from the end of the file because a ZIP archive is
156
189
  # identified by its tail, which is what allows one to be appended to an
@@ -167,10 +200,6 @@ module Ocran
167
200
  "it cannot be a cosmopolitan APE with an embedded ZIP store"
168
201
  end
169
202
 
170
- if tail.rindex(ZIP64_EOCD_LOCATOR_SIGNATURE)
171
- raise "#{path} uses the ZIP64 format extensions, which OCRAN cannot append to"
172
- end
173
-
174
203
  _signature, _disk, _cd_disk, _disk_entries, total_entries, cd_size, cd_offset, comment_length =
175
204
  tail.byteslice(offset, EOCD_SIZE).unpack("a4vvvvVVv")
176
205
 
@@ -179,6 +208,14 @@ module Ocran
179
208
  raise "#{path} has trailing data after its ZIP archive; OCRAN cannot append to it"
180
209
  end
181
210
 
211
+ # A ZIP64 archive has its end-of-central-directory locator right
212
+ # before this record. Only that position counts: the same four bytes
213
+ # anywhere else in the tail are file data or names.
214
+ locator_offset = offset - ZIP64_EOCD_LOCATOR_SIZE
215
+ if locator_offset >= 0 && tail.byteslice(locator_offset, 4) == ZIP64_EOCD_LOCATOR_SIGNATURE
216
+ raise "#{path} uses the ZIP64 format extensions, which OCRAN cannot append to"
217
+ end
218
+
182
219
  { cd_offset: cd_offset, cd_size: cd_size, total_entries: total_entries }
183
220
  end
184
221
 
@@ -264,10 +301,10 @@ module Ocran
264
301
  end
265
302
 
266
303
  def end_of_central_directory(total_entries, cd_size, cd_offset)
267
- if total_entries > 0xffff
304
+ if total_entries > MAX_ENTRIES
268
305
  raise "too many ZIP entries (#{total_entries}); OCRAN does not write ZIP64 archives"
269
306
  end
270
- if cd_offset + cd_size > 0xffffffff
307
+ if cd_offset + cd_size > MAX_OFFSET
271
308
  raise "the packaged archive would exceed 4 GiB; OCRAN does not write ZIP64 archives"
272
309
  end
273
310
 
@@ -276,9 +313,11 @@ module Ocran
276
313
  end
277
314
 
278
315
  # MS-DOS packed time and date. The format has two-second resolution and
279
- # starts in 1980, so earlier timestamps are clamped.
316
+ # starts in 1980, so earlier timestamps are clamped. It has no time zone:
317
+ # file times are written as local time, as zip tools do, and a UTC time
318
+ # as given, so that a fixed time packs to the same bytes everywhere.
280
319
  def dos_timestamp(time)
281
- time = time.getlocal
320
+ time = time.getlocal unless time.utc? || time.utc_offset.zero?
282
321
  year = [time.year, 1980].max
283
322
  [(time.hour << 11) | (time.min << 5) | (time.sec / 2),
284
323
  ((year - 1980) << 9) | (time.month << 5) | time.day]
data/src/Makefile CHANGED
@@ -8,6 +8,7 @@ UNAME := $(shell uname -s 2>/dev/null || echo Windows)
8
8
  IS_POSIX := $(filter $(UNAME),Linux Darwin)
9
9
 
10
10
  CC := gcc
11
+ WINDRES ?= windres
11
12
  CFLAGS := -Wall -O2 -DWITH_LZMA -Ilzma
12
13
  LDFLAGS := -s
13
14
 
@@ -21,6 +22,9 @@ ifneq ($(IS_POSIX),)
21
22
  RESOURCE_OBJ :=
22
23
  else
23
24
  EXEEXT := .exe
25
+ # No build time in the PE header, so that the stubs (and the platform
26
+ # gem that ships them) build reproducibly.
27
+ LDFLAGS += -Wl,--no-insert-timestamp
24
28
  LDLIBS := -lbcrypt
25
29
  GUI_LDFLAGS := -mwindows
26
30
  STUB_CFLAGS := $(CFLAGS) -D_CONSOLE
@@ -36,7 +40,7 @@ BINARIES := $(addsuffix $(EXEEXT), $(PROG_NAMES))
36
40
  LZMA_SRCS := lzma/LzmaDec.c
37
41
  LZMA_OBJS := $(LZMA_SRCS:.c=.o)
38
42
 
39
- COMMON_SRCS := $(SYSTEM_UTILS_SRC) inst_dir.c script_info.c unpack.c
43
+ COMMON_SRCS := $(SYSTEM_UTILS_SRC) path_utils.c inst_dir.c script_info.c unpack.c
40
44
  COMMON_OBJS := $(COMMON_SRCS:.c=.o) $(LZMA_OBJS) $(RESOURCE_OBJ)
41
45
 
42
46
  VARIANT_SRCS := stub.c error.c
@@ -46,6 +50,18 @@ WINDOW_OBJS := $(VARIANT_SRCS:.c=_window.o)
46
50
  .PHONY: all clean install
47
51
  all: $(BINARIES)
48
52
 
53
+ # Header dependencies: every object depends on every header it could
54
+ # include. Coarse, but it needs no compiler support (gcc, mingw and cosmocc
55
+ # all build from this Makefile), leaves no generated files behind, and
56
+ # cannot go stale as includes change. Rebuilding everything takes seconds.
57
+ HEADERS := $(wildcard *.h)
58
+ LZMA_HEADERS := $(wildcard lzma/*.h)
59
+
60
+ $(filter-out $(LZMA_OBJS) $(RESOURCE_OBJ), $(COMMON_OBJS)) \
61
+ $(CONSOLE_OBJS) $(WINDOW_OBJS): $(HEADERS) $(LZMA_HEADERS)
62
+
63
+ $(LZMA_OBJS): $(LZMA_HEADERS)
64
+
49
65
  %.o: %.c
50
66
  $(CC) $(CFLAGS) -c $< -o $@
51
67
 
@@ -57,7 +73,7 @@ all: $(BINARIES)
57
73
 
58
74
  ifeq ($(IS_POSIX),)
59
75
  stub.res: stub.rc
60
- windres -i stub.rc -O coff -o stub.res
76
+ $(WINDRES) -i stub.rc -O coff -o stub.res
61
77
  endif
62
78
 
63
79
  stub$(EXEEXT): $(COMMON_OBJS) $(CONSOLE_OBJS)
@@ -0,0 +1,65 @@
1
+ # NMAKE makefile for building the Windows stubs with Microsoft Visual C++
2
+ # (for mswin Ruby, which has no MinGW/MSYS2 toolchain). Run it from a
3
+ # Visual Studio developer prompt (vcvars64.bat), in this directory:
4
+ #
5
+ # nmake -f Makefile.mswin [install|clean]
6
+ #
7
+ # The stubs link the C runtime statically (/MT): they run before anything
8
+ # is extracted, so they must not depend on vcruntime140.dll themselves.
9
+
10
+ CC = cl
11
+ RC = rc
12
+ CFLAGS = -nologo -W3 -O2 -MT -DWITH_LZMA -D_CRT_SECURE_NO_WARNINGS -Ilzma
13
+ LDFLAGS = -nologo -release -manifest:no
14
+ LDLIBS = bcrypt.lib shell32.lib user32.lib advapi32.lib
15
+
16
+ BINDIR = ..\share\ocran
17
+
18
+ COMMON_OBJS = system_utils.obj path_utils.obj inst_dir.obj script_info.obj \
19
+ unpack.obj lzma\LzmaDec.obj stub.res
20
+ CONSOLE_OBJS = stub_console.obj error_console.obj
21
+ WINDOW_OBJS = stub_window.obj error_window.obj
22
+
23
+ all: stub.exe stubw.exe
24
+
25
+ # Coarse header dependencies, as in Makefile
26
+ HEADERS = error.h inst_dir.h script_info.h system_utils.h unpack.h \
27
+ lzma\LzmaDec.h lzma\7zTypes.h lzma\Compiler.h
28
+
29
+ .c.obj:
30
+ $(CC) $(CFLAGS) -c $< -Fo$@
31
+
32
+ lzma\LzmaDec.obj: lzma\LzmaDec.c $(HEADERS)
33
+ $(CC) $(CFLAGS) -c lzma\LzmaDec.c -Fo$@
34
+
35
+ system_utils.obj path_utils.obj inst_dir.obj script_info.obj unpack.obj: $(HEADERS)
36
+
37
+ stub_console.obj: stub.c $(HEADERS)
38
+ $(CC) $(CFLAGS) -D_CONSOLE -c stub.c -Fo$@
39
+
40
+ error_console.obj: error.c $(HEADERS)
41
+ $(CC) $(CFLAGS) -D_CONSOLE -c error.c -Fo$@
42
+
43
+ stub_window.obj: stub.c $(HEADERS)
44
+ $(CC) $(CFLAGS) -c stub.c -Fo$@
45
+
46
+ error_window.obj: error.c $(HEADERS)
47
+ $(CC) $(CFLAGS) -c error.c -Fo$@
48
+
49
+ stub.res: stub.rc stub.manifest vit-ruby.ico
50
+ $(RC) -nologo -fo stub.res stub.rc
51
+
52
+ stub.exe: $(COMMON_OBJS) $(CONSOLE_OBJS)
53
+ link $(LDFLAGS) -subsystem:console -out:$@ $** $(LDLIBS)
54
+
55
+ # -mwindows equivalent: GUI subsystem, but keep main() as the entry point
56
+ stubw.exe: $(COMMON_OBJS) $(WINDOW_OBJS)
57
+ link $(LDFLAGS) -subsystem:windows -entry:mainCRTStartup -out:$@ $** $(LDLIBS)
58
+
59
+ install: all
60
+ if not exist $(BINDIR) mkdir $(BINDIR)
61
+ copy /y stub.exe $(BINDIR)\stub.exe >nul
62
+ copy /y stubw.exe $(BINDIR)\stubw.exe >nul
63
+
64
+ clean:
65
+ -del /q stub.exe stubw.exe *.obj lzma\*.obj stub.res 2>nul
data/src/inst_dir.c CHANGED
@@ -99,14 +99,17 @@ const char *CreateInstDir(bool is_extract_to_exe_dir)
99
99
  }
100
100
 
101
101
  /* Normalize 8.3 short names (e.g. a TEMP under C:\Users\RUNNER~1) so all
102
- paths derived from the extraction dir use one consistent spelling. */
102
+ paths derived from the extraction dir use one consistent spelling.
103
+ The directory exists by now: should that fail, keep the spelling at
104
+ hand, so that the directory is still known and gets deleted. */
103
105
  char *long_dir = ToLongPath(inst_dir);
104
- free(inst_dir);
105
- if (!long_dir) {
106
- return NULL;
106
+ if (long_dir) {
107
+ free(inst_dir);
108
+ InstDir = long_dir;
109
+ } else {
110
+ APP_ERROR("Failed to normalize the extraction directory path");
111
+ InstDir = inst_dir;
107
112
  }
108
-
109
- InstDir = long_dir;
110
113
  return InstDir;
111
114
  }
112
115
 
@@ -310,6 +313,25 @@ cleanup:
310
313
  }
311
314
 
312
315
  #ifndef _WIN32
316
+ /* Whether a symlink target names a sibling of the link: a single clean
317
+ path segment. That is all the packer emits (shared library aliases like
318
+ libruby.so -> libruby.so.3.4.1 in bin), and it keeps every link, and
319
+ every path running through one, inside the installation directory. An
320
+ absolute target, or one climbing out with "..", would let the files
321
+ extracted after it land anywhere. */
322
+ static bool is_sibling_name(const char *name)
323
+ {
324
+ if (!IsCleanRelativePath(name)) {
325
+ return false;
326
+ }
327
+ for (const char *p = name; *p; p++) {
328
+ if (is_path_separator(*p)) {
329
+ return false;
330
+ }
331
+ }
332
+ return true;
333
+ }
334
+
313
335
  bool CreateSymlinkUnderInstDir(const char *rel_link_path, const char *target)
314
336
  {
315
337
  if (!IsInstDirSet()) {
@@ -327,6 +349,12 @@ bool CreateSymlinkUnderInstDir(const char *rel_link_path, const char *target)
327
349
  return false;
328
350
  }
329
351
 
352
+ if (!is_sibling_name(target)) {
353
+ APP_ERROR("symlink target '%s' of '%s' is not a file name in the same "
354
+ "directory", target, rel_link_path);
355
+ return false;
356
+ }
357
+
330
358
  char *link_path = ExpandInstDirPath(rel_link_path);
331
359
  if (!link_path) {
332
360
  return false;