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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ocran
4
+ # Reads the names of the DLLs a PE image (exe, dll, or a Ruby extension
5
+ # .so on Windows) imports, from its import and delay-load import tables.
6
+ # Pure Ruby, so it also works where the image cannot be loaded.
7
+ module PEImports
8
+ IMPORT_DIRECTORY = 1
9
+ DELAY_IMPORT_DIRECTORY = 13
10
+
11
+ module_function
12
+
13
+ # Returns the imported DLL names in table order, or [] for a file that
14
+ # is not a well-formed PE image.
15
+ def dll_names(path)
16
+ Image.new(File.binread(path)).dll_names
17
+ rescue SystemCallError, RangeError, ArgumentError
18
+ []
19
+ end
20
+
21
+ class Image
22
+ def initialize(data)
23
+ @data = data
24
+ end
25
+
26
+ def dll_names
27
+ return [] unless @data.byteslice(0, 2) == "MZ"
28
+
29
+ pe = u32(0x3c)
30
+ return [] unless @data.byteslice(pe, 4) == "PE\0\0"
31
+
32
+ section_count = u16(pe + 6)
33
+ optional_header = pe + 24
34
+ optional_header_size = u16(pe + 20)
35
+ directories, directory_count =
36
+ case u16(optional_header)
37
+ when 0x10b then [optional_header + 96, u32(optional_header + 92)] # PE32
38
+ when 0x20b then [optional_header + 112, u32(optional_header + 108)] # PE32+
39
+ else return []
40
+ end
41
+
42
+ @sections = Array.new(section_count) do |i|
43
+ s = optional_header + optional_header_size + i * 40
44
+ { va: u32(s + 12), size: [u32(s + 8), u32(s + 16)].max, raw: u32(s + 20) }
45
+ end
46
+
47
+ names = []
48
+ if directory_count > IMPORT_DIRECTORY
49
+ # IMAGE_IMPORT_DESCRIPTOR: 20 bytes, the DLL name RVA at +12
50
+ each_descriptor(directories + IMPORT_DIRECTORY * 8, 20, 12) { |name| names << name }
51
+ end
52
+ if directory_count > DELAY_IMPORT_DIRECTORY
53
+ # IMAGE_DELAYLOAD_DESCRIPTOR: 32 bytes, the DLL name RVA at +4
54
+ each_descriptor(directories + DELAY_IMPORT_DIRECTORY * 8, 32, 4) { |name| names << name }
55
+ end
56
+ names.uniq(&:downcase)
57
+ end
58
+
59
+ private
60
+
61
+ def each_descriptor(directory, size, name_field)
62
+ table = offset_of(u32(directory))
63
+ return unless table
64
+
65
+ # Never past the table's declared size, in case a malformed image
66
+ # lacks the terminating all-zero descriptor.
67
+ (u32(directory + 4) / size).times do
68
+ name_rva = u32(table + name_field)
69
+ break if name_rva.zero?
70
+
71
+ offset = offset_of(name_rva)
72
+ break unless offset
73
+
74
+ name = @data.byteslice(offset, 260)&.unpack1("Z*")
75
+ yield name if name && !name.empty?
76
+ table += size
77
+ end
78
+ end
79
+
80
+ def offset_of(rva)
81
+ return nil if rva.zero?
82
+
83
+ section = @sections.find { |s| rva >= s[:va] && rva < s[:va] + s[:size] }
84
+ section && rva - section[:va] + section[:raw]
85
+ end
86
+
87
+ def u16(offset)
88
+ @data.byteslice(offset, 2)&.unpack1("v") or raise RangeError
89
+ end
90
+
91
+ def u32(offset)
92
+ @data.byteslice(offset, 4)&.unpack1("V") or raise RangeError
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+ require_relative "ed_resource_builder"
3
+
4
+ module Ocran
5
+ # Reads and rewrites the resources of a PE executable in pure Ruby, so that
6
+ # icons and version information can be set on any build host - the
7
+ # Windows API OCRAN uses for the stub (BeginUpdateResource) exists only on
8
+ # Windows. Used for --static-runtime, whose executable is a copy of the runtime.
9
+ #
10
+ # The resource tree is a Hash: type => { name => { language => data } },
11
+ # where types and names are Integers (IDs) or Strings. #write puts the
12
+ # rebuilt tree into a new section at the end of the image and points the
13
+ # resource data directory at it; the old resource section stays where it
14
+ # is, unused (renamed .rsrc_o), so no other section has to move.
15
+ class PeResources
16
+ RT_ICON = 3
17
+ RT_STRING = 6
18
+ RT_GROUP_ICON = 14
19
+ RT_VERSION = 16
20
+ RT_MANIFEST = 24
21
+ SECTION_CHARACTERISTICS = 0x40000040 # initialized data, readable
22
+
23
+ attr_reader :tree
24
+
25
+ def initialize(bytes)
26
+ @bytes = bytes.b
27
+ parse_headers
28
+ @tree = read_tree
29
+ end
30
+
31
+ def self.update(path)
32
+ pe = new(File.binread(path))
33
+ yield pe
34
+ File.binwrite(path, pe.to_bytes)
35
+ end
36
+
37
+ # Replaces the icon(s) with the images of an .ico file.
38
+ def set_icon(ico_data)
39
+ ico = ico_data.b
40
+ reserved, type, count = ico.unpack("vvv")
41
+ raise "not an icon file" unless reserved.zero? && type == 1 && count.positive?
42
+
43
+ icons = {}
44
+ group = [0, 1, count].pack("vvv")
45
+ count.times do |i|
46
+ w, h, colors, res, planes, bits, size, offset = ico.unpack("CCCCvvVV", offset: 6 + i * 16)
47
+ icons[i + 1] = { EdResource::LANGID => ico.byteslice(offset, size) }
48
+ group << [w, h, colors, res, planes, bits, size, i + 1].pack("CCCCvvVv")
49
+ end
50
+ @tree[RT_ICON] = icons
51
+ @tree[RT_GROUP_ICON] = { 1 => { EdResource::LANGID => group } }
52
+ end
53
+
54
+ def set_version_info(blob)
55
+ @tree[RT_VERSION] = { 1 => { EdResource::LANGID => blob.b } }
56
+ end
57
+
58
+ def manifest
59
+ @tree.dig(RT_MANIFEST, 1)&.values&.first
60
+ end
61
+
62
+ def set_manifest(xml)
63
+ @tree[RT_MANIFEST] = { 1 => { EdResource::LANGID => xml.b } }
64
+ end
65
+
66
+ def set_strings(id_to_value)
67
+ table = (@tree[RT_STRING] ||= {})
68
+ id_to_value.group_by { |id, _| id.to_i / 16 }.each do |bundle, pairs|
69
+ table[bundle + 1] = { EdResource::LANGID => EdResource::Build.string_bundle(pairs.to_h) }
70
+ end
71
+ end
72
+
73
+ def to_bytes
74
+ out = @bytes.dup
75
+ rva = align(@sections.map { |s| s[:va] + [s[:vsize], s[:rsize]].max }.max, @section_align)
76
+ section = build_tree(rva)
77
+ raw_size = align(section.bytesize, @file_align)
78
+ raw_at = align(@sections.map { |s| s[:raw] + s[:rsize] }.max, @file_align)
79
+ # The new section goes where the file's sections end; whatever follows
80
+ # them (an Authenticode signature, an overlay) would be cut off.
81
+ if @bytes.bytesize > raw_at
82
+ raise "the PE file has #{@bytes.bytesize - raw_at} bytes after its last section " \
83
+ "(a signature or other appended data); its resources cannot be rewritten"
84
+ end
85
+ table_at = @section_table + @sections.size * 40
86
+ raise "no room in the PE header for another section" if table_at + 40 > @size_of_headers
87
+
88
+ out = out.byteslice(0, raw_at).ljust(raw_at, "\0".b)
89
+ out << section << ("\0".b * (raw_size - section.bytesize))
90
+ header = [".rsrc".b.ljust(8, "\0".b), section.bytesize, rva, raw_size, raw_at, 0, 0, 0, 0,
91
+ SECTION_CHARACTERISTICS].pack("a8VVVVVVvvV")
92
+ out[table_at, 40] = header
93
+ out[@pe + 6, 2] = [@sections.size + 1].pack("v")
94
+ out[@optional + 56, 4] = [align(rva + section.bytesize, @section_align)].pack("V") # SizeOfImage
95
+ out[@optional + 64, 4] = [0].pack("V") # CheckSum: not checked for user-mode executables
96
+ out[@data_dirs + 16, 8] = [rva, section.bytesize].pack("VV")
97
+ if (old = @sections.find { |s| s[:va] == @resource_rva })
98
+ out[old[:header], 8] = ".rsrc_o".b.ljust(8, "\0".b)
99
+ end
100
+ out
101
+ end
102
+
103
+ private
104
+
105
+ def align(value, to) = (value + to - 1) / to * to
106
+
107
+ def parse_headers
108
+ raise "not a PE executable" unless @bytes.start_with?("MZ")
109
+
110
+ @pe = @bytes.unpack1("V", offset: 0x3c)
111
+ raise "not a PE executable" unless @bytes.byteslice(@pe, 4) == "PE\0\0".b
112
+
113
+ count = @bytes.unpack1("v", offset: @pe + 6)
114
+ size_of_optional = @bytes.unpack1("v", offset: @pe + 20)
115
+ @optional = @pe + 24
116
+ pe32_plus = @bytes.unpack1("v", offset: @optional) == 0x20b
117
+ @section_align, @file_align = @bytes.unpack("VV", offset: @optional + 32)
118
+ @size_of_headers = @bytes.unpack1("V", offset: @optional + 60)
119
+ @data_dirs = @optional + (pe32_plus ? 112 : 96)
120
+ @resource_rva, @resource_size = @bytes.unpack("VV", offset: @data_dirs + 16)
121
+ @section_table = @optional + size_of_optional
122
+ @sections = (0...count).map do |i|
123
+ at = @section_table + i * 40
124
+ vsize, va, rsize, raw = @bytes.unpack("VVVV", offset: at + 8)
125
+ { header: at, vsize: vsize, va: va, rsize: rsize, raw: raw }
126
+ end
127
+ end
128
+
129
+ def rva_to_offset(rva)
130
+ s = @sections.find { |sec| rva >= sec[:va] && rva < sec[:va] + [sec[:vsize], sec[:rsize]].max }
131
+ raise "RVA #{rva} is in no section" unless s
132
+
133
+ rva - s[:va] + s[:raw]
134
+ end
135
+
136
+ def read_tree
137
+ return {} if @resource_rva.zero?
138
+
139
+ @root = rva_to_offset(@resource_rva)
140
+ read_directory(0, 3)
141
+ end
142
+
143
+ def read_directory(offset, depth)
144
+ at = @root + offset
145
+ named, ids = @bytes.unpack("vv", offset: at + 12)
146
+ (0...(named + ids)).to_h do |i|
147
+ name, target = @bytes.unpack("VV", offset: at + 16 + i * 8)
148
+ key = name & 0x80000000 != 0 ? read_name(name & 0x7fffffff) : name
149
+ value =
150
+ if target & 0x80000000 != 0
151
+ read_directory(target & 0x7fffffff, depth - 1)
152
+ else
153
+ data_rva, size = @bytes.unpack("VV", offset: @root + target)
154
+ @bytes.byteslice(rva_to_offset(data_rva), size)
155
+ end
156
+ [key, value]
157
+ end
158
+ end
159
+
160
+ def read_name(offset)
161
+ length = @bytes.unpack1("v", offset: @root + offset)
162
+ @bytes.byteslice(@root + offset + 2, length * 2).force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8)
163
+ end
164
+
165
+ # Serializes the tree the way resource compilers lay it out: all
166
+ # directories, then name strings, then data entries, then the data.
167
+ def build_tree(rva)
168
+ levels = []
169
+ collect = lambda do |node, depth|
170
+ dir = { entries: sort_entries(node) }
171
+ levels << dir
172
+ dir[:entries].each do |entry|
173
+ entry[:child] = depth < 2 ? collect.(entry[:value], depth + 1) : { leaf: entry[:value] }
174
+ end
175
+ dir
176
+ end
177
+ root = collect.(@tree, 0)
178
+
179
+ offset = 0
180
+ levels.each { |d| d[:offset] = offset; offset += 16 + d[:entries].size * 8 }
181
+ names = {}
182
+ levels.each do |d|
183
+ d[:entries].each do |e|
184
+ next unless e[:key].is_a?(String)
185
+
186
+ names[e[:key]] ||= begin
187
+ at = offset
188
+ offset += 2 + e[:key].encode(Encoding::UTF_16LE).bytesize
189
+ at
190
+ end
191
+ end
192
+ end
193
+ offset = align(offset, 4)
194
+ leaves = levels.flat_map { |d| d[:entries].map { |e| e[:child] } }.select { |c| c[:leaf] }
195
+ leaves.each { |l| l[:entry] = offset; offset += 16 }
196
+ leaves.each { |l| offset = align(offset, 8); l[:data] = offset; offset += l[:leaf].bytesize }
197
+
198
+ out = "\0".b * offset
199
+ levels.each do |d|
200
+ named = d[:entries].count { |e| e[:key].is_a?(String) }
201
+ out[d[:offset], 16] = [0, 0, 0, 0, named, d[:entries].size - named].pack("VVvvvv")
202
+ d[:entries].each_with_index do |e, i|
203
+ name = e[:key].is_a?(String) ? 0x80000000 | names[e[:key]] : e[:key]
204
+ target = e[:child][:leaf] ? e[:child][:entry] : 0x80000000 | e[:child][:offset]
205
+ out[d[:offset] + 16 + i * 8, 8] = [name, target].pack("VV")
206
+ end
207
+ end
208
+ names.each do |name, at|
209
+ utf16 = name.encode(Encoding::UTF_16LE).b
210
+ out[at, 2 + utf16.bytesize] = [utf16.bytesize / 2].pack("v") + utf16
211
+ end
212
+ leaves.each do |l|
213
+ out[l[:entry], 16] = [rva + l[:data], l[:leaf].bytesize, 0, 0].pack("VVVV")
214
+ out[l[:data], l[:leaf].bytesize] = l[:leaf]
215
+ end
216
+ out
217
+ end
218
+
219
+ # Named entries first, in case-insensitive order, then IDs ascending,
220
+ # as Windows' binary search through a directory expects.
221
+ def sort_entries(node)
222
+ named, ids = node.partition { |k, _| k.is_a?(String) }
223
+ (named.sort_by { |k, _| k.upcase } + ids.sort_by(&:first)).map { |k, v| { key: k, value: v } }
224
+ end
225
+ end
226
+ end
@@ -4,7 +4,24 @@ require "pathname"
4
4
  module Ocran
