Class: Ocran::StubBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/ocran/stub_builder.rb

Overview

Utility class that produces the actual executable. Opcodes (create_file, mkdir etc) are added by invoking methods on an instance of OcranBuilder.

Constant Summary collapse

Signature =
[0x41, 0xb6, 0xba, 0x4e].freeze
OP_CREATE_DIRECTORY =
1
OP_CREATE_FILE =
2
OP_SETENV =
3
OP_SET_SCRIPT =
4
5
DEBUG_MODE =
0x01
EXTRACT_TO_EXE_DIR =
0x02
AUTO_CLEAN_INST_DIR =
0x04
CHDIR_BEFORE_SCRIPT =
0x08
DATA_COMPRESSED =
0x10
RUN_IN_EXE_DIR =
0x20
CHDIR_TO_EXE_DIR =
0x40
WINDOWS =
Gem.win_platform?
STUB_PATH =
File.expand_path(WINDOWS ? "stub.exe" : "stub", base_dir)
STUBW_PATH =
WINDOWS ? File.expand_path("stubw.exe", base_dir) : nil
LZMA_PATH =
WINDOWS ? File.expand_path("lzma.exe", base_dir) : nil
LZMA_CMD =
WINDOWS ? [LZMA_PATH, "e", "-si", "-so"] : find_posix_lzma_cmd

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, chdir_before: nil, chdir_to_exe_dir: nil, debug_extract: nil, debug_mode: nil, enable_compression: nil, gui_mode: nil, icon_path: nil, run_in_exe_dir: nil, stub_path: nil) ⇒ StubBuilder

chdir_before: When set to true, the working directory is changed to the application's deployment location at runtime.

chdir_to_exe_dir: When set to true, the working directory is changed to the directory containing the (stub) executable before the script starts. This makes relative file access resolve next to the executable regardless of how it was invoked. Mutually exclusive with chdir_before.

debug_mode: When the debug_mode option is set to true, the stub will output debug information when the exe file is executed. Debug mode can also be enabled within the directive code using the enable_debug_mode method. This option is provided to transition to debug mode from the initialization point of the stub.

debug_extract: When set to true, the runtime file is extracted to the directory where the executable resides, and the extracted files remain even after the application exits. When set to false, the runtime file is extracted to the system's temporary directory, and the extracted files are deleted after the application exits.

gui_mode: When set to true, the stub does not display a console window at startup. Errors are shown in a dialog window. When set to false, the stub reports errors through the console window.

icon_path: Specifies the path to the icon file to be embedded in the stub's resources.

run_in_exe_dir: When set to true, the stub runs the application directly from its own directory instead of extracting to a temporary directory. Used for installer (Inno Setup) wrapper executables where the application files are installed next to the stub (pre-1.4/OCRA behavior). The directory is never deleted on exit.

stub_path: Path to a stub binary to package with instead of the pre-built stub shipped with the gem (e.g. an APE stub freshly compiled with cosmocc, see --cosmo). When set, it takes precedence over both STUB_PATH and STUBW_PATH.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/ocran/stub_builder.rb', line 119

def initialize(path, chdir_before: nil, chdir_to_exe_dir: nil,
               debug_extract: nil, debug_mode: nil,
               enable_compression: nil, gui_mode: nil, icon_path: nil,
               run_in_exe_dir: nil, stub_path: nil)
  @dirs = FilePathSet.new
  @files = FilePathSet.new
  @data_size = 0

  if icon_path && !File.exist?(icon_path)
    raise "Icon file #{icon_path} not found"
  end

  output_dir = File.dirname(path)
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
  stub_tmp = File.join(output_dir, ".ocran_stub_#{$$}_#{Time.now.to_i}")
  stub_src = if stub_path
               stub_path
             elsif gui_mode && WINDOWS
               STUBW_PATH
             else
               STUB_PATH
             end
  IO.copy_stream(stub_src, stub_tmp)
  stub = stub_tmp

  # Clear any invalid security directory entries from the stub (Windows only)
  self.class.clear_invalid_security_entry(stub) if WINDOWS

  # Embed icon resource (Windows only)
  if icon_path && WINDOWS
    require_relative "ed_icon"
    EdIcon.update_icon(stub, icon_path.to_s)
  end

  File.open(stub, "ab") do |of|
    @of = of
    @opcode_offset = @of.size

    write_header(debug_mode, debug_extract, chdir_before, enable_compression, run_in_exe_dir, chdir_to_exe_dir)

    b = proc {
      yield(self)
    }

    if enable_compression && LZMA_CMD
      compress(&b)
    else
      b.yield
    end

    write_footer
  end

  File.rename(stub, path)
  File.chmod(0755, path) unless WINDOWS
