diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 288eea3b223..24f3573baab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -179,6 +179,7 @@ /modules/regex/tests/ @godotengine/core @godotengine/tests /modules/zip/ @godotengine/core /modules/zip/doc_classes/ @godotengine/core @godotengine/documentation +/modules/zip/tests @godotengine/core @godotengine/tests # Platform diff --git a/modules/zip/SCsub b/modules/zip/SCsub index 0bab3ff5f9a..0ae5ae91093 100644 --- a/modules/zip/SCsub +++ b/modules/zip/SCsub @@ -8,3 +8,7 @@ env_zip = env_modules.Clone() # Module files env_zip.add_source_files(env.modules_sources, "*.cpp") + +if env["tests"]: + env_zip.Append(CPPDEFINES=["TESTS_ENABLED"]) + env_zip.add_source_files(env.modules_sources, "./tests/*.cpp") diff --git a/modules/zip/doc_classes/ZIPPacker.xml b/modules/zip/doc_classes/ZIPPacker.xml index 1d8b613f313..330323c40a1 100644 --- a/modules/zip/doc_classes/ZIPPacker.xml +++ b/modules/zip/doc_classes/ZIPPacker.xml @@ -62,6 +62,11 @@ + + + The compression level used when [method start_file] is called. Use [enum ZIPPacker.CompressionLevel] as a reference. + + Create a new zip archive at the given path. @@ -72,5 +77,17 @@ Add new files to the existing zip archive at the given path. + + Start a file with the default Deflate compression level ([code]6[/code]). This is a good compromise between speed and file size. + + + Start a file with no compression. This is also known as the "Store" compression mode and is the fastest method of packing files inside a ZIP archive. Consider using this mode for files that are already compressed (such as JPEG, PNG, MP3, or Ogg Vorbis files). + + + Start a file with the fastest Deflate compression level ([code]1[/code]). This is fast to compress, but results in larger file sizes than [constant COMPRESSION_DEFAULT]. Decompression speed is generally unaffected by the chosen compression level. + + + Start a file with the the best Deflate compression level ([code]9[/code]). This is slow to compress, but results in smaller file sizes than [constant COMPRESSION_DEFAULT]. Decompression speed is generally unaffected by the chosen compression level. + diff --git a/modules/zip/doc_classes/ZIPReader.xml b/modules/zip/doc_classes/ZIPReader.xml index 6a5f8623af7..bacf2a5f77b 100644 --- a/modules/zip/doc_classes/ZIPReader.xml +++ b/modules/zip/doc_classes/ZIPReader.xml @@ -61,6 +61,14 @@ Must be called after [method open]. + + + + + + Returns the compression level of the file in the loaded zip archive. Returns [code]-1[/code] if the file doesn't exist or any other error occurs. Must be called after [method open]. + + diff --git a/modules/zip/tests/data/test.zip b/modules/zip/tests/data/test.zip new file mode 100644 index 00000000000..fcca5e4f81d Binary files /dev/null and b/modules/zip/tests/data/test.zip differ diff --git a/modules/zip/tests/test_zip.cpp b/modules/zip/tests/test_zip.cpp new file mode 100644 index 00000000000..0692639c2d3 --- /dev/null +++ b/modules/zip/tests/test_zip.cpp @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* test_zip.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "test_zip.h" + +namespace TestZip { + +void check_file_size(const String &p_path, int p_expected_size) { + Ref f = FileAccess::open(p_path, FileAccess::READ); + CHECK(f.is_valid()); + CHECK(f->get_length() == p_expected_size); +} + +} // namespace TestZip diff --git a/modules/zip/tests/test_zip.h b/modules/zip/tests/test_zip.h new file mode 100644 index 00000000000..7f8376e6f81 --- /dev/null +++ b/modules/zip/tests/test_zip.h @@ -0,0 +1,105 @@ +/**************************************************************************/ +/* test_zip.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "tests/test_macros.h" +#include "tests/test_utils.h" + +#include "../zip_packer.h" +#include "../zip_reader.h" + +namespace TestZip { + +void check_file_size(const String &p_path, int p_expected_size); + +TEST_CASE("[ZIPPacker] default compression") { + const String path = TestUtils::get_temp_path("compressed.zip"); + Ref packer; + packer.instantiate(); + Error open_result = packer->open(path, ZIPPacker::APPEND_CREATE); + CHECK(open_result == OK); + Error start_file_result = packer->start_file("demo.txt"); + CHECK(start_file_result == OK); + String text = "hello world!"; + Error write_file_result = packer->write_file(text.to_utf8_buffer()); + CHECK(write_file_result == OK); + Error close_file_result = packer->close_file(); + CHECK(close_file_result == OK); + Error close_result = packer->close(); + CHECK(close_result == OK); + check_file_size(path, 128); +} + +TEST_CASE("[ZIPPacker] no compression") { + const String path = TestUtils::get_temp_path("uncompressed.zip"); + Ref packer; + packer.instantiate(); + Error open_result = packer->open(path, ZIPPacker::APPEND_CREATE); + CHECK(open_result == OK); + packer->set_compression_level(ZIPPacker::COMPRESSION_NONE); + Error start_file_result = packer->start_file("demo.txt"); + CHECK(start_file_result == OK); + String text = "hello world!"; + Error write_file_result = packer->write_file(text.to_utf8_buffer()); + CHECK(write_file_result == OK); + Error close_file_result = packer->close_file(); + CHECK(close_file_result == OK); + Error close_result = packer->close(); + CHECK(close_result == OK); + check_file_size(path, 131); +} + +TEST_CASE("[ZIPReader] read files") { + String test_data = String("modules/zip/tests/data/").path_join("test.zip"); + Ref reader; + reader.instantiate(); + Error open_result = reader->open(test_data); + CHECK(open_result == OK); + + const String hello_path = "hello.txt"; + const String world_path = "world.txt"; + PackedStringArray expected_files; + expected_files.push_back(hello_path); + expected_files.push_back(world_path); + CHECK(reader->get_files() == expected_files); + + const String expected_hello_text = "hello world!"; + const String expected_world_text = "game over!"; + PackedByteArray hello_bytes = reader->read_file(hello_path, false); + PackedByteArray world_bytes = reader->read_file(world_path, true); + CHECK(hello_bytes == expected_hello_text.to_utf8_buffer()); + CHECK(world_bytes == expected_world_text.to_utf8_buffer()); + + CHECK(reader->get_compression_level(hello_path, true) == 6); + CHECK(reader->get_compression_level(world_path, false) == 9); +} + +} // namespace TestZip diff --git a/modules/zip/zip_packer.cpp b/modules/zip/zip_packer.cpp index 4e182f97878..b40929e3895 100644 --- a/modules/zip/zip_packer.cpp +++ b/modules/zip/zip_packer.cpp @@ -55,6 +55,15 @@ Error ZIPPacker::close() { return err; } +void ZIPPacker::set_compression_level(int p_compression_level) { + ERR_FAIL_COND_MSG(p_compression_level < Z_DEFAULT_COMPRESSION || p_compression_level > Z_BEST_COMPRESSION, "Invalid compression level."); + compression_level = p_compression_level; +} + +int ZIPPacker::get_compression_level() const { + return compression_level; +} + Error ZIPPacker::start_file(const String &p_path) { ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker must be opened before use."); @@ -81,7 +90,7 @@ Error ZIPPacker::start_file(const String &p_path) { 0, nullptr, Z_DEFLATED, - Z_DEFAULT_COMPRESSION, + compression_level, 0, -MAX_WBITS, DEF_MEM_LEVEL, @@ -107,6 +116,9 @@ Error ZIPPacker::close_file() { void ZIPPacker::_bind_methods() { ClassDB::bind_method(D_METHOD("open", "path", "append"), &ZIPPacker::open, DEFVAL(Variant(APPEND_CREATE))); + ClassDB::bind_method(D_METHOD("set_compression_level", "compression_level"), &ZIPPacker::set_compression_level); + ClassDB::bind_method(D_METHOD("get_compression_level"), &ZIPPacker::get_compression_level); + ADD_PROPERTY(PropertyInfo(Variant::INT, "compression_level"), "set_compression_level", "get_compression_level"); ClassDB::bind_method(D_METHOD("start_file", "path"), &ZIPPacker::start_file); ClassDB::bind_method(D_METHOD("write_file", "data"), &ZIPPacker::write_file); ClassDB::bind_method(D_METHOD("close_file"), &ZIPPacker::close_file); @@ -115,9 +127,15 @@ void ZIPPacker::_bind_methods() { BIND_ENUM_CONSTANT(APPEND_CREATE); BIND_ENUM_CONSTANT(APPEND_CREATEAFTER); BIND_ENUM_CONSTANT(APPEND_ADDINZIP); + + BIND_ENUM_CONSTANT(COMPRESSION_DEFAULT); + BIND_ENUM_CONSTANT(COMPRESSION_NONE); + BIND_ENUM_CONSTANT(COMPRESSION_FAST); + BIND_ENUM_CONSTANT(COMPRESSION_BEST); } -ZIPPacker::ZIPPacker() {} +ZIPPacker::ZIPPacker() { +} ZIPPacker::~ZIPPacker() { if (fa.is_valid()) { diff --git a/modules/zip/zip_packer.h b/modules/zip/zip_packer.h index 39405567003..87c2d0da8d2 100644 --- a/modules/zip/zip_packer.h +++ b/modules/zip/zip_packer.h @@ -40,6 +40,7 @@ class ZIPPacker : public RefCounted { Ref fa; zipFile zf = nullptr; + int compression_level = Z_DEFAULT_COMPRESSION; protected: static void _bind_methods(); @@ -51,9 +52,19 @@ public: APPEND_ADDINZIP = 2, }; + enum CompressionLevel { + COMPRESSION_DEFAULT = Z_DEFAULT_COMPRESSION, + COMPRESSION_NONE = Z_NO_COMPRESSION, + COMPRESSION_FAST = Z_BEST_SPEED, + COMPRESSION_BEST = Z_BEST_COMPRESSION, + }; + Error open(const String &p_path, ZipAppend p_append); Error close(); + void set_compression_level(int p_compression_level); + int get_compression_level() const; + Error start_file(const String &p_path); Error write_file(const Vector &p_data); Error close_file(); @@ -63,3 +74,4 @@ public: }; VARIANT_ENUM_CAST(ZIPPacker::ZipAppend) +VARIANT_ENUM_CAST(ZIPPacker::CompressionLevel) diff --git a/modules/zip/zip_reader.cpp b/modules/zip/zip_reader.cpp index 76f48edb692..5817c1c2e7e 100644 --- a/modules/zip/zip_reader.cpp +++ b/modules/zip/zip_reader.cpp @@ -140,6 +140,25 @@ bool ZIPReader::file_exists(const String &p_path, bool p_case_sensitive) { return true; } +int ZIPReader::get_compression_level(const String &p_path, bool p_case_sensitive) { + ERR_FAIL_COND_V_MSG(fa.is_null(), -1, "ZIPReader must be opened before use."); + + int cs = p_case_sensitive ? 1 : 2; + if (unzLocateFile(uzf, p_path.utf8().get_data(), cs) != UNZ_OK) { + return -1; + } + + int method; + int level; + if (unzOpenCurrentFile2(uzf, &method, &level, 1) != UNZ_OK) { + return -1; + } + + unzCloseCurrentFile(uzf); + + return level; +} + ZIPReader::ZIPReader() {} ZIPReader::~ZIPReader() { @@ -154,4 +173,5 @@ void ZIPReader::_bind_methods() { ClassDB::bind_method(D_METHOD("get_files"), &ZIPReader::get_files); ClassDB::bind_method(D_METHOD("read_file", "path", "case_sensitive"), &ZIPReader::read_file, DEFVAL(Variant(true))); ClassDB::bind_method(D_METHOD("file_exists", "path", "case_sensitive"), &ZIPReader::file_exists, DEFVAL(Variant(true))); + ClassDB::bind_method(D_METHOD("get_compression_level", "path", "case_sensitive"), &ZIPReader::get_compression_level, DEFVAL(Variant(true))); } diff --git a/modules/zip/zip_reader.h b/modules/zip/zip_reader.h index 02ba01b1a1e..8135afb687d 100644 --- a/modules/zip/zip_reader.h +++ b/modules/zip/zip_reader.h @@ -51,6 +51,7 @@ public: PackedStringArray get_files(); PackedByteArray read_file(const String &p_path, bool p_case_sensitive); bool file_exists(const String &p_path, bool p_case_sensitive); + int get_compression_level(const String &p_path, bool p_case_sensitive); ZIPReader(); ~ZIPReader();