ocran 1.3.17 → 1.4.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 +306 -272
- data/LICENSE.txt +22 -22
- data/README.md +549 -531
- data/exe/ocran +5 -5
- data/ext/extconf.rb +15 -0
- data/lib/ocran/build_constants.rb +16 -16
- data/lib/ocran/build_facade.rb +17 -17
- data/lib/ocran/build_helper.rb +110 -105
- data/lib/ocran/command_output.rb +22 -22
- data/lib/ocran/dir_builder.rb +162 -0
- data/lib/ocran/direction.rb +623 -386
- data/lib/ocran/file_path_set.rb +69 -69
- data/lib/ocran/gem_spec_queryable.rb +172 -172
- data/lib/ocran/host_config_helper.rb +57 -37
- data/lib/ocran/inno_setup_script_builder.rb +111 -111
- data/lib/ocran/launcher_batch_builder.rb +85 -85
- data/lib/ocran/library_detector.rb +61 -61
- data/lib/ocran/library_detector_posix.rb +55 -0
- data/lib/ocran/option.rb +323 -273
- data/lib/ocran/refine_pathname.rb +104 -104
- data/lib/ocran/runner.rb +115 -105
- data/lib/ocran/runtime_environment.rb +46 -46
- data/lib/ocran/stub_builder.rb +298 -224
- data/lib/ocran/version.rb +5 -5
- data/lib/ocran/windows_command_escaping.rb +15 -15
- data/lib/ocran.rb +7 -7
- data/share/ocran/lzma.exe +0 -0
- data/src/Makefile +75 -0
- data/src/edicon.c +161 -0
- data/src/error.c +100 -0
- data/src/error.h +66 -0
- data/src/inst_dir.c +334 -0
- data/src/inst_dir.h +157 -0
- data/src/lzma/7zTypes.h +529 -0
- data/src/lzma/Compiler.h +43 -0
- data/src/lzma/LzmaDec.c +1363 -0
- data/src/lzma/LzmaDec.h +236 -0
- data/src/lzma/Precomp.h +10 -0
- data/src/script_info.c +246 -0
- data/src/script_info.h +7 -0
- data/src/stub.c +133 -0
- data/src/stub.manifest +29 -0
- data/src/stub.rc +3 -0
- data/src/system_utils.c +1002 -0
- data/src/system_utils.h +209 -0
- data/src/system_utils_posix.c +500 -0
- data/src/unpack.c +574 -0
- data/src/unpack.h +85 -0
- data/src/vit-ruby.ico +0 -0
- metadata +55 -22
- data/share/ocran/edicon.exe +0 -0
- data/share/ocran/stub.exe +0 -0
- data/share/ocran/stubw.exe +0 -0
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "tempfile"
|
|
3
|
-
require_relative "file_path_set"
|
|
4
|
-
require_relative "windows_command_escaping"
|
|
5
|
-
|
|
6
|
-
module Ocran
|
|
7
|
-
class InnoSetupScriptBuilder
|
|
8
|
-
ISCC_CMD = "ISCC"
|
|
9
|
-
ISCC_SUCCESS = 0
|
|
10
|
-
ISCC_INVALID_PARAMS = 1
|
|
11
|
-
ISCC_COMPILATION_FAILED = 2
|
|
12
|
-
|
|
13
|
-
extend WindowsCommandEscaping
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
def compile(iss_filename, quiet: false)
|
|
17
|
-
unless system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
|
|
18
|
-
raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
cmd_line = [ISCC_CMD]
|
|
22
|
-
cmd_line << "/Q" if quiet
|
|
23
|
-
cmd_line << iss_filename
|
|
24
|
-
system(*cmd_line)
|
|
25
|
-
|
|
26
|
-
case $?&.exitstatus
|
|
27
|
-
when ISCC_SUCCESS
|
|
28
|
-
# ISCC reported success
|
|
29
|
-
when ISCC_INVALID_PARAMS
|
|
30
|
-
raise "ISCC reports invalid command line parameters"
|
|
31
|
-
when ISCC_COMPILATION_FAILED
|
|
32
|
-
raise "ISCC reports that compilation failed"
|
|
33
|
-
else
|
|
34
|
-
raise "ISCC failed to run"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
include WindowsCommandEscaping
|
|
40
|
-
|
|
41
|
-
def initialize(inno_setup_script)
|
|
42
|
-
# ISSC generates the installer files relative to the directory of the
|
|
43
|
-
# ISS file. Therefore, it is necessary to create Tempfiles in the
|
|
44
|
-
# working directory.
|
|
45
|
-
@build_file = Tempfile.new("", Dir.pwd)
|
|
46
|
-
if inno_setup_script
|
|
47
|
-
IO.copy_stream(inno_setup_script, @build_file)
|
|
48
|
-
end
|
|
49
|
-
@dirs = FilePathSet.new
|
|
50
|
-
@files = FilePathSet.new
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def build
|
|
54
|
-
@build_file.tap do |f|
|
|
55
|
-
if @dirs.any?
|
|
56
|
-
f.puts
|
|
57
|
-
f.puts "[Dirs]"
|
|
58
|
-
@dirs.each { |_source, target| f.puts build_dir_item(target) }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
if @files.any?
|
|
62
|
-
f.puts
|
|
63
|
-
f.puts "[Files]"
|
|
64
|
-
@files.each { |source, target| f.puts build_file_item(source, target) }
|
|
65
|
-
end
|
|
66
|
-
end.close
|
|
67
|
-
path
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def path
|
|
71
|
-
@build_file.to_path
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def compile(verbose: false)
|
|
75
|
-
InnoSetupScriptBuilder.compile(path, quiet: !verbose)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def mkdir(target)
|
|
79
|
-
@dirs.add?("/", target)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def cp(source, target)
|
|
83
|
-
unless File.exist?(source)
|
|
84
|
-
raise "The file does not exist (#{source})"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
@files.add?(source, target)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def build_dir_item(target)
|
|
91
|
-
name = File.join("{app}", target)
|
|
92
|
-
"Name: #{quote_and_escape(name)};"
|
|
93
|
-
end
|
|
94
|
-
private :build_dir_item
|
|
95
|
-
|
|
96
|
-
def build_file_item(source, target)
|
|
97
|
-
dest_dir = File.join("{app}", File.dirname(target))
|
|
98
|
-
s = [
|
|
99
|
-
"Source: #{quote_and_escape(source)};",
|
|
100
|
-
"DestDir: #{quote_and_escape(dest_dir)};"
|
|
101
|
-
]
|
|
102
|
-
src_name = File.basename(source)
|
|
103
|
-
dest_name = File.basename(target)
|
|
104
|
-
if src_name != dest_name
|
|
105
|
-
s << "DestName: #{quote_and_escape(dest_name)};"
|
|
106
|
-
end
|
|
107
|
-
s.join(" ")
|
|
108
|
-
end
|
|
109
|
-
private :build_file_item
|
|
110
|
-
end
|
|
111
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "tempfile"
|
|
3
|
+
require_relative "file_path_set"
|
|
4
|
+
require_relative "windows_command_escaping"
|
|
5
|
+
|
|
6
|
+
module Ocran
|
|
7
|
+
class InnoSetupScriptBuilder
|
|
8
|
+
ISCC_CMD = "ISCC"
|
|
9
|
+
ISCC_SUCCESS = 0
|
|
10
|
+
ISCC_INVALID_PARAMS = 1
|
|
11
|
+
ISCC_COMPILATION_FAILED = 2
|
|
12
|
+
|
|
13
|
+
extend WindowsCommandEscaping
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def compile(iss_filename, quiet: false)
|
|
17
|
+
unless Gem.win_platform? || system("where #{quote_and_escape(ISCC_CMD)} >NUL 2>&1")
|
|
18
|
+
raise "ISCC command not found. Is the InnoSetup directory in your PATH?"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
cmd_line = [ISCC_CMD]
|
|
22
|
+
cmd_line << "/Q" if quiet
|
|
23
|
+
cmd_line << iss_filename
|
|
24
|
+
system(*cmd_line)
|
|
25
|
+
|
|
26
|
+
case $?&.exitstatus
|
|
27
|
+
when ISCC_SUCCESS
|
|
28
|
+
# ISCC reported success
|
|
29
|
+
when ISCC_INVALID_PARAMS
|
|
30
|
+
raise "ISCC reports invalid command line parameters"
|
|
31
|
+
when ISCC_COMPILATION_FAILED
|
|
32
|
+
raise "ISCC reports that compilation failed"
|
|
33
|
+
else
|
|
34
|
+
raise "ISCC failed to run"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
include WindowsCommandEscaping
|
|
40
|
+
|
|
41
|
+
def initialize(inno_setup_script)
|
|
42
|
+
# ISSC generates the installer files relative to the directory of the
|
|
43
|
+
# ISS file. Therefore, it is necessary to create Tempfiles in the
|
|
44
|
+
# working directory.
|
|
45
|
+
@build_file = Tempfile.new("", Dir.pwd)
|
|
46
|
+
if inno_setup_script
|
|
47
|
+
IO.copy_stream(inno_setup_script, @build_file)
|
|
48
|
+
end
|
|
49
|
+
@dirs = FilePathSet.new
|
|
50
|
+
@files = FilePathSet.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build
|
|
54
|
+
@build_file.tap do |f|
|
|
55
|
+
if @dirs.any?
|
|
56
|
+
f.puts
|
|
57
|
+
f.puts "[Dirs]"
|
|
58
|
+
@dirs.each { |_source, target| f.puts build_dir_item(target) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if @files.any?
|
|
62
|
+
f.puts
|
|
63
|
+
f.puts "[Files]"
|
|
64
|
+
@files.each { |source, target| f.puts build_file_item(source, target) }
|
|
65
|
+
end
|
|
66
|
+
end.close
|
|
67
|
+
path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def path
|
|
71
|
+
@build_file.to_path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def compile(verbose: false)
|
|
75
|
+
InnoSetupScriptBuilder.compile(path, quiet: !verbose)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def mkdir(target)
|
|
79
|
+
@dirs.add?("/", target)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def cp(source, target)
|
|
83
|
+
unless File.exist?(source)
|
|
84
|
+
raise "The file does not exist (#{source})"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@files.add?(source, target)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_dir_item(target)
|
|
91
|
+
name = File.join("{app}", target)
|
|
92
|
+
"Name: #{quote_and_escape(name)};"
|
|
93
|
+
end
|
|
94
|
+
private :build_dir_item
|
|
95
|
+
|
|
96
|
+
def build_file_item(source, target)
|
|
97
|
+
dest_dir = File.join("{app}", File.dirname(target))
|
|
98
|
+
s = [
|
|
99
|
+
"Source: #{quote_and_escape(source)};",
|
|
100
|
+
"DestDir: #{quote_and_escape(dest_dir)};"
|
|
101
|
+
]
|
|
102
|
+
src_name = File.basename(source)
|
|
103
|
+
dest_name = File.basename(target)
|
|
104
|
+
if src_name != dest_name
|
|
105
|
+
s << "DestName: #{quote_and_escape(dest_name)};"
|
|
106
|
+
end
|
|
107
|
+
s.join(" ")
|
|
108
|
+
end
|
|
109
|
+
private :build_file_item
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "tempfile"
|
|
3
|
-
require_relative "windows_command_escaping"
|
|
4
|
-
require_relative "build_constants"
|
|
5
|
-
|
|
6
|
-
module Ocran
|
|
7
|
-
class LauncherBatchBuilder
|
|
8
|
-
include BuildConstants, WindowsCommandEscaping
|
|
9
|
-
|
|
10
|
-
# BATCH_FILE_DIR is a parameter expansion used in Windows batch files,
|
|
11
|
-
# representing the full path to the directory where the batch file resides.
|
|
12
|
-
# It allows for the use of pseudo-relative paths by referencing the
|
|
13
|
-
# batch file's own location without changing the working directory.
|
|
14
|
-
BATCH_FILE_DIR = "%~dp0"
|
|
15
|
-
|
|
16
|
-
# BATCH_FILE_PATH is a parameter expansion used in Windows batch files,
|
|
17
|
-
# representing the full path to the batch file itself, including the file name.
|
|
18
|
-
BATCH_FILE_PATH = "%~f0"
|
|
19
|
-
|
|
20
|
-
def initialize(chdir_before: nil, title: nil)
|
|
21
|
-
@build_file = Tempfile.new
|
|
22
|
-
@title = title
|
|
23
|
-
@chdir_before = chdir_before
|
|
24
|
-
@environments = {}
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def build
|
|
28
|
-
@build_file.tap do |f|
|
|
29
|
-
f.puts "@echo off"
|
|
30
|
-
@environments.each { |name, val| f.puts build_set_command(name, val) }
|
|
31
|
-
f.puts build_set_command("OCRAN_EXECUTABLE", BATCH_FILE_PATH)
|
|
32
|
-
f.puts build_start_command(@title, @executable, @script, *@args, chdir_before: @chdir_before)
|
|
33
|
-
f
|
|
34
|
-
end.close
|
|
35
|
-
path
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def path
|
|
39
|
-
@build_file.to_path
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def export(name, value)
|
|
43
|
-
@environments[name] = value
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def exec(executable, script, *args)
|
|
47
|
-
@executable, @script, @args = executable, script, args
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def replace_inst_dir_placeholder(s)
|
|
51
|
-
s.to_s.gsub(/#{Regexp.escape(EXTRACT_ROOT.to_s)}[\/\\]/, BATCH_FILE_DIR)
|
|
52
|
-
end
|
|
53
|
-
private :replace_inst_dir_placeholder
|
|
54
|
-
|
|
55
|
-
def build_set_command(name, value)
|
|
56
|
-
"set \"#{name}=#{replace_inst_dir_placeholder(value)}\""
|
|
57
|
-
end
|
|
58
|
-
private :build_set_command
|
|
59
|
-
|
|
60
|
-
def build_start_command(title, executable, script, *args, chdir_before: nil)
|
|
61
|
-
cmd = ["start"]
|
|
62
|
-
|
|
63
|
-
# Title for Command Prompt window title bar
|
|
64
|
-
cmd << quote_and_escape(title)
|
|
65
|
-
|
|
66
|
-
# Use /d to set the startup directory for the process,
|
|
67
|
-
# which will be BATCH_FILE_DIR/SRCDIR. This path is where
|
|
68
|
-
# the script is located, establishing the working directory
|
|
69
|
-
# at process start.
|
|
70
|
-
if chdir_before
|
|
71
|
-
cmd << "/d #{quote_and_escape("#{BATCH_FILE_DIR}#{SRCDIR}")}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
cmd << quote_and_escape("#{BATCH_FILE_DIR}#{executable}")
|
|
75
|
-
cmd << quote_and_escape("#{BATCH_FILE_DIR}#{script}")
|
|
76
|
-
cmd += args.map { |arg| quote_and_escape(replace_inst_dir_placeholder(arg)) }
|
|
77
|
-
|
|
78
|
-
# Forward batch file arguments to the command with `%*`
|
|
79
|
-
cmd << "%*"
|
|
80
|
-
|
|
81
|
-
cmd.join(" ")
|
|
82
|
-
end
|
|
83
|
-
private :build_start_command
|
|
84
|
-
end
|
|
85
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "tempfile"
|
|
3
|
+
require_relative "windows_command_escaping"
|
|
4
|
+
require_relative "build_constants"
|
|
5
|
+
|
|
6
|
+
module Ocran
|
|
7
|
+
class LauncherBatchBuilder
|
|
8
|
+
include BuildConstants, WindowsCommandEscaping
|
|
9
|
+
|
|
10
|
+
# BATCH_FILE_DIR is a parameter expansion used in Windows batch files,
|
|
11
|
+
# representing the full path to the directory where the batch file resides.
|
|
12
|
+
# It allows for the use of pseudo-relative paths by referencing the
|
|
13
|
+
# batch file's own location without changing the working directory.
|
|
14
|
+
BATCH_FILE_DIR = "%~dp0"
|
|
15
|
+
|
|
16
|
+
# BATCH_FILE_PATH is a parameter expansion used in Windows batch files,
|
|
17
|
+
# representing the full path to the batch file itself, including the file name.
|
|
18
|
+
BATCH_FILE_PATH = "%~f0"
|
|
19
|
+
|
|
20
|
+
def initialize(chdir_before: nil, title: nil)
|
|
21
|
+
@build_file = Tempfile.new
|
|
22
|
+
@title = title
|
|
23
|
+
@chdir_before = chdir_before
|
|
24
|
+
@environments = {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build
|
|
28
|
+
@build_file.tap do |f|
|
|
29
|
+
f.puts "@echo off"
|
|
30
|
+
@environments.each { |name, val| f.puts build_set_command(name, val) }
|
|
31
|
+
f.puts build_set_command("OCRAN_EXECUTABLE", BATCH_FILE_PATH)
|
|
32
|
+
f.puts build_start_command(@title, @executable, @script, *@args, chdir_before: @chdir_before)
|
|
33
|
+
f
|
|
34
|
+
end.close
|
|
35
|
+
path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def path
|
|
39
|
+
@build_file.to_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def export(name, value)
|
|
43
|
+
@environments[name] = value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exec(executable, script, *args)
|
|
47
|
+
@executable, @script, @args = executable, script, args
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def replace_inst_dir_placeholder(s)
|
|
51
|
+
s.to_s.gsub(/#{Regexp.escape(EXTRACT_ROOT.to_s)}[\/\\]/, BATCH_FILE_DIR)
|
|
52
|
+
end
|
|
53
|
+
private :replace_inst_dir_placeholder
|
|
54
|
+
|
|
55
|
+
def build_set_command(name, value)
|
|
56
|
+
"set \"#{name}=#{replace_inst_dir_placeholder(value)}\""
|
|
57
|
+
end
|
|
58
|
+
private :build_set_command
|
|
59
|
+
|
|
60
|
+
def build_start_command(title, executable, script, *args, chdir_before: nil)
|
|
61
|
+
cmd = ["start"]
|
|
62
|
+
|
|
63
|
+
# Title for Command Prompt window title bar
|
|
64
|
+
cmd << quote_and_escape(title)
|
|
65
|
+
|
|
66
|
+
# Use /d to set the startup directory for the process,
|
|
67
|
+
# which will be BATCH_FILE_DIR/SRCDIR. This path is where
|
|
68
|
+
# the script is located, establishing the working directory
|
|
69
|
+
# at process start.
|
|
70
|
+
if chdir_before
|
|
71
|
+
cmd << "/d #{quote_and_escape("#{BATCH_FILE_DIR}#{SRCDIR}")}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
cmd << quote_and_escape("#{BATCH_FILE_DIR}#{executable}")
|
|
75
|
+
cmd << quote_and_escape("#{BATCH_FILE_DIR}#{script}")
|
|
76
|
+
cmd += args.map { |arg| quote_and_escape(replace_inst_dir_placeholder(arg)) }
|
|
77
|
+
|
|
78
|
+
# Forward batch file arguments to the command with `%*`
|
|
79
|
+
cmd << "%*"
|
|
80
|
+
|
|
81
|
+
cmd.join(" ")
|
|
82
|
+
end
|
|
83
|
+
private :build_start_command
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
require "fiddle/import"
|
|
2
|
-
require "fiddle/types"
|
|
3
|
-
|
|
4
|
-
module Ocran
|
|
5
|
-
module LibraryDetector
|
|
6
|
-
# Windows API functions for handling files may return long paths,
|
|
7
|
-
# with a maximum character limit of 32,767.
|
|
8
|
-
# "\\?\" prefix(4 characters) + long path(32762 characters) + NULL = 32767 characters
|
|
9
|
-
# https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
|
10
|
-
MAX_PATH = 32767
|
|
11
|
-
|
|
12
|
-
# The byte size of the buffer given as an argument to the EnumProcessModules function.
|
|
13
|
-
# This buffer is used to store the handles of the loaded modules.
|
|
14
|
-
# If the buffer size is smaller than the number of loaded modules,
|
|
15
|
-
# it will automatically increase the buffer size and call the EnumProcessModules function again.
|
|
16
|
-
# Increasing the initial buffer size can reduce the number of iterations required.
|
|
17
|
-
# https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-modules-for-a-process
|
|
18
|
-
DEFAULT_HMODULE_BUFFER_SIZE = 1024
|
|
19
|
-
|
|
20
|
-
extend Fiddle::Importer
|
|
21
|
-
dlload "kernel32.dll", "psapi.dll"
|
|
22
|
-
|
|
23
|
-
include Fiddle::Win32Types
|
|
24
|
-
# https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
|
|
25
|
-
typealias "HMODULE", "HINSTANCE"
|
|
26
|
-
typealias "LPDWORD", "PDWORD"
|
|
27
|
-
typealias "LPWSTR", "char*"
|
|
28
|
-
|
|
29
|
-
extern "BOOL EnumProcessModules(HANDLE, HMODULE*, DWORD, LPDWORD)"
|
|
30
|
-
extern "DWORD GetModuleFileNameW(HMODULE, LPWSTR, DWORD)"
|
|
31
|
-
extern "HANDLE GetCurrentProcess()"
|
|
32
|
-
extern "DWORD GetLastError()"
|
|
33
|
-
|
|
34
|
-
class << self
|
|
35
|
-
def loaded_dlls
|
|
36
|
-
dword = "L" # A DWORD is a 32-bit unsigned integer.
|
|
37
|
-
bytes_needed = [0].pack(dword)
|
|
38
|
-
bytes = DEFAULT_HMODULE_BUFFER_SIZE
|
|
39
|
-
process_handle = GetCurrentProcess()
|
|
40
|
-
handles = while true
|
|
41
|
-
buffer = "\x00" * bytes
|
|
42
|
-
if EnumProcessModules(process_handle, buffer, buffer.bytesize, bytes_needed) == 0
|
|
43
|
-
raise "EnumProcessModules failed with error code #{GetLastError()}"
|
|
44
|
-
end
|
|
45
|
-
bytes = bytes_needed.unpack1(dword)
|
|
46
|
-
if bytes <= buffer.bytesize
|
|
47
|
-
break buffer.unpack("J#{bytes / Fiddle::SIZEOF_VOIDP}")
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
str = "\x00".encode("UTF-16LE") * MAX_PATH
|
|
51
|
-
handles.map do |handle|
|
|
52
|
-
length = GetModuleFileNameW(handle, str, str.bytesize)
|
|
53
|
-
if length == 0
|
|
54
|
-
raise "GetModuleFileNameW failed with error code #{GetLastError()}"
|
|
55
|
-
end
|
|
56
|
-
str[0, length].encode("UTF-8")
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
1
|
+
require "fiddle/import"
|
|
2
|
+
require "fiddle/types"
|
|
3
|
+
|
|
4
|
+
module Ocran
|
|
5
|
+
module LibraryDetector
|
|
6
|
+
# Windows API functions for handling files may return long paths,
|
|
7
|
+
# with a maximum character limit of 32,767.
|
|
8
|
+
# "\\?\" prefix(4 characters) + long path(32762 characters) + NULL = 32767 characters
|
|
9
|
+
# https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
|
10
|
+
MAX_PATH = 32767
|
|
11
|
+
|
|
12
|
+
# The byte size of the buffer given as an argument to the EnumProcessModules function.
|
|
13
|
+
# This buffer is used to store the handles of the loaded modules.
|
|
14
|
+
# If the buffer size is smaller than the number of loaded modules,
|
|
15
|
+
# it will automatically increase the buffer size and call the EnumProcessModules function again.
|
|
16
|
+
# Increasing the initial buffer size can reduce the number of iterations required.
|
|
17
|
+
# https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-modules-for-a-process
|
|
18
|
+
DEFAULT_HMODULE_BUFFER_SIZE = 1024
|
|
19
|
+
|
|
20
|
+
extend Fiddle::Importer
|
|
21
|
+
dlload "kernel32.dll", "psapi.dll"
|
|
22
|
+
|
|
23
|
+
include Fiddle::Win32Types
|
|
24
|
+
# https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
|
|
25
|
+
typealias "HMODULE", "HINSTANCE"
|
|
26
|
+
typealias "LPDWORD", "PDWORD"
|
|
27
|
+
typealias "LPWSTR", "char*"
|
|
28
|
+
|
|
29
|
+
extern "BOOL EnumProcessModules(HANDLE, HMODULE*, DWORD, LPDWORD)"
|
|
30
|
+
extern "DWORD GetModuleFileNameW(HMODULE, LPWSTR, DWORD)"
|
|
31
|
+
extern "HANDLE GetCurrentProcess()"
|
|
32
|
+
extern "DWORD GetLastError()"
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def loaded_dlls
|
|
36
|
+
dword = "L" # A DWORD is a 32-bit unsigned integer.
|
|
37
|
+
bytes_needed = [0].pack(dword)
|
|
38
|
+
bytes = DEFAULT_HMODULE_BUFFER_SIZE
|
|
39
|
+
process_handle = GetCurrentProcess()
|
|
40
|
+
handles = while true
|
|
41
|
+
buffer = "\x00" * bytes
|
|
42
|
+
if EnumProcessModules(process_handle, buffer, buffer.bytesize, bytes_needed) == 0
|
|
43
|
+
raise "EnumProcessModules failed with error code #{GetLastError()}"
|
|
44
|
+
end
|
|
45
|
+
bytes = bytes_needed.unpack1(dword)
|
|
46
|
+
if bytes <= buffer.bytesize
|
|
47
|
+
break buffer.unpack("J#{bytes / Fiddle::SIZEOF_VOIDP}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
str = "\x00".encode("UTF-16LE") * MAX_PATH
|
|
51
|
+
handles.map do |handle|
|
|
52
|
+
length = GetModuleFileNameW(handle, str, str.bytesize / 2)
|
|
53
|
+
if length == 0
|
|
54
|
+
raise "GetModuleFileNameW failed with error code #{GetLastError()}"
|
|
55
|
+
end
|
|
56
|
+
str[0, length].encode("UTF-8")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ocran
|
|
4
|
+
module LibraryDetector
|
|
5
|
+
# Detect loaded shared libraries on POSIX systems (Linux, macOS)
|
|
6
|
+
def self.loaded_dlls
|
|
7
|
+
if RUBY_PLATFORM.include?("linux")
|
|
8
|
+
detect_linux
|
|
9
|
+
elsif RUBY_PLATFORM.include?("darwin")
|
|
10
|
+
detect_macos
|
|
11
|
+
else
|
|
12
|
+
[]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.detect_linux
|
|
17
|
+
File.readlines("/proc/self/maps").filter_map do |line|
|
|
18
|
+
# Format: address perms offset dev ino pathname
|
|
19
|
+
# Example: 56206f09c000-56206f0bc000 r--p 00000000 101:02 1234567 /path/to/lib.so.1
|
|
20
|
+
fields = line.split
|
|
21
|
+
path = fields[5]
|
|
22
|
+
|
|
23
|
+
# Only include absolute paths to shared libraries
|
|
24
|
+
next unless path&.start_with?("/")
|
|
25
|
+
next unless path.end_with?(".so") || path.match?(/\.so\.\d/)
|
|
26
|
+
|
|
27
|
+
path
|
|
28
|
+
end.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.detect_macos
|
|
32
|
+
# On macOS, we can use `vmmap` to list the memory regions, but it's slow.
|
|
33
|
+
# A better way might be using ObjectSpace for loaded features, but that's for Ruby files.
|
|
34
|
+
# For shared libraries (.dylib), we can use `otool -L` on the ruby binary,
|
|
35
|
+
# but that only gives linked libraries, not dynamically loaded ones.
|
|
36
|
+
# For now, let's use `vmmap` or similar if available, or just rely on $LOADED_FEATURES for Ruby-based extensions.
|
|
37
|
+
|
|
38
|
+
# Using `vmmap` is one way to get mapped files:
|
|
39
|
+
libs = []
|
|
40
|
+
begin
|
|
41
|
+
IO.popen(["vmmap", Process.pid.to_s], err: [:child, :out]) do |io|
|
|
42
|
+
io.each_line do |line|
|
|
43
|
+
# Look for lines with paths ending in .dylib
|
|
44
|
+
if line =~ %r{ (/.*\.dylib)$}
|
|
45
|
+
libs << $1
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue Errno::ENOENT
|
|
50
|
+
# vmmap not available
|
|
51
|
+
end
|
|
52
|
+
libs.uniq
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|