end

Instance Attribute Details

#data_sizeObject (readonly)

Returns the value of attribute data_size.



47
48
49
# File 'lib/ocran/stub_builder.rb', line 47

def data_size
  @data_size
end

Class Method Details

.clear_invalid_security_entry(file_path) ⇒ Object

Clear invalid security directory entries from PE executables This is necessary because some linkers may set non-zero values in the security directory even when there is no actual digital signature



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ocran/stub_builder.rb', line 52

def self.clear_invalid_security_entry(file_path)
  data = File.binread(file_path)
  return unless data.size > 64 # Minimum PE header size

  # Read DOS header to find PE header offset
  e_lfanew_offset = 60
  pe_offset = data[e_lfanew_offset, 4].unpack1("L")
  return if pe_offset + 160 > data.size # Not enough room for headers

  # Calculate security directory offset
  # PE signature (4) + FILE_HEADER (20) + partial OPTIONAL_HEADER to DataDirectory
  security_entry_offset = pe_offset + 4 + 20 + 128

  # Read security directory entry (VirtualAddress and Size)
  sec_addr = data[security_entry_offset, 4].unpack1("L")
  sec_size = data[security_entry_offset + 4, 4].unpack1("L")

  # Check if security entry is invalid (points beyond file or size is 0)
  if sec_size != 0 && (sec_addr == 0 || sec_addr >= data.size || sec_addr + sec_size > data.size)
    # Clear the invalid security entry
    data[security_entry_offset, 8] = "\x00" * 8
    File.binwrite(file_path, data)
  end
end

.find_posix_lzma_cmdObject



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ocran/stub_builder.rb', line 33

def self.find_posix_lzma_cmd
  if system("which lzma > /dev/null 2>&1")
    ["lzma", "--compress", "--stdout"]
  elsif system("which xz > /dev/null 2>&1")
    ["xz", "--format=lzma", "--compress", "--stdout"]
  elsif File.exist?("/opt/homebrew/bin/lzma")
    ["/opt/homebrew/bin/lzma", "--compress", "--stdout"]
  else
    nil
  end
end

Instance Method Details

#cp(source, target) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/ocran/stub_builder.rb', line 189

def cp(source, target)
  unless File.exist?(source)
    raise "The file does not exist (#{source})"
  end

  return unless @files.add?(source, target)

  write_opcode(OP_CREATE_FILE)
  write_path(target)
  write_file(source)
end

#exec(image, script, *argv) ⇒ Object

Specifies the final application script to be launched, which can be called from any position in the data stream. It cannot be specified more than once.

You can omit setting OP_SET_SCRIPT without issues, in which case the stub terminates without launching anything after performing other runtime operations.



207
208
209
210
211
212
213
214
215
# File 'lib/ocran/stub_builder.rb', line 207

def exec(image, script, *argv)
  if @script_set
    raise "Script is already set"
  end
  @script_set = true

  write_opcode(OP_SET_SCRIPT)
  write_string_array(convert_to_native(image), convert_to_native(script), *argv)
end

#export(name, value) ⇒ Object



217
218
219
220
221
# File 'lib/ocran/stub_builder.rb', line 217

def export(name, value)
  write_opcode(OP_SETENV)
  write_string(name.to_s)
  write_string(value.to_s)
end

#mkdir(target) ⇒ Object



176
177
178
179
180
181
# File 'lib/ocran/stub_builder.rb', line 176

def mkdir(target)
  return unless @dirs.add?("/", target)

  write_opcode(OP_CREATE_DIRECTORY)
  write_path(target)
end


183
184
185
186
187
# File 'lib/ocran/stub_builder.rb', line 183

def symlink(link_path, target)
  write_opcode(OP_CREATE_SYMLINK)
  write_path(link_path)
  write_string(target.to_s)
end