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.
- checksums.yaml +4 -4
- data/CHANGELOG.txt +68 -0
- data/README.md +295 -10
- data/ext/extconf.rb +5 -1
- data/lib/ocran/aot_toolchain.rb +154 -0
- data/lib/ocran/command_output.rb +8 -1
- data/lib/ocran/compatibility_scan.rb +414 -0
- data/lib/ocran/cosmo_toolchain.rb +29 -7
- data/lib/ocran/dir_builder.rb +88 -38
- data/lib/ocran/direction.rb +190 -30
- data/lib/ocran/ed_resource.rb +113 -0
- data/lib/ocran/ed_resource_builder.rb +199 -0
- data/lib/ocran/file_path_set.rb +9 -5
- data/lib/ocran/gem_spec_queryable.rb +7 -3
- data/lib/ocran/host_config_helper.rb +11 -1
- data/lib/ocran/inno_setup_script_builder.rb +24 -14
- data/lib/ocran/launcher_batch_builder.rb +8 -11
- data/lib/ocran/npm_package.rb +106 -0
- data/lib/ocran/option.rb +425 -35
- data/lib/ocran/pe_imports.rb +96 -0
- data/lib/ocran/pe_resources.rb +226 -0
- data/lib/ocran/refine_pathname.rb +29 -32
- data/lib/ocran/roundhouse_builder.rb +316 -0
- data/lib/ocran/runner.rb +43 -11
- data/lib/ocran/runtime_sdk.rb +252 -0
- data/lib/ocran/spinel_builder.rb +183 -0
- data/lib/ocran/spinel_compatibility.rb +149 -0
- data/lib/ocran/static_runtime/boot.rb +443 -0
- data/lib/ocran/static_runtime/build_static_ext.rb +86 -0
- data/lib/ocran/static_runtime_builder.rb +814 -0
- data/lib/ocran/stub_builder.rb +70 -25
- data/lib/ocran/version.rb +1 -1
- data/lib/ocran/wasm_builder.rb +570 -0
- data/lib/ocran/wasm_compatibility.rb +198 -0
- data/lib/ocran/windows_command_escaping.rb +21 -0
- data/lib/ocran/zip_payload_builder.rb +18 -9
- data/lib/ocran/zip_writer.rb +48 -9
- data/src/Makefile +18 -2
- data/src/Makefile.mswin +65 -0
- data/src/inst_dir.c +34 -6
- data/src/path_utils.c +144 -0
- data/src/script_info.c +5 -0
- data/src/stub.c +77 -19
- data/src/stub.rc +1 -1
- data/src/system_utils.c +276 -155
- data/src/system_utils.h +46 -3
- data/src/system_utils_posix.c +243 -148
- data/src/unpack.c +9 -0
- 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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
data/lib/ocran/zip_writer.rb
CHANGED
|
@@ -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
|
-
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
76
|
+
$(WINDRES) -i stub.rc -O coff -o stub.res
|
|
61
77
|
endif
|
|
62
78
|
|
|
63
79
|
stub$(EXEEXT): $(COMMON_OBJS) $(CONSOLE_OBJS)
|
data/src/Makefile.mswin
ADDED
|
@@ -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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|