diff --git a/Library/Homebrew/dependency_collector.rb b/Library/Homebrew/dependency_collector.rb index bdd8a463d3..ae039e887a 100644 --- a/Library/Homebrew/dependency_collector.rb +++ b/Library/Homebrew/dependency_collector.rb @@ -81,6 +81,10 @@ class DependencyCollector Dependency.new("xz", tags) unless which("xz") end + def zstd_dep_if_needed(tags) + Dependency.new("zstd", tags) unless which("zstd") + end + def unzip_dep_if_needed(tags) Dependency.new("unzip", tags) unless which("unzip") end @@ -171,6 +175,7 @@ class DependencyCollector def parse_url_spec(url, tags) case File.extname(url) when ".xz" then xz_dep_if_needed(tags) + when ".zst" then zstd_dep_if_needed(tags) when ".zip" then unzip_dep_if_needed(tags) when ".bz2" then bzip2_dep_if_needed(tags) when ".lha", ".lzh" then Dependency.new("lha", tags) diff --git a/Library/Homebrew/extend/pathname.rb b/Library/Homebrew/extend/pathname.rb index 062a68ec3d..28abd8b0a0 100644 --- a/Library/Homebrew/extend/pathname.rb +++ b/Library/Homebrew/extend/pathname.rb @@ -231,7 +231,7 @@ class Pathname bottle_ext, = HOMEBREW_BOTTLES_EXTNAME_REGEX.match(basename).to_a return bottle_ext if bottle_ext - archive_ext = basename[/(\.(tar|cpio|pax)\.(gz|bz2|lz|xz|Z))\Z/, 1] + archive_ext = basename[/(\.(tar|cpio|pax)\.(gz|bz2|lz|xz|zst|Z))\Z/, 1] return archive_ext if archive_ext # Don't treat version numbers as extname. diff --git a/Library/Homebrew/test/support/fixtures/cask/container.tar.zst b/Library/Homebrew/test/support/fixtures/cask/container.tar.zst new file mode 100644 index 0000000000..8cf2890887 Binary files /dev/null and b/Library/Homebrew/test/support/fixtures/cask/container.tar.zst differ diff --git a/Library/Homebrew/test/unpack_strategy/zstd_spec.rb b/Library/Homebrew/test/unpack_strategy/zstd_spec.rb new file mode 100644 index 0000000000..6e9198b996 --- /dev/null +++ b/Library/Homebrew/test/unpack_strategy/zstd_spec.rb @@ -0,0 +1,10 @@ +# typed: false +# frozen_string_literal: true + +require_relative "shared_examples" + +describe UnpackStrategy::Zstd do + let(:path) { TEST_FIXTURE_DIR/"cask/container.tar.zst" } + + include_examples "UnpackStrategy::detect" +end diff --git a/Library/Homebrew/unpack_strategy.rb b/Library/Homebrew/unpack_strategy.rb index 1e0ece5101..587b5f32bc 100644 --- a/Library/Homebrew/unpack_strategy.rb +++ b/Library/Homebrew/unpack_strategy.rb @@ -44,12 +44,13 @@ module UnpackStrategy def self.strategies @strategies ||= [ - Tar, # Needs to be before Bzip2/Gzip/Xz/Lzma. + Tar, # Needs to be before Bzip2/Gzip/Xz/Lzma/Zstd. Pax, Gzip, Dmg, # Needs to be before Bzip2/Xz/Lzma. Lzma, Xz, + Zstd, Lzip, Air, # Needs to be before `Zip`. Jar, # Needs to be before `Zip`. @@ -203,3 +204,4 @@ require "unpack_strategy/uncompressed" require "unpack_strategy/xar" require "unpack_strategy/xz" require "unpack_strategy/zip" +require "unpack_strategy/zstd" diff --git a/Library/Homebrew/unpack_strategy/tar.rb b/Library/Homebrew/unpack_strategy/tar.rb index 33d6ffcb46..9b0797cfa1 100644 --- a/Library/Homebrew/unpack_strategy/tar.rb +++ b/Library/Homebrew/unpack_strategy/tar.rb @@ -20,14 +20,15 @@ module UnpackStrategy ".tbz", ".tbz2", ".tar.bz2", ".tgz", ".tar.gz", ".tlzma", ".tar.lzma", - ".txz", ".tar.xz" + ".txz", ".tar.xz", + ".tar.zst" ] end def self.can_extract?(path) return true if path.magic_number.match?(/\A.{257}ustar/n) - return false unless [Bzip2, Gzip, Lzip, Xz].any? { |s| s.can_extract?(path) } + return false unless [Bzip2, Gzip, Lzip, Xz, Zstd].any? { |s| s.can_extract?(path) } # Check if `tar` can list the contents, then it can also extract it. stdout, _, status = system_command("tar", args: ["--list", "--file", path], print_stderr: false) @@ -39,12 +40,12 @@ module UnpackStrategy sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).returns(T.untyped) } def extract_to_dir(unpack_dir, basename:, verbose:) Dir.mktmpdir do |tmpdir| - tar_path = path - - if DependencyCollector.tar_needs_xz_dependency? && Xz.can_extract?(path) - tmpdir = Pathname(tmpdir) - Xz.new(path).extract(to: tmpdir, verbose: verbose) - tar_path = tmpdir.children.first + tar_path = if DependencyCollector.tar_needs_xz_dependency? && Xz.can_extract?(path) + subextract(Xz, Pathname(tmpdir), verbose) + elsif Zstd.can_extract?(path) + subextract(Zstd, Pathname(tmpdir), verbose) + else + path end system_command! "tar", @@ -54,5 +55,13 @@ module UnpackStrategy verbose: verbose end end + + sig { + params(extractor: T.any(T.class_of(Xz), T.class_of(Zstd)), dir: Pathname, verbose: T::Boolean).returns(Pathname) + } + def subextract(extractor, dir, verbose) + extractor.new(path).extract(to: dir, verbose: verbose) + T.must(dir.children.first) + end end end diff --git a/Library/Homebrew/unpack_strategy/zstd.rb b/Library/Homebrew/unpack_strategy/zstd.rb new file mode 100644 index 0000000000..6a57373267 --- /dev/null +++ b/Library/Homebrew/unpack_strategy/zstd.rb @@ -0,0 +1,38 @@ +# typed: true +# frozen_string_literal: true + +module UnpackStrategy + # Strategy for unpacking zstd archives. + class Zstd + extend T::Sig + + include UnpackStrategy + + using Magic + + sig { returns(T::Array[String]) } + def self.extensions + [".zst"] + end + + def self.can_extract?(path) + path.magic_number.match?(/\x28\xB5\x2F\xFD/n) + end + + def dependencies + @dependencies ||= [Formula["zstd"]] + end + + private + + sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).returns(T.untyped) } + def extract_to_dir(unpack_dir, basename:, verbose:) + FileUtils.cp path, unpack_dir/basename, preserve: true + quiet_flags = verbose ? [] : ["-q"] + system_command! "unzstd", + args: [*quiet_flags, "-T0", "--", unpack_dir/basename], + env: { "PATH" => PATH.new(Formula["zstd"].opt_bin, ENV["PATH"]) }, + verbose: verbose + end + end +end