2018-07-01 23:35:29 +02:00
|
|
|
class UnpackStrategy
|
|
|
|
# length of the longest regex (currently TarUnpackStrategy)
|
|
|
|
MAX_MAGIC_NUMBER_LENGTH = 262
|
|
|
|
private_constant :MAX_MAGIC_NUMBER_LENGTH
|
|
|
|
|
|
|
|
def self.strategies
|
|
|
|
@strategies ||= [
|
|
|
|
JarUnpackStrategy,
|
2018-07-12 22:15:21 +02:00
|
|
|
LuaRockUnpackStrategy,
|
|
|
|
MicrosoftOfficeXmlUnpackStrategy,
|
2018-07-01 23:35:29 +02:00
|
|
|
ZipUnpackStrategy,
|
|
|
|
XarUnpackStrategy,
|
|
|
|
CompressUnpackStrategy,
|
|
|
|
TarUnpackStrategy,
|
|
|
|
GzipUnpackStrategy,
|
|
|
|
Bzip2UnpackStrategy,
|
|
|
|
XzUnpackStrategy,
|
|
|
|
LzipUnpackStrategy,
|
|
|
|
GitUnpackStrategy,
|
|
|
|
MercurialUnpackStrategy,
|
|
|
|
SubversionUnpackStrategy,
|
|
|
|
CvsUnpackStrategy,
|
|
|
|
FossilUnpackStrategy,
|
|
|
|
BazaarUnpackStrategy,
|
|
|
|
P7ZipUnpackStrategy,
|
|
|
|
RarUnpackStrategy,
|
|
|
|
LhaUnpackStrategy,
|
|
|
|
].freeze
|
|
|
|
end
|
|
|
|
private_class_method :strategies
|
|
|
|
|
2018-07-09 22:11:26 +02:00
|
|
|
def self.detect(path, ref_type: nil, ref: nil)
|
2018-07-01 23:35:29 +02:00
|
|
|
magic_number = if path.directory?
|
|
|
|
""
|
|
|
|
else
|
2018-07-09 20:04:33 +02:00
|
|
|
File.binread(path, MAX_MAGIC_NUMBER_LENGTH) || ""
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
strategy = strategies.detect do |s|
|
|
|
|
s.can_extract?(path: path, magic_number: magic_number)
|
|
|
|
end
|
|
|
|
|
|
|
|
# This is so that bad files produce good error messages.
|
|
|
|
strategy ||= case path.extname
|
2018-07-09 20:04:33 +02:00
|
|
|
when ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz", ".tar.xz", ".txz"
|
2018-07-01 23:35:29 +02:00
|
|
|
TarUnpackStrategy
|
|
|
|
when ".zip"
|
|
|
|
ZipUnpackStrategy
|
|
|
|
else
|
|
|
|
UncompressedUnpackStrategy
|
|
|
|
end
|
|
|
|
|
2018-07-09 22:11:26 +02:00
|
|
|
strategy.new(path, ref_type: ref_type, ref: ref)
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
attr_reader :path
|
|
|
|
|
2018-07-09 22:11:26 +02:00
|
|
|
def initialize(path, ref_type: nil, ref: nil)
|
2018-07-01 23:35:29 +02:00
|
|
|
@path = Pathname(path).expand_path
|
2018-07-09 22:11:26 +02:00
|
|
|
@ref_type = ref_type
|
|
|
|
@ref = ref
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def extract(to: nil, basename: nil)
|
2018-07-09 20:04:33 +02:00
|
|
|
basename ||= path.basename
|
2018-07-01 23:35:29 +02:00
|
|
|
unpack_dir = Pathname(to || Dir.pwd).expand_path
|
|
|
|
unpack_dir.mkpath
|
|
|
|
extract_to_dir(unpack_dir, basename: basename)
|
|
|
|
end
|
2018-07-16 19:00:49 +02:00
|
|
|
|
|
|
|
def extract_nestedly(to: nil, basename: nil)
|
|
|
|
Dir.mktmpdir do |tmp_unpack_dir|
|
|
|
|
tmp_unpack_dir = Pathname(tmp_unpack_dir)
|
|
|
|
|
|
|
|
extract(to: tmp_unpack_dir, basename: basename)
|
|
|
|
|
|
|
|
children = tmp_unpack_dir.children
|
|
|
|
|
2018-07-16 20:10:22 +02:00
|
|
|
if children.count == 1 && !children.first.directory?
|
2018-07-16 19:00:49 +02:00
|
|
|
s = self.class.detect(children.first)
|
|
|
|
|
2018-07-19 00:47:52 +02:00
|
|
|
s.extract_nestedly(to: to)
|
2018-07-16 19:00:49 +02:00
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
DirectoryUnpackStrategy.new(tmp_unpack_dir).extract(to: to)
|
|
|
|
end
|
|
|
|
end
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
class DirectoryUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
path.directory?
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-18 13:03:29 +02:00
|
|
|
FileUtils.cp_r File.join(path, "."), unpack_dir, preserve: true
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class UncompressedUnpackStrategy < UnpackStrategy
|
2018-07-16 21:36:18 +02:00
|
|
|
alias extract_nestedly extract
|
|
|
|
|
2018-07-01 23:35:29 +02:00
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
FileUtils.cp path, unpack_dir/basename, preserve: true
|
|
|
|
end
|
2018-07-12 22:15:21 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
class MicrosoftOfficeXmlUnpackStrategy < UncompressedUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
return false unless ZipUnpackStrategy.can_extract?(path: path, magic_number: magic_number)
|
|
|
|
|
|
|
|
# Check further if the ZIP is a Microsoft Office XML document.
|
|
|
|
magic_number.match?(/\APK\003\004/n) &&
|
|
|
|
magic_number.match?(%r{\A.{30}(\[Content_Types\]\.xml|_rels/\.rels)}n)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class LuaRockUnpackStrategy < UncompressedUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
return false unless ZipUnpackStrategy.can_extract?(path: path, magic_number: magic_number)
|
|
|
|
|
|
|
|
# Check further if the ZIP is a LuaRocks package.
|
2018-07-20 17:21:24 +02:00
|
|
|
out, = Open3.capture3("zipinfo", "-1", path)
|
|
|
|
out.encode(Encoding::UTF_8, invalid: :replace)
|
|
|
|
.split("\n")
|
|
|
|
.any? { |line| line.match?(%r{\A[^/]+.rockspec\Z}) }
|
2018-07-12 22:15:21 +02:00
|
|
|
end
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
class JarUnpackStrategy < UncompressedUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
return false unless ZipUnpackStrategy.can_extract?(path: path, magic_number: magic_number)
|
|
|
|
|
|
|
|
# Check further if the ZIP is a JAR/WAR.
|
2018-07-20 17:21:24 +02:00
|
|
|
out, = Open3.capture3("zipinfo", "-1", path)
|
|
|
|
out.encode(Encoding::UTF_8, invalid: :replace)
|
|
|
|
.split("\n")
|
|
|
|
.include?("META-INF/MANIFEST.MF")
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class P7ZipUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\A7z\xBC\xAF\x27\x1C/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system "7zr", "x", "-y", "-bd", "-bso0", path, "-o#{unpack_dir}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class ZipUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\APK(\003\004|\005\006)/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system "unzip", "-qq", path, "-d", unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class TarUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
return true if magic_number.match?(/\A.{257}ustar/n)
|
|
|
|
|
|
|
|
# Check if `tar` can list the contents, then it can also extract it.
|
|
|
|
IO.popen(["tar", "tf", path], err: File::NULL) do |stdout|
|
|
|
|
!stdout.read(1).nil?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system "tar", "xf", path, "-C", unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CompressUnpackStrategy < TarUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\A\037\235/n)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-16 19:00:49 +02:00
|
|
|
class XzUnpackStrategy < UnpackStrategy
|
2018-07-01 23:35:29 +02:00
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\A\xFD7zXZ\x00/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-09 21:47:55 +02:00
|
|
|
super
|
2018-07-16 19:40:29 +02:00
|
|
|
safe_system Formula["xz"].opt_bin/"unxz", "-q", "-T0", unpack_dir/basename
|
2018-07-19 12:45:50 +02:00
|
|
|
extract_nested_tar(unpack_dir)
|
2018-07-09 21:20:00 +02:00
|
|
|
end
|
|
|
|
|
2018-07-19 12:45:50 +02:00
|
|
|
def extract_nested_tar(unpack_dir)
|
2018-07-09 21:20:00 +02:00
|
|
|
return unless DependencyCollector.tar_needs_xz_dependency?
|
|
|
|
return if (children = unpack_dir.children).count != 1
|
|
|
|
return if (tar = children.first).extname != ".tar"
|
|
|
|
|
|
|
|
Dir.mktmpdir do |tmpdir|
|
|
|
|
tmpdir = Pathname(tmpdir)
|
|
|
|
FileUtils.mv tar, tmpdir/tar.basename
|
2018-07-19 12:45:50 +02:00
|
|
|
TarUnpackStrategy.new(tmpdir/tar.basename).extract(to: unpack_dir)
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-16 19:00:49 +02:00
|
|
|
class Bzip2UnpackStrategy < UnpackStrategy
|
2018-07-01 23:35:29 +02:00
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\ABZh/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-16 19:00:49 +02:00
|
|
|
FileUtils.cp path, unpack_dir/basename, preserve: true
|
2018-07-09 21:47:55 +02:00
|
|
|
safe_system "bunzip2", "-q", unpack_dir/basename
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-16 19:00:49 +02:00
|
|
|
class GzipUnpackStrategy < UnpackStrategy
|
2018-07-01 23:35:29 +02:00
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\A\037\213/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-16 19:00:49 +02:00
|
|
|
FileUtils.cp path, unpack_dir/basename, preserve: true
|
2018-07-09 21:47:55 +02:00
|
|
|
safe_system "gunzip", "-q", "-N", unpack_dir/basename
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-16 19:00:49 +02:00
|
|
|
class LzipUnpackStrategy < UnpackStrategy
|
2018-07-01 23:35:29 +02:00
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\ALZIP/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-16 19:00:49 +02:00
|
|
|
FileUtils.cp path, unpack_dir/basename, preserve: true
|
2018-07-09 21:47:55 +02:00
|
|
|
safe_system Formula["lzip"].opt_bin/"lzip", "-d", "-q", unpack_dir/basename
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class XarUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\Axar!/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system "xar", "-x", "-f", path, "-C", unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class RarUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\ARar!/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
2018-07-14 03:25:42 +02:00
|
|
|
safe_system Formula["unrar"].opt_bin/"unrar", "x", "-inul", path, unpack_dir
|
2018-07-01 23:35:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class LhaUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
magic_number.match?(/\A..-(lh0|lh1|lz4|lz5|lzs|lh\\40|lhd|lh2|lh3|lh4|lh5)-/n)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system Formula["lha"].opt_bin/"lha", "xq2w=#{unpack_dir}", path
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class GitUnpackStrategy < DirectoryUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
super && (path/".git").directory?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class SubversionUnpackStrategy < DirectoryUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
super && (path/".svn").directory?
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
safe_system "svn", "export", "--force", path, unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CvsUnpackStrategy < DirectoryUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
super && (path/"CVS").directory?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class MercurialUnpackStrategy < DirectoryUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
super && (path/".hg").directory?
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
with_env "PATH" => PATH.new(Formula["mercurial"].opt_bin, ENV["PATH"]) do
|
|
|
|
safe_system "hg", "--cwd", path, "archive", "--subrepos", "-y", "-t", "files", unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class FossilUnpackStrategy < UnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
return false unless magic_number.match?(/\ASQLite format 3\000/n)
|
|
|
|
|
|
|
|
# Fossil database is made up of artifacts, so the `artifact` table must exist.
|
|
|
|
query = "select count(*) from sqlite_master where type = 'view' and name = 'artifact'"
|
|
|
|
Utils.popen_read("sqlite3", path, query).to_i == 1
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
args = if @ref_type && @ref
|
|
|
|
[@ref]
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
|
|
|
|
with_env "PATH" => PATH.new(Formula["fossil"].opt_bin, ENV["PATH"]) do
|
|
|
|
safe_system "fossil", "open", path, *args, chdir: unpack_dir
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class BazaarUnpackStrategy < DirectoryUnpackStrategy
|
|
|
|
def self.can_extract?(path:, magic_number:)
|
|
|
|
super && (path/".bzr").directory?
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_to_dir(unpack_dir, basename:)
|
|
|
|
super
|
|
|
|
|
|
|
|
# The export command doesn't work on checkouts (see https://bugs.launchpad.net/bzr/+bug/897511).
|
|
|
|
FileUtils.rm_r unpack_dir/".bzr"
|
|
|
|
end
|
|
|
|
end
|