All formats

ffi-libarchive

LanguageRuby
LicenseApache 2.0 (libarchive itself: BSD 2-Clause)
Repositorygithub.com/chef/ffi-libarchive
DocumentationREADME, libarchive
Depends onlibarchive shared library, ffi gem
Read / writeyes / no for RAR; RAR4 and RAR5; no passwords, no volumes, no solid RAR4

ffi-libarchive wraps the C library libarchive through Ruby FFI, so no C extension has to compile when you install the gem. It’s maintained by Chef, which uses it to unpack packages. The reader detects the archive format from the data, so the same code opens RAR, ZIP, 7z, TAR and compressed TAR files.

RAR support is whatever the installed libarchive provides: RAR4 and RAR5 compression, but no decryption, no multi-volume archives through this API, and no solid RAR4 archives. Use libarchive 3.4 or newer for RAR5.

Installation#

Shell
# the C library
sudo apt install libarchive13      # Debian / Ubuntu
brew install libarchive            # macOS
# the gem
gem install ffi-libarchive

Or in a Gemfile: gem "ffi-libarchive".

Basic usage: list and extract a RAR file#

Ruby
require "ffi-libarchive"

flags = Archive::EXTRACT_TIME | Archive::EXTRACT_SECURE_NODOTDOT | Archive::EXTRACT_SECURE_SYMLINKS

Archive::Reader.open_filename("archive.rar") do |reader|
  reader.each_entry do |entry|
    puts "#{entry.pathname} #{entry.size} bytes"
    reader.extract(entry, flags, destination: "output")
  end
end

open_filename with a block closes the archive at the end. extract creates folders as needed and writes the entry under destination.

How to list files in a RAR#

Ruby
require "ffi-libarchive"

Archive::Reader.open_filename("archive.rar") do |reader|
  reader.each_entry do |entry|
    type = entry.directory? ? "dir " : "file"
    puts "#{type} #{entry.pathname.ljust(40)} #{entry.size.to_s.rjust(10)} #{entry.mtime}"
  end
end

Moving to the next entry skips the data of the current one, so listing doesn’t decompress anything.

How to read a file from a RAR into memory#

Ruby
require "ffi-libarchive"

def read_entry(path, name)
  Archive::Reader.open_filename(path) do |reader|
    reader.each_entry do |entry|
      next unless entry.pathname == name
      data = reader.read_data
      return data.is_a?(String) ? data : ""   # read_data returns 0 for an empty file
    end
  end
  nil
end

puts read_entry("archive.rar", "docs/readme.txt")

For large entries, pass a block to read_data to get the data in chunks:

Ruby
File.open("big.bin", "wb") do |out|
  reader.read_data(64 * 1024) { |chunk| out.write(chunk) }
end

How to handle errors#

libarchive errors, such as an encrypted entry or a damaged archive, raise Archive::Error:

Ruby
begin
  Archive::Reader.open_filename("secret.rar") do |reader|
    reader.each_entry { |entry| reader.extract(entry, 0, destination: "output") }
  end
rescue Archive::Error => e
  warn "cannot extract: #{e.message}"
end

Tips#