5
5
  # The Pathname class in Ruby is modified to handle mixed path separators and
6
6
  # to be case-insensitive.
7
+ #
8
+ # Caveat: refinements apply only to calls written in a file that is
9
+ # `using RefinePathname`. Hash, Set, Array#uniq, Array#include? and Array#-
10
+ # call hash/eql?/== from C, which never sees a refinement, so collections
11
+ # of Pathnames keep comparing them case- and separator-sensitively. Where
12
+ # a collection has to treat two spellings of one file as the same, key it
13
+ # on Pathname#path_key instead (e.g. `paths.uniq { |p| p.path_key }`).
7
14
  module RefinePathname
15
+ # The spelling-independent form of +path+ on this platform: separators
16
+ # unified and, on case-insensitive file systems (Windows), downcased. On
17
+ # POSIX it is the path itself.
18
+ def self.path_key(path)
19
+ s = path.to_s
20
+ s = s.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
21
+ s = s.downcase if File::FNM_SYSCASE.nonzero?
22
+ s
23
+ end
24
+
8
25
  refine Pathname do
9
26
  def normalize_file_separator(s)
10
27
  if File::ALT_SEPARATOR
@@ -15,56 +32,36 @@ module Ocran
15
32
  end
16
33
  private :normalize_file_separator
