Module: Leakferret::Binary
- Defined in:
- lib/leakferret/binary.rb
Overview
Resolves (and, if needed, downloads) the native ‘leakferret` binary.
The binary is fetched into an absolute, user-writable cache directory rather than into the gem’s own tree. RubyGems builds extensions in a throwaway temp dir, so anything written relative to the gem during ‘gem install` is discarded — the cache path sidesteps that entirely and also lets a plain `gem install` (no extension) work.
Constant Summary collapse
- BUNDLED_DIR =
A binary vendored inside the gem, if one was shipped (normally empty).
Pathname.new(__dir__).join('bin').freeze
- CHECKSUMS =
SHA256 of each release tarball, pinned to BINARY_VERSION. The download is verified against these before the archive is ever unpacked, so a tampered or corrupted release asset is rejected instead of being executed. Because the digests live in the gem source, auditing the published gem tells you exactly which binary bytes it will run. Regenerate on every binary bump from the release’s ‘*.tar.gz.sha256` files.
{ 'aarch64-apple-darwin' => '62d7152954e3e2e50d8423c8a1e792ba1783123b8a9d8c5fbc2a71013e890992', 'aarch64-pc-windows-msvc' => '6ad3eb20a661579c11857259159f8fb55b26f72608c75ecc206fff5f9da9c800', 'x86_64-apple-darwin' => 'd8b28edf427b975412458007069a848e16cea45825e43dff3652bdcd3fd3f1d3', 'x86_64-pc-windows-msvc' => 'f447424f148a6874dc2ead208eb460a9f6b20d6ddbce6f74ca9b2d47655e1b2b', 'x86_64-unknown-linux-gnu' => 'bf24746f1188d14b2b420e760ebd374a4f88a68ea1b718e7977d8c7309a9f1da' }.freeze
Class Method Summary collapse
-
.cache_dir ⇒ Object
User-writable cache directory, namespaced by the binary version so a gem upgrade fetches a fresh binary instead of reusing a stale one.
- .cache_path ⇒ Object
- .download_url ⇒ Object
-
.ensure! ⇒ Object
Download and unpack the binary into the cache.
- .install_instructions(candidate) ⇒ Object
-
.path ⇒ Object
Absolute path to the native binary, downloading it on first use if necessary.
Class Method Details
.cache_dir ⇒ Object
User-writable cache directory, namespaced by the binary version so a gem upgrade fetches a fresh binary instead of reusing a stale one.
66 67 68 69 70 71 72 73 74 |
# File 'lib/leakferret/binary.rb', line 66 def cache_dir base = if Platform.windows? ENV['LOCALAPPDATA'] || File.join(Dir.home, 'AppData', 'Local') else ENV['XDG_CACHE_HOME'] || File.join(Dir.home, '.cache') end Pathname.new(base).join('leakferret', BINARY_VERSION) end |
.cache_path ⇒ Object
76 77 78 |
# File 'lib/leakferret/binary.rb', line 76 def cache_path cache_dir.join(Platform.binary_name) end |
.download_url ⇒ Object
80 81 82 83 |
# File 'lib/leakferret/binary.rb', line 80 def download_url 'https://github.com/leakferrethq/leakferret/releases/download/' \ "v#{BINARY_VERSION}/leakferret-#{BINARY_VERSION}-#{Platform.triple}.tar.gz" end |
.ensure! ⇒ Object
Download and unpack the binary into the cache. Idempotent: a no-op when the binary is already cached. Returns the path; raises on failure.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/leakferret/binary.rb', line 87 def ensure! dest = cache_path return dest.to_s if dest.file? require 'fileutils' require 'open-uri' require 'zlib' require 'digest' require 'stringio' require 'rubygems/package' expected = CHECKSUMS[Platform.triple] if expected.nil? raise BinaryNotFoundError, "no pinned checksum for platform #{Platform.triple}; refusing to run an " \ 'unverified binary. Build from source and set LEAKFERRET_BIN instead.' end FileUtils.mkdir_p(dest.dirname) # Download the whole tarball, verify its SHA256 against the pinned value, # and only then unpack. Nothing is written to the cache (let alone marked # executable) until the bytes match, so a tampered or truncated release # asset is rejected rather than run. tarball = URI.open(download_url, &:read) # rubocop:disable Security/Open actual = Digest::SHA256.hexdigest(tarball) unless actual.casecmp?(expected) raise BinaryNotFoundError, "checksum mismatch for #{download_url}\n" \ " expected #{expected}\n got #{actual}\n" \ 'Refusing to install a binary that does not match the pinned hash.' end # Unpack in pure Ruby (no external `tar`, which on Windows mis-reads `C:\` # as a remote host). The archive nests everything under # leakferret-<version>-<triple>/, so match by basename. found = false Zlib::GzipReader.wrap(StringIO.new(tarball)) do |gz| Gem::Package::TarReader.new(gz) do |tar| tar.each do |entry| next unless entry.file? next unless File.basename(entry.full_name) == Platform.binary_name File.binwrite(dest, entry.read) found = true end end end raise BinaryNotFoundError, "binary not found inside #{download_url}" unless found FileUtils.chmod(0o755, dest) unless Platform.windows? dest.to_s end |
.install_instructions(candidate) ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/leakferret/binary.rb', line 141 def install_instructions(candidate) <<~MSG leakferret native binary not found, and the automatic download failed. Expected it at: #{candidate} Download the binary for your platform from: https://github.com/leakferrethq/leakferret/releases then either place it at the path above or point LEAKFERRET_BIN at it. MSG end |
.path ⇒ Object
Absolute path to the native binary, downloading it on first use if necessary. Resolution order:
1. LEAKFERRET_BIN — explicit override
2. lib/leakferret/bin/ — a binary vendored in the gem
3. the per-version cache — fetched on a prior run or at install
4. download into the cache now
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/leakferret/binary.rb', line 43 def path override = ENV['LEAKFERRET_BIN'] unless override.nil? || override.empty? unless File.file?(override) raise BinaryNotFoundError, "LEAKFERRET_BIN points to a missing file: #{override}" end return override end bundled = BUNDLED_DIR.join(Platform.binary_name) return bundled.to_s if bundled.file? return cache_path.to_s if cache_path.file? ensure! raise BinaryNotFoundError, install_instructions(cache_path) unless cache_path.file? cache_path.to_s end |