Module: Ryac::Packer

Defined in:
lib/ryac/packer.rb,
sig/ryac/packer.rbs

Overview

Self-extracting output encoding, applied after minification. The emitted file is a short plain-Ruby stub followed by the real program compressed after END; running it inflates the bytes and evals them with $0 as the file name, so a $PROGRAM_NAME == __FILE__ launcher still fires and relative requires resolve as they would from the plain artifact.

This is an output format, not a stage: stages rewrite Ruby into equivalent Ruby and re-parse to a fixed point, while a packed file is opaque bytes. Two formats:

:self — an LZSS decoder (4KB window, 3..18-byte matches) inlined in
      the stub; no require at all, runs on any Ruby.
:zlib — deflate via the zlib default gem; smaller, but dead on a Ruby
      built without it.

Constant Summary collapse

FORMATS =

Returns:

  • (Array[Symbol])
%i[self zlib].freeze
SELF_STUB =

Returns:

  • (String)
's=DATA.binmode.read;o="".b;i=0;while i<s.size;f=s.getbyte i;i+=1;' \
'8.times{break if i>=s.size;if f&1>0;o<<s.getbyte(i);i+=1;else;' \
'a=s.getbyte i;b=s.getbyte i+1;i+=2;d=(a<<4|b>>4)+1;l=(b&15)+3;' \
'l.times{o<<o.getbyte(o.size-d)};end;f>>=1};end;' \
'eval o.force_encoding("UTF-8"),nil,$0' \
"\n__END__\n"
ZLIB_STUB =

Returns:

  • (String)
'require"zlib";eval Zlib.inflate(DATA.binmode.read).force_encoding("UTF-8"),nil,$0' \
"\n__END__\n"

Class Method Summary collapse

Class Method Details

.pack(source, format) ⇒ String

Parameters:

  • source (String)
  • format (String, Symbol)

Returns:

  • (String)


38
39
40
41
42
43
44
45
46
47
# File 'lib/ryac/packer.rb', line 38

def self.pack(source, format)
  reject_data_readers(source)
  case resolve_format(format)
  when :zlib
    require 'zlib'
    ZLIB_STUB.b + Zlib::Deflate.deflate(source, Zlib::BEST_COMPRESSION)
  else
    SELF_STUB.b + lzss_compress(source)
  end
end

.reject_data_readers(source) ⇒ void

This method returns an undefined value.

The stub consumes the packed file's one END/DATA stream, so a program carrying its own data section or reading DATA would read compressed garbage instead.

Parameters:

  • source (String)

Raises:



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/ryac/packer.rb', line 52

def self.reject_data_readers(source)
  result = Prism.parse(source)
  offending = result.data_loc ? '__END__' : nil
  unless offending
    AstUtils.each_node(result.value) do |node|
      case node
      when Prism::ConstantReadNode
        offending = 'DATA' if node.name == :DATA
      when Prism::ConstantPathNode
        offending = 'DATA' if node.parent.nil? && node.name == :DATA
      end
      break if offending
    end
  end
  return unless offending

  raise MinifyError, "cannot pack: the program uses #{offending}, and the self-extracting stub owns the packed file's data section"
end

.resolve_format(value) ⇒ Symbol

Parameters:

  • value (String, Symbol)

Returns:

  • (Symbol)

Raises:

  • (ArgumentError)


31
32
33
34
35
36
# File 'lib/ryac/packer.rb', line 31

def self.resolve_format(value)
  format = value.to_sym
  return format if FORMATS.include?(format)

  raise ArgumentError, "Invalid pack format: #{value} (valid: #{FORMATS.join(', ')})"
end