17
34
 
18
- # Compares two paths for equality based on the case sensitivity of the
19
- # Ruby execution environment's file system.
20
- # If the file system is case-insensitive, it performs a case-insensitive
21
- # comparison. Otherwise, it performs a case-sensitive comparison.
22
- def pathequal(a, b)
23
- if File::FNM_SYSCASE.nonzero?
24
- a.casecmp(b) == 0
25
- else
26
- a == b
27
- end
28
- end
29
- private :pathequal
30
-
31
35
  def to_posix
32
36
  normalize_file_separator(to_s)
33
37
  end
34
38
 
39
+ # See RefinePathname.path_key. Two Pathnames denote the same file on
40
+ # this platform when their keys are equal.
41
+ def path_key
42
+ RefinePathname.path_key(self)
43
+ end
44
+
35
45
  # Checks if two Pathname objects are equal, considering the file system's
36
46
  # case sensitivity and path separators. Returns false if the other object is not
37
47
  # an Pathname.
38
- # This method enables the use of the `uniq` method on arrays of Pathname objects.
48
+ # NOTE: only calls written in refined code see this; Array#uniq and
49
+ # the like do not (see the caveat on RefinePathname).
39
50
  def eql?(other)
40
51
  return false unless other.is_a?(Pathname)
41
52
 
