ffi-libarchive
| Language | Ruby |
|---|---|
| License | Apache 2.0 (libarchive itself: BSD 2-Clause) |
| Repository | github.com/chef/ffi-libarchive |
| Documentation | README, libarchive |
| Depends on | libarchive shared library, ffi gem |
| Read / write | yes / 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#
# 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#
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#
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#
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:
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:
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#
- Check the libarchive version with
bsdtar --version; old distributions ship versions without RAR5 support. - For encrypted or multi-volume archives, call the
unraror7zcommand line tool withsystemorOpen3.