# frozen_string_literal: true

RSpec.describe Compression::Engine do
  let(:available_size) { SiteSetting.decompressed_theme_max_file_size_mb }
  let(:folder_name) { 'test' }
  let(:temp_folder) do
    path = "#{Pathname.new(Dir.tmpdir).realpath}/#{SecureRandom.hex}"
    FileUtils.mkdir(path)
    path
  end

  before do
    Dir.chdir(temp_folder) do
      FileUtils.mkdir_p("#{folder_name}/a")
      File.write("#{folder_name}/hello.txt", 'hello world')
      File.write("#{folder_name}/a/inner", 'hello world inner')
    end
  end

  after { FileUtils.rm_rf(temp_folder) }

  it 'raises an exception when the file is not supported' do
    unknown_extension = 'a_file.crazyext'
    expect { described_class.engine_for(unknown_extension) }.to raise_error Compression::Engine::UnsupportedFileExtension
  end

  describe 'compressing and decompressing files' do
    before do
      Dir.chdir(temp_folder) do
        @compressed_path = Compression::Engine.engine_for("#{folder_name}#{extension}").compress(temp_folder, folder_name)
        FileUtils.rm_rf("#{folder_name}/")
      end
    end

    context 'when working with zip files' do
      let(:extension) { '.zip' }

      it 'decompresses the folder and inspects files correctly' do
        engine = described_class.engine_for(@compressed_path)

        extract_location = "#{temp_folder}/extract_location"
        FileUtils.mkdir(extract_location)
        engine.decompress(extract_location, "#{temp_folder}/#{folder_name}.zip", available_size)

        expect(read_file("extract_location/hello.txt")).to eq("hello world")
        expect(read_file("extract_location/a/inner")).to eq("hello world inner")
      end

      it "doesn't allow files to be extracted outside the target directory" do
        FileUtils.rm_rf(temp_folder)
        FileUtils.mkdir(temp_folder)

        zip_file = "#{temp_folder}/theme.zip"
        Zip::File.open(zip_file, create: true) do |zipfile|
          zipfile.get_output_stream("child-file") do |f|
            f.puts("child file")
          end
          zipfile.get_output_stream("../escape-decompression-folder.txt") do |f|
            f.puts("file that attempts to escape the decompression destination directory")
          end
          zipfile.mkdir("child-dir")
          zipfile.get_output_stream("child-dir/grandchild-file") do |f|
            f.puts("grandchild file")
          end
        end

        extract_location = "#{temp_folder}/extract_location"
        FileUtils.mkdir(extract_location)
        engine = described_class.engine_for(zip_file)
        engine.decompress(extract_location, zip_file, available_size)
        Dir.chdir(temp_folder) do
          expect(Dir.glob("**/*")).to contain_exactly(
            "extract_location",
            "extract_location/child-file",
            "extract_location/child-dir",
            "extract_location/child-dir/grandchild-file",
            "theme.zip"
          )
        end
      end

      it "decompresses into symlinked directory" do
        real_location = "#{temp_folder}/extract_location"
        extract_location = "#{temp_folder}/is/symlinked"

        FileUtils.mkdir(real_location)
        FileUtils.mkdir_p(extract_location)
        extract_location = "#{extract_location}/extract_location"
        FileUtils.symlink(real_location, extract_location)

        engine = described_class.engine_for(@compressed_path)
        engine.decompress(extract_location, "#{temp_folder}/#{folder_name}.zip", available_size)

        expect(File.realpath(extract_location)).to eq(real_location)
        expect(read_file("is/symlinked/extract_location/hello.txt")).to eq("hello world")
        expect(read_file("is/symlinked/extract_location/a/inner")).to eq("hello world inner")
      end
    end

    context 'when working with .tar.gz files' do
      let(:extension) { '.tar.gz' }

      it 'decompresses the folder and inspects files correctly' do
        engine = described_class.engine_for(@compressed_path)

        engine.decompress(temp_folder, "#{temp_folder}/#{folder_name}.tar.gz", available_size)

        expect(read_file("test/hello.txt")).to eq("hello world")
        expect(read_file("test/a/inner")).to eq("hello world inner")
      end

      it "doesn't allow files to be extracted outside the target directory" do
        FileUtils.rm_rf(temp_folder)
        FileUtils.mkdir(temp_folder)

        tar_file = "#{temp_folder}/theme.tar"
        File.open(tar_file, "wb") do |file|
          Gem::Package::TarWriter.new(file) do |tar|
            tar.add_file("child-file", 644) do |tf|
              tf.write("child file")
            end
            tar.add_file("../escape-extraction-folder", 644) do |tf|
              tf.write("file that attempts to escape the decompression destination directory")
            end
            tar.mkdir("child-dir", 755)
            tar.add_file("child-dir/grandchild-file", 644) do |tf|
              tf.write("grandchild file")
            end
          end
        end
        tar_gz_file = "#{temp_folder}/theme.tar.gz"
        Zlib::GzipWriter.open(tar_gz_file) do |gz|
          gz.orig_name = tar_file
          gz.write(File.binread(tar_file))
        end
        FileUtils.rm(tar_file)

        extract_location = "#{temp_folder}/extract_location"
        FileUtils.mkdir(extract_location)
        engine = described_class.engine_for(tar_gz_file)
        engine.decompress(extract_location, tar_gz_file, available_size)
        Dir.chdir(temp_folder) do
          expect(Dir.glob("**/*")).to contain_exactly(
            "extract_location",
            "extract_location/child-file",
            "extract_location/child-dir",
            "extract_location/child-dir/grandchild-file",
          )
        end
      end

      it "decompresses into symlinked directory" do
        real_location = "#{temp_folder}/extract_location"
        extract_location = "#{temp_folder}/is/symlinked"

        FileUtils.mkdir(real_location)
        FileUtils.mkdir_p(extract_location)
        extract_location = "#{extract_location}/extract_location"
        FileUtils.symlink(real_location, extract_location)

        engine = described_class.engine_for(@compressed_path)
        engine.decompress(extract_location, "#{temp_folder}/#{folder_name}.tar.gz", available_size)

        expect(File.realpath(extract_location)).to eq(real_location)
        expect(read_file("is/symlinked/extract_location/test/hello.txt")).to eq("hello world")
        expect(read_file("is/symlinked/extract_location/test/a/inner")).to eq("hello world inner")
      end
    end

    context 'when working with .tar files' do
      let(:extension) { '.tar' }

      it 'decompress the folder and inspect files correctly' do
        engine = described_class.engine_for(@compressed_path)

        engine.decompress(temp_folder, "#{temp_folder}/#{folder_name}.tar", available_size)

        expect(read_file("test/hello.txt")).to eq("hello world")
        expect(read_file("test/a/inner")).to eq("hello world inner")
      end
    end
  end

  def read_file(relative_path)
    File.read("#{temp_folder}/#{relative_path}")
  end
end