42
- a = normalize_file_separator(to_s)
43
- b = normalize_file_separator(other.to_s)
44
- pathequal(a, b)
53
+ path_key == other.path_key
45
54
  end
46
55
 
47
56
  alias == eql?
48
57
  alias === eql?
49
58
 
50
- # Calculates a normalized hash value for a pathname to ensure consistent
51
- # hashing across different environments, particularly in Windows.
52
- # This method first normalizes the path by:
53
- # 1. Converting the file separator from the platform-specific separator
54
- # to the common POSIX separator ('/') if necessary.
55
- # 2. Converting the path to lowercase if the filesystem is case-insensitive.
56
- # The normalized path string is then hashed, providing a stable hash value
57
- # that is consistent with the behavior of eql? method, thus maintaining
58
- # the integrity of hash-based data structures like Hash or Set.
59
+ # A hash value consistent with eql?, based on the normalized path. As
60
+ # with eql?, Hash and Set do not call this refinement.
59
61
  #
60
62
  # @return [Integer] A hash integer based on the normalized path.
61
63
  def hash
62
- path = if File::FNM_SYSCASE.nonzero?
63
- to_s.downcase
64
- else
65
- to_s
66
- end
67
- normalize_file_separator(path).hash
64
+ path_key.hash
68
65
  end
69
66
 
70
67
  # Checks if the current path is a sub path of the specified base_directory.