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,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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
51
|
-
#
|
|
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
|
-
|
|
